# 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处理流程

![](https://3957301685-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FgBeNWokmyQJWAYaVPPDF%2Fuploads%2Fgit-blob-3416b0cd3a5451ca189eae075b17c6d79b2e2d9e%2Fspring-mvc.png?alt=media)

如图所示是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会使用此文件中的相应组件来补充。
