# 3.4 Web MVC

Web开发最终肯定是需要一个Web开发框架的，而MVC（Model View Controller）是Web开发中最常用的框架。从Struts1到Struts2，再到现在的Spring MVC、Jersy等等，都是MVC模式的实现框架。它将Web开发分为了模型、视图以及控制器三层，做到了职责分离，使得应用的模块能够高内聚、低耦合。

* 模型：代表业务数据和业务逻辑或者可以控制这些数据访问的模块
* 视图：对模型的展现
* 控制器：定义应用的各种行为

目前，互联网领域主要以Spring MVC为主要的Web开发框架，因此本节主要讲述Spring MVC的使用。Spring版本为4.3.7.RELEASE。

## 3.4.1 为什么是Spring MVC

Spring MVC是Spring Web的一个重要模块。其结构简单，强大不失灵活，性能也很优秀。相比起其他的框架，具有但不限于以下特点：

* 学习门槛低，易上手。
* 由于Spring MVC框架封装的比较好，因此使用Spring MVC很容易写出优秀的程序。
* Spring MVC继承了Spring框架的灵活性，非常易于扩展。
* 框架的各个组件之间松耦合。
* 支持多种视图展现。
* 可以很方便的使用Spring生态下的组件。

## 3.4.2 Spring MVC处理流程

![](/files/eKzx0aIgpnyXF6pZpbfR)

如图所示是Spring MVC的几个关键组件。对于一个用户的Web请求的处理流程一般如下：

1. 用户发起请求到DispatchServlet（在web.xml中配置, 是Spring mvc的前置控制器）。
2. 从HandlerMapping中匹配此次请求信息的handler，匹配的条件包括：请求路径、请求方法、header信息等。常用的几个HandlerMapping有：
   * SimpleUrlHandlerMapping：简单的映射一个URL到一个Handler。
   * RequestMappingHandlerMapping: 扫描RequestMapping注解，根据相关配置，绑定URL到一个Handler。
3. 获取到对应的Handler（Controller,控制器）后, 调用相应的方法。这里牵扯到一个关键组件：HandlerAdapter，是Controller的适配器，Spring MVC最终是通过HandlerAdapter来调用实际的Controller方法的。常用的有以下几个：
   * SimpleControllerHandlerAdapter: 处理实现了Controller接口的Controller。
   * RequestMappingHandlerAdapter: 处理类型为HandlerMethod的handlder, 这里使用RequestMapping注解的Controller的方法就是一种HandlerMethod。
4. Handler执行完毕，返回相应的ModelAndView。
5. 使用配置好的ViewResolver来解析返回结果。常用的几个ViewResolver:
   * UrlBasedViewResolver: 通过配置文件，根据URL把一个视图名交给到一个View来处理。
   * InternalResourceViewResolver类: 根据配置好的资源路径，解析为jsp视图,支持jstl。
   * FreeMarkerViewResolver：Freemarker的视图解析器。
6. 生成视图返回给用户。常用的几个的视图类如下：
   * MappingJackson2JsonView：使用mappingJackson输出JSON数据的视图，数据来源于ModelMap
   * FreeMarkerView: 使用FreeMarker模板引擎的视图。
   * JSTLView: 输出jsp页面，可以使用jsp标准标签库。

此外，还有HandlerInterceptor和HandlerExceptionResolver两个重要组件。

* HandlerInterceptor是请求路径上的拦截器，需要自己实现这个接口，以拦截请求，做一些对Handler的前置和后置处理工作。
* HandlerExceptionResolver是异常处理器。可以实现自己的处理器在全局层面拦截Handler抛出的Exception再做进一步的处理。Spring MVC自带了几个处理器：
  * SimpleMappingExceptionResolver： 可以将不同的异常映射到不同的jsp页面。
  * ExceptionHandlerExceptionResolver： 解析使用了@ExceptionHandler注解的方法来处理异常。
  * ResponseStatusExceptionResolver：处理@ResponseStatus注解的Exception。
  * DefaultHandlerExceptionResolver：默认的处理器，包括不支持method、不支持mediaType等等。

## 3.4.3 典型配置

要使用Spring MVC，首先就是需要配置DispatchServlet负责分发所有请求。

```
<!--web.xml-->
<servlet>
   <servlet-name>mvc-dispatcher</servlet-name>
   <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
   <init-param>
       <param-name>contextConfigLocation</param-name>
       <param-value>
           classpath:mvc-dispatcher-servlet.xml
       </param-value>
   </init-param>
   <load-on-startup>1</load-on-startup><!--是启动顺序，让这个Servlet随Servlet容器一起启动。-->
</servlet>
<servlet-mapping>
   <servlet-name>mvc-dispatcher</servlet-name>
   <url-pattern>/</url-pattern>
</servlet-mapping>
```

上面配置中,对应于servlet-mapping的url-pattern配置为/，是表示默认的URL映射，即当此次request匹配不到其他Servlet时，会默认进入此Servlet，包括静态资源请求。此外，对应于contextConfigLocation参数的mvc-dispatcher-servlet.xml则是对Spring MVC的一些配置。如果想要使用Spring的注解配置，则可以这么配置:

```
<servlet>
   <servlet-name>mvc-dispatcher</servlet-name>
   <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
   <init-param>
       <param-name>contextClass</param-name>
       <param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value>
   </init-param>
   <init-param>
       <param-name>contextConfigLocation</param-name>
       <param-value>
           me.rowkey.config.SpringWebConfig
       </param-value>
   </init-param>
   <load-on-startup>1</load-on-startup>
</servlet>
```

接着需要配置MVC相关的组件，如下：

```
<mvc:default-servlet-handler/>

<context:annotation-config/>

<mvc:annotation-driven>
   <mvc:message-converters>
       <bean
               class="org.springframework.http.converter.ByteArrayHttpMessageConverter"/>
       <bean class="org.springframework.http.converter.FormHttpMessageConverter"/>
       <bean
               class="org.springframework.http.converter.xml.SourceHttpMessageConverter"/>
       <bean
               class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter"/>
       <bean class="org.springframework.http.converter.StringHttpMessageConverter">
           <constructor-arg value="UTF-8"/>
       </bean>
   </mvc:message-converters>
</mvc:annotation-driven>

<bean
         class="org.springframework.web.servlet.view.InternalResourceViewResolver">
     <property name="cache" value="false"/>
     <property name="prefix" value="/WEB-INF/jsp/"/>
     <property name="suffix" value=".jsp"/>
     <property name="contentType" value="text/html;charset=UTF-8"/>
</bean>

<bean id="exceptionResolver"
     class="me.rowkey.exception.AppsExceptionResolver">
</bean>

<bean id="multipartResolver"
     class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
   <!-- one of the properties available; the maximum file size in bytes -->
   <property name="maxUploadSize" value="1000000000"/>
</bean>

<bean
      class="org.springframework.context.support.PropertyPlaceholderConfigurer">
  <property name="order" value="0"/>
  <property name="ignoreUnresolvablePlaceholders" value="true"/>
  <property name="locations">
      <list>
          <value>classpath:xx.properties</value>
      </list>
  </property>
</bean>
```

1. mvc:default-servlet-handler是配置默认的Servlet作为静态资源的Handler。
2. context:annotation-config是开启Spring的注解配置功能。
3. mvc:annotation-driven是开启MVC的注解驱动，如创建了RequestMappingHandlerMapping和RequestMappingHandlerAdapter来处理注解handler。此外，上面配置了一些MessageConverter来处理各种使用了@ResposneBody标记返回数据的Handler。
4. 配置了一个InternalResourceViewResolver处理返回数据。
5. 配置exceptionResolver来处理异常。
6. 配置CommonsMultipartResolver来处理文件上传。
7. 配置PropertyPlaceholderConfigurer来读取相关资源文件，从而使得在配置文件中可以使用占位符填充，在代码中可以使用@Value注解来引用properties中的值。

此外，对应于MVC的namespace，还有以下几个常用配置：

* mvc:interceptors: 配置拦截器

  ```
  <mvc:interceptors>  
      <mvc:interceptor>
          <mvc:mapping path="/**" />
          <bean class="me.rowkey.web.interceptor.MyInteceptor" /> 
      </mvc:interceptor> 
  </mvc:interceptors>
  ```
* mvc:argument-resolvers: 配置自己实现的参数解析器，可用于参数的名称转换，如：API传递的参数是underScore时，可以统一转换为lowCamel。

这里需要注意的是，如果倾向于使用注解配置，那么对应于这些XML配置，Spring都提供了对应的注解，可以参考官方文档。一个简单的注解配置如下：

```
@EnableWebMvc
@Configuration
@ComponentScan("me.rowkey.pje.web")
public class SpringWebConfig {
    
}

@ControllerAdvice
public class ExceptionCatcher {
    @ExceptionHandler
    @ResponseBody
    public String paramMissing(RuntimeException e) {
        ...
    }

}
```

上面的@ControllerAdvice是Spring Web3.2后引入的注解。主要是为了统一对Controller添加@ExceptionHandler、@InitBinder、@ModelAttribute等注解，相比起之前写一个Base Controller做好相关配置然后每一个Controller去继承这种方式简化了很多。

## 3.4.4 零XML配置

自从Servlet3.0开始，可以完全脱离XML对Spring Web项目进行配置，如下所示：

```
public class MyWebApplicationInitializer implements
                WebApplicationInitializer {

        @Override
        public void onStartup(ServletContext appContext)
                        throws ServletException {
                AnnotationConfigWebApplicationContext rootContext = new AnnotationConfigWebApplicationContext();
                rootContext.register(SpringWebConfig.class);

                appContext.addListener(new ContextLoaderListener(rootContext));

                ServletRegistration.Dynamic dispatcher = appContext.addServlet(
                                "dispatcher", new DispatcherServlet(rootContext));
                dispatcher.setLoadOnStartup(1);
                dispatcher.addMapping("/");

        }
}
```

以上就不需要再配置任何XML文件即可使得Spring MVC项目得以运行。原理如下：

1. Servlet3.0加入了一个特性: 容器启动时会使用Java的SPI（Service Provider Interface）机制在启动的时候去读取META-INF/services下的javax.servlet.ServletContainerInitializer文件，并会对其中列出的每一个ServletContainerInitializer进行实例化并调用onStartup方法。
2. Spring Web在自己的classpath：META-INF/services下的javax.servlet.ServletContainerInitializer文件中加入了org.springframework.web.SpringServletContainerInitializer一行，于是此类被实例化并调用。
3. SpringServletContainerInitializer使用HandlesTypes声明自己处理实现了WebApplicationInitializer的类，于是上面我们新建的MyWebApplicationInitializer会被实例化并调用onStartup方法。

## 3.4.5 单元测试

Spring MVC支持对Controller的单元测试。

```
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {
        "classpath:mvc-dispatcher-servlet.xml",
})
@WebAppConfiguration
public class ControllerJUnitBase {

    @Resource
    private RequestMappingHandlerMapping handlerMapping;

    @Resource
    private RequestMappingHandlerAdapter handlerAdapter;

    /**
     * 执行request对象请求的action
     *
     * @param request
     * @param response
     * @return
     * @throws Exception
     */
    public ModelAndView excuteAction(HttpServletRequest request, HttpServletResponse response) throws Exception

    {
        HandlerExecutionChain chain = handlerMapping.getHandler(request);

        final ModelAndView model = handlerAdapter.handle(request, response, chain.getHandler());
        return model;
    }
    
    @Test
    public void test throws Exception(){
        MockHttpServletRequest request = new MockHttpServletRequest();         
        request.setRequestURI("/api/user/login");
        request.addParameter("mobile", "180xxxx3360");
        request.setMethod("POST");  
        
        MockHttpServletResponse response = new MockHttpServletResponse();  
        final ModelAndView mav = this.excuteAction(request, response);  
        Assert.assertEquals("user_login", mav.getViewName());    
    }
}

```

## 3.4.6 Web参数验证

Web开发中对前端传入的参数验证是一个关键的环节，对于每一个Controller都单独做验证是一种方式，但是更好的方式则是用一套框架将这个验证流程统一起来。

在Spring Web开发中,常用的验证方式主要有两种：

* 支持Spring框架定义的Validator接口定义的校验。
* 支持JSR303 Bean Validation定义的校验规范。

### Spring Validator

此种方式是Spring框架自带的。

首先需要实现org.springframework.validation.Validator接口。

```
pulic class User{
    private String name;
    
    ...
}

public class UserValidator implements Validator {  
   
     @Override  
     public boolean supports(Class<?> clazz) {  
         return clazz.equals(User.class);  
     }  

    @Override  
    public void validate(Object target, Errors errors) { 
        ValidationUtils.rejectIfEmpty(errors, "name", "user.name.required", "用户名不能为空");  
        
        User user = (User)target;  
        int length = user.getName().length();  
        if(length > 10){  
            errors.rejectValue("name", "user.name.too_long", "用户名不能超过{20}个字符");  
        } 
    }
}   
```

其次，需要设置Validator并触发校验。在Controller里增加方法并以@InitBinder注解，并在对应的Controller method中触发。

```
@InitBinder  
protected void initBinder(WebDataBinder binder){  
    binder.setValidator(new UserValidator());  
}  

@RequestMapping (method = RequestMethod.POST)  
public String reg(@Validated User user, BindingResult result){  
    //校验没有通过  
    if(result.hasErrors()){  
        return "user";  
    }  
      
    if(user != null){  
        userService.saveUser(user);  
    }  
      
    return "user";  
}
```

如此，从页面提交的User对象可以通过我们实现的UserValidator类来校验，校验的结果信息存入BindingResult对象中。

### JSR303 Bean Validation

在Spring3.1中增加了对JSR303 Bean Validation规范的支持，不仅可以对Spring的MVC进行校验，也可以对Hibernate的存储对象进行校验，是一个通用的校验框架。

这里必须要引入hibernate-validator，并开启MVC注解支持（\<mvc:annotation-driven />）, 它是JSR303规范的具体实现。

此外，需要对要校验的meta类的属性做注解Constraints限制。JSR303定义的Constraint如下：

* @Null：验证对象是否为空
* @NotNull：验证对象是否为非空
* @AssertTrue：验证 Boolean 对象是否为 true
* @AssertFalse：验证 Boolean 对象是否为 false
* @Min：验证 Number 和 String 对象是否大等于指定的值
* @Max：验证 Number 和 String 对象是否小等于指定的值
* @DecimalMin:验证 Number 和 String 对象是否大等于指定的值，需要注意小数的精度问题
* @DecimalMax: 验证 Number 和 String 对象是否小等于指定的值，需要注意小数的精度问题
* @Size: 验证对象（Array,Collection,Map,String）长度是否在给定的范围之内
* @Digits: 验证 Number 和 String 的构成是否合法
* @Past: 验证 Date 和 Calendar 对象是否在当前时间之前
* @Future: 验证 Date 和 Calendar 对象是否在当前时间之后
* @Pattern: 验证 String 对象是否符合正则表达式的规则

此外，hibernate-validator也提供了一些注解支持，如：

* @NotEmpty: 验证对象不为null也不为empty。
* @NotBlank: 验证对象不为null也不为empty，连续的空格也认为是empty。
* @Range：验证对象在指定的范围内。

配置很简单，只要对被校验的meta注解Constraint即可。

```
pulic class User{
    @NotNull
    private String name;
    
    ...
}
```

然后，在Controller的对应方法中，给对应的参数加@Valid注解。

```
public String doRegister(@Valid User user, BindingResult result){  
   
   //校验没有通过  
   if(result.hasErrors()){  
       return "user";  
   }  
      
    if(user != null){  
        userService.saveUser(user);  
    }  
      
    return "user";  
}  
```

这样就可以完成针对输入数据User对象的校验了，校验结果保存在BindingResult对象中。需要注意的一点是，BindingResult参数如果放在验证参数的后面，那么错误信息是会绑定到此BindingResult上的，否则会抛出MethodArgumentNotValidException异常。

## 3.4.7 异步Servlet

Servlet3.0引入了异步Servlet，即Connector的线程只负责将请求派发到业务逻辑线程池即可，业务逻辑处理完成后再通过Servlet的异步上下文句柄将结果返回并响应, 能够避免Web Server的连接池被长期占用而引起性能问题。此外，异步Servlet还经常用在长轮训消息通知：客户端发起HTTP请求，设置一个较长的超时事件，服务端接受请求后如果有消息则直接返回，如果没有则hold住请求，一直等到有消息到达再发送响应。而客户端如果请求超时没有获取到消息，则继续循环/递归进行再一次的请求（超时时间可以自适应调整）。

Spring MVC提供了对异步Servlet的支持。

1. 在web.xml启用异步支持。

   ```
   <filter>
       <filter-name>Set Character Encoding</filter-name>
       <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
       <async-supported>true</async-supported>
       ...
   </filter>
   <filter-mapping>
       <filter-name>Set Character Encoding</filter-name>
       <url-pattern>/*</url-pattern>
   </filter-mapping>

   <servlet>
       <servlet-name>mvc-dispatcher</servlet-name>
       <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
       ...        
       <async-supported>true</async-supported>
   </servlet>
   ```

   这里需要注意除了Servlet之外，也要把所有经过的filter的async-supported都设置为true。
2. 实现异步Controller。

   返回结果为java.util.concurrent.Callable即为异步Contoller。

   ```
   @Controller
   @RequestMapping("/async")
   public class CallableController {
       @RequestMapping("/test")
       @ResponseBody
       public Callable<String> callable() {

           return new Callable<String>() {
               @Override
               public String call() throws Exception {
                   Thread.sleep(1000);
                   return "Asyn Controller Result";
               }
           };
       }
   }
   ```
3. 配置异步Controller使用的线程池和超时参数。

   ```
   <task:executor id="myExecutor" pool-size="7-42" queue-capacity="11"/>

   <mvc:annotation-driven>
       <mvc:async-support task-executor="myExecutor" default-timeout="2000"/>
   </mvc:annotation-driven>
   ```

   如此，处理业务的线程池即为myExecutor, 业务线程超时时间为2秒。

此外，如果想要针对具体的业务分别使用不同的线程池，那么可以通过返回org.springframework.web.context.request.async.DeferredResult进行。

```
private Executor executor = Executors.newFixedThreadPool(200); //业务线程池

@RequestMapping("/deferred")
@ResponseBody
public DeferredResult<String> quotes() {
   DeferredResult<String> deferredResult = new DeferredResult<String>(2000L); //设置超时时间为2秒
   deferredResult.onCompletion(new Runnable() {
       @Override
       public void run() {
           System.out.println("Deferred Result done !!!");
       }
   });

   executor.execute(new Runnable() {
       @Override
       public void run() {
           try {
               Thread.sleep(1000);
           } catch (InterruptedException e) {
               e.printStackTrace();
           }

           deferredResult.setResult("Deferred Result");
       }
   });

   return deferredResult;
}
```

如此，请求将会被挂起，直到在其他线程中将DeferredResult放入数据才会响应, 并且能够给DeferredResult设置回调方法，在数据返回后做相应的后续操作。

还需要注意的是，当使用异步Servlet时，Tomcat等Servlet容器的线程池大小可以设置为1。

```
<Connector port="8080" protocol="org.apache.coyote.http11.Http11AprProtocol"
               ...
               maxThreads="1" 
               minSpareThreads="1"
               .../>
```

## 3.4.8 使用提示

1. 如果遇到一个项目中即提供了JSON API又提供了Web页面，那么单单配置一个ViewResolver是不行的。这时可以使用Spring MVC的视图协商器ContentNegotiatingViewResolver。配置如下：

   ```
   <bean id="contentNegotiatingViewResolver"
         class="org.springframework.web.servlet.view.ContentNegotiatingViewResolver">
       <property name="contentNegotiationManager">
           <bean class="org.springframework.web.accept.ContentNegotiationManager">
               <constructor-arg>
                   <list>
                       <bean class="org.springframework.web.accept.PathExtensionContentNegotiationStrategy">
                           <constructor-arg>
                               <map>
                                   <entry key="html" value="text/html;charset=UTF-8"/>
                                   <entry key="json" value="application/json;charset=UTF-8"/>
                                   <entry key="xls" value="application/vnd.ms-excel"/>
                                   <entry key="pdf" value="application/pdf"/>
                               </map>
                           </constructor-arg>
                       </bean>
                       <bean class="org.springframework.web.accept.HeaderContentNegotiationStrategy"/>
                       <bean class="org.springframework.web.accept.FixedContentNegotiationStrategy">
                           <constructor-arg value="application/json;charset=UTF-8"></constructor-arg>
                       </bean>
                   </list>
               </constructor-arg>
           </bean>
       </property>
       <property name="viewResolvers">
           <list>
               <bean
                       class="org.springframework.web.servlet.view.InternalResourceViewResolver">
                   <property name="cache" value="false"/>
                   <property name="prefix" value="/WEB-INF/jsp/"/>
                   <property name="suffix" value=".jsp"/>
                   <property name="contentType" value="text/html;charset=UTF-8"/>
               </bean>
           </list>
       </property>
       <property name="defaultViews">
           <list>
               <bean
                       class="org.springframework.web.servlet.view.json.MappingJackson2JsonView">
                   <property name="objectMapper" ref="objectMapper"/>
                   <property name="contentType" value="application/json;charset=UTF-8"/>
                   <property name="modelKeys">
                       <set>
                           <value>data</value>
                           <value>status</value>
                           <value>desc</value>
                       </set>
                   </property>
               </bean>
           </list>
       </property>
   </bean>
   ```

   上面ContentNegotiationManager中配置的PathExtensionContentNegotiationStrategy表示根据path的扩展名来匹配返回的数据格式，如\*.json就返回的JSON数据格式，此外，还配置了HeaderContentNegotiationStrategy根据header里的accept信息匹配返回数据格式，最后配置一个FixedContentNegotiationStrategy作为以上都无效时的默认返回数据格式。接着，ContentNegotiatingViewResolver又配置了一个解析为jsp页面的ViewResolver来处理返回格式为text/html的请求，如果此ViewResolver匹配不上，那么最后使用默认视图defaultViews来对ModelMap的值做JSON解析，上面的配置则是仅仅取ModelMap中对应于data、status以及desc的值作为JSON的视图字段。
2. 如果使用视图解析器解析handler的数据并返回响应视图，那么当Controller中的参数具有HttpServletResponse时，假若Controller没有返回值，那么此次请求是不会走到视图解析器的。如下：

   ```
   @RequestMapping(value = "test", method = RequestMethod.GET, headers = "Accept=text/html")
   public void testApi(ModelMap modelMap, HttpServletRequest request, HttpServletResponse response, String id, int status){
       ......
   }
   ```

   Spring中对于这种含有HttpServletResponse参数的Controller认为是视图是由handler自己生成的。如果仍然想要走视图解析器，则必须要返回一个值。
3. API请求返回JSON/JSONP/XML数据的时候，可以通过@ResponseBody来注解Controller方法，并配置好对应的MessageConvertor。
4. Spring的Controller、Service、DAO等都是单实例的，因此Controller、Service、DAO等各层组件，应该设计为有行为无状态、有方法无属性，即使有属性，也只是对下一层组件的持有。而与之对比，项目中的Entity、Domain、DTO等各种实体，有状态无行为，有属性无方法，即使有方法，也只是getter和setter等，围着状态打转。
5. org/springframework/web/servlet路径下的DispatcherServlet.properties中配置了Spring MVC兜底使用的组件。即当项目的配置中缺少某一类组件的时候，Spring MVC会使用此文件中的相应组件来补充。


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://rowkey-books.gitbook.io/pragmatic-java-engineer/chapter3-framework/mvc.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
