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框架的灵活性,非常易于扩展。
3.4.2 Spring MVC处理流程
如图所示是Spring MVC的几个关键组件。对于一个用户的Web请求的处理流程一般如下:
用户发起请求到DispatchServlet(在web.xml中配置, 是Spring mvc的前置控制器)。
从HandlerMapping中匹配此次请求信息的handler,匹配的条件包括:请求路径、请求方法、header信息等。常用的几个HandlerMapping有:
SimpleUrlHandlerMapping:简单的映射一个URL到一个Handler。
RequestMappingHandlerMapping: 扫描RequestMapping注解,根据相关配置,绑定URL到一个Handler。
获取到对应的Handler(Controller,控制器)后, 调用相应的方法。这里牵扯到一个关键组件:HandlerAdapter,是Controller的适配器,Spring MVC最终是通过HandlerAdapter来调用实际的Controller方法的。常用的有以下几个:
SimpleControllerHandlerAdapter: 处理实现了Controller接口的Controller。
RequestMappingHandlerAdapter: 处理类型为HandlerMethod的handlder, 这里使用RequestMapping注解的Controller的方法就是一种HandlerMethod。
Handler执行完毕,返回相应的ModelAndView。
使用配置好的ViewResolver来解析返回结果。常用的几个ViewResolver:
UrlBasedViewResolver: 通过配置文件,根据URL把一个视图名交给到一个View来处理。
InternalResourceViewResolver类: 根据配置好的资源路径,解析为jsp视图,支持jstl。
FreeMarkerViewResolver:Freemarker的视图解析器。
生成视图返回给用户。常用的几个的视图类如下:
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负责分发所有请求。
Copy <!--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的注解配置,则可以这么配置:
Copy <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相关的组件,如下:
Copy <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>
mvc:default-servlet-handler是配置默认的Servlet作为静态资源的Handler。
context:annotation-config是开启Spring的注解配置功能。
mvc:annotation-driven是开启MVC的注解驱动,如创建了RequestMappingHandlerMapping和RequestMappingHandlerAdapter来处理注解handler。此外,上面配置了一些MessageConverter来处理各种使用了@ResposneBody标记返回数据的Handler。
配置了一个InternalResourceViewResolver处理返回数据。
配置exceptionResolver来处理异常。
配置CommonsMultipartResolver来处理文件上传。
配置PropertyPlaceholderConfigurer来读取相关资源文件,从而使得在配置文件中可以使用占位符填充,在代码中可以使用@Value注解来引用properties中的值。
此外,对应于MVC的namespace,还有以下几个常用配置:
mvc:interceptors: 配置拦截器
Copy <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都提供了对应的注解,可以参考官方文档。一个简单的注解配置如下:
Copy @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项目进行配置,如下所示:
Copy 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项目得以运行。原理如下:
Servlet3.0加入了一个特性: 容器启动时会使用Java的SPI(Service Provider Interface)机制在启动的时候去读取META-INF/services下的javax.servlet.ServletContainerInitializer文件,并会对其中列出的每一个ServletContainerInitializer进行实例化并调用onStartup方法。
Spring Web在自己的classpath:META-INF/services下的javax.servlet.ServletContainerInitializer文件中加入了org.springframework.web.SpringServletContainerInitializer一行,于是此类被实例化并调用。
SpringServletContainerInitializer使用HandlesTypes声明自己处理实现了WebApplicationInitializer的类,于是上面我们新建的MyWebApplicationInitializer会被实例化并调用onStartup方法。
3.4.5 单元测试
Spring MVC支持对Controller的单元测试。
Copy @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接口。
Copy 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中触发。
Copy @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如下:
@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。
配置很简单,只要对被校验的meta注解Constraint即可。
Copy pulic class User{
@NotNull
private String name;
...
}
然后,在Controller的对应方法中,给对应的参数加@Valid注解。
Copy 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的支持。
在web.xml启用异步支持。
Copy <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。
实现异步Controller。
返回结果为java.util.concurrent.Callable即为异步Contoller。
Copy @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";
}
};
}
}
配置异步Controller使用的线程池和超时参数。
Copy <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进行。
Copy 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。
Copy <Connector port="8080" protocol="org.apache.coyote.http11.Http11AprProtocol"
...
maxThreads="1"
minSpareThreads="1"
.../>
3.4.8 使用提示
如果遇到一个项目中即提供了JSON API又提供了Web页面,那么单单配置一个ViewResolver是不行的。这时可以使用Spring MVC的视图协商器ContentNegotiatingViewResolver。配置如下:
Copy <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的视图字段。
如果使用视图解析器解析handler的数据并返回响应视图,那么当Controller中的参数具有HttpServletResponse时,假若Controller没有返回值,那么此次请求是不会走到视图解析器的。如下:
Copy @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自己生成的。如果仍然想要走视图解析器,则必须要返回一个值。
API请求返回JSON/JSONP/XML数据的时候,可以通过@ResponseBody来注解Controller方法,并配置好对应的MessageConvertor。
Spring的Controller、Service、DAO等都是单实例的,因此Controller、Service、DAO等各层组件,应该设计为有行为无状态、有方法无属性,即使有属性,也只是对下一层组件的持有。而与之对比,项目中的Entity、Domain、DTO等各种实体,有状态无行为,有属性无方法,即使有方法,也只是getter和setter等,围着状态打转。
org/springframework/web/servlet路径下的DispatcherServlet.properties中配置了Spring MVC兜底使用的组件。即当项目的配置中缺少某一类组件的时候,Spring MVC会使用此文件中的相应组件来补充。