Spring MVC 体系结构

image.png

  1. 整个过程始于客户端发出一个 HTTP 请求,Web 应用服务器接收到这个请求。如果匹配 DispatcherServlet 的请求映射路径,则 Web 容器将该请求转交给 DispatcherServlet 处理。

  2. DispatcherServlet 接收到这个请求后,将根据请求的信息(URL、HTTP 方法、请求报文头、请求参数、Cookie 等)及 HandlerMapping 的配置获取 Handler。

  3. 当 DispatcherServlet 根据 HandlerMapping 得到对应当前请求的 Handler 后,通过 HandlerAdapter 适配器对 Handler 进行封装,再以统一的适配器接口对 Handler 方法进行调用。

  4. 处理器完成业务逻辑的处理后将返回一个 ModelAndView 给 DispatcherServlet,ModelAndView 包含了视图逻辑名和模型数据信息。

  5. ModelAndView 中包含的是“逻辑视图名”而非真正的视图对象,DispatcherServlet 借由 ViewResolver 完成逻辑视图名到真实视图对象的解析工作。

  6. 当得到真实的视图对象 View 后,DispatcherServlet 就使用这个 View 对象对 ModelAndView 中的模型数据进行视图渲染。最终客户端得到的响应消息可能是一个普通的 HTML 页面,也可能是一个 XML 或 JSON 串等不同的媒体形式。

    1. HandlerMapping

    在第二步中,DispatcherServlet 将请求交给处理器映射器(HandlerMapping),让它找出对应处理该请求的 Handler 时,HandlerMapping 会先通过 getHandler 方法找到对应该请求的 HandlerExecutionChain。

    1. public interface HandlerMapping {
    2. @Nullable
    3. HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception;
    4. }

    HandlerExecutionChain 是一个执行链,它包含一个处理该请求的处理器(Handler),同时包含若干个对该请求实施拦截的拦截器(HandlerInterceptor)。

    1. public class HandlerExecutionChain {
    2. // 处理器
    3. private final Object handler;
    4. // 处理器拦截器
    5. @Nullable
    6. private HandlerInterceptor[] interceptors;
    7. @Nullable
    8. private List<HandlerInterceptor> interceptorList;
    9. private int interceptorIndex = -1;
    10. }

    当 HandlerMapping 返回 HandlerExecutionChain 后,DispatcherServlet 将请求交给定义在执行链中的拦截器和处理器一并处理。请求在被 Handler 执行的前后,链中装配的 HandlerInterceptor 会实施拦截操作。
    image.png

    2. HandlerAdapter

    HandlerMapping 完成 URL 与 Handler 的映射关系,HandlerAdapter 用来自定义各种 Handler。
    Spring MVC - 图4

    • HttpRequestHandlerAdapter:可以继承 HttpRequestHandler 接口,所有的 Handler 可以实现其 handRequest() 方法,这个方法没有返回值。
  1. public class HttpRequestHandlerAdapter implements HandlerAdapter {
  2. @Override
  3. public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
  4. ((HttpRequestHandler) handler).handleRequest(request, response);
  5. return null;
  6. }
  7. }
  • SimpleControllerHandlerAdapter:可以继承 Controller接口,所有的 Handler 可以实现其 hand() 方法,该方法会返回 ModelAndView 对象,用于后续的模板渲染。
  1. public class SimpleControllerHandlerAdapter implements HandlerAdapter {
  2. @Override
  3. public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
  4. return ((Controller) handler).handleRequest(request, response);
  5. }
  6. }
  • SimpleServletHandlerAdapter:可以直接继承 Servlet 接口,可以将一个 Servlet 作为一个 Handler 来处理这个请求。
  1. public class SimpleServletHandlerAdapter implements HandlerAdapter {
  2. @Override
  3. public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
  4. ((Servlet) handler).service(request, response);
  5. return null;
  6. }
  7. }

Spring MVC 工作原理

思考以下三个问题:

  • DispatcherServlet 如何截获特定的 HTTP 请求并交由 Spring MVC 框架处理?

  • 位于 Web 层的 WebApplicationContext 如何与位于业务层的 ApplicationContext 建立关联,以使 Web 层的 Bean 可以调用业务层的 Bean?

  • 如何初始化 Spring MVC 的各个组件,并将它们装配到 DispatcherServlet 中?

1. WebApplicationContext 初始化Spring MVC - 图5

HttpServlet 初始化时会调用 HttpServletBean 的 init() 方法,在该方法内部会调用 initServletBean() 方法来完成 WebApplicationContext 容器对象的创建。

  1. @Override
  2. protected final void initServletBean() throws ServletException {
  3. ......
  4. this.webApplicationContext = initWebApplicationContext();
  5. initFrameworkServlet();
  6. }

onRefresh() 方法会在成功刷新上下文后调用,由 DispatcherServlet 子类实现,完成 DispatcherServlet 的初始化逻辑。

  1. protected WebApplicationContext initWebApplicationContext() {
  2. ......
  3. if (!this.refreshEventReceived) {
  4. synchronized (this.onRefreshMonitor) {
  5. onRefresh(wac);
  6. }
  7. }
  8. ......
  9. }

2. DispatcherServlet 截获 URL 请求

通过 web.xml 配置 DispatcherServlet:

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee"
  3. xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_4.xsd"
  4. version="2.4">
  5. <!-- 业务层和持久层的Spring配置文件,这些配置文件被父Spring容器所使用 -->
  6. <listener>
  7. <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
  8. </listener>
  9. <context-param>
  10. <param-name>contextConfigLocation</param-name>
  11. <param-value>WEB-INF/applicationContext.xml</param-value>
  12. </context-param>
  13. <!-- 声明了名为smartDispatcherServlet,它默认自动加载/WEB-INF/<servlet-name>-servlet.xmlSpring配置文件,启动Web层的Spring容器 -->
  14. <servlet>
  15. <servlet-name>smart</servlet-name>
  16. <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
  17. <load-on-startup>1</load-on-startup>
  18. </servlet>
  19. <!-- 设置DispatcherServlet匹配的URL模式 -->
  20. <servlet-mapping>
  21. <servlet-name>smart</servlet-name>
  22. <url-pattern>*.html</url-pattern>
  23. </servlet-mapping>
  24. </web-app>

多个 Spring 容器之间可设置为父子级关系,以实现良好的解耦。在这里,WebApplicationContext 将作为 ApplicationContext 的子容器,因此 Web 层容器可以引用业务层容器的 Bean,而业务层容器却访问不到 Web 层容器的 Bean。

通过编程配置 DispatcherServlet

  1. public class SmartApplicationInitializer implements WebApplicationInitializer {
  2. @Override
  3. public void onStartup(ServletContext servletContext) throws ServletException {
  4. ServletRegistration.Dynamic registration =
  5. servletContext.addServlet("dispatcher", new DispatcherServlet());
  6. registration.setLoadOnStartup(1);
  7. registration.addMapping("*.html");
  8. }
  9. }

在 Servlet 中,容器会在类路径中查找实现 javax.servlet.ServletContainerInitializer 的类,如果发现已有实现类,就会调用它来配置 Servlet 容器。Spring 的 SpringServletContainerInitializer 类实现了该接口,同时这个类又会查找实现 WebApplicationInitializer 接口的类,并将配置任务交给这些实现类去完成。因此,当应用部署到 Servlet 中时,容器启动时就会自动发现它,并使用它来配置 Servlet 上下文。

3. DispatcherServlet 内部逻辑

DispatcherServlet 类继承了 HttpServlet,初始化逻辑在 initStrategies() 方法中。

  1. public class DispatcherServlet extends FrameworkServlet {
  2. ......
  3. // 该方法会在WebApplicationContext初始化后被调用,此时Spring上下文中的Bean已经初始化完毕
  4. @Override
  5. protected void onRefresh(ApplicationContext context) {
  6. initStrategies(context);
  7. }
  8. // 通过反射机制查找并装配Spring容器中用户显式自定义的组件Bean,如果找不到,则装配默认的组件实例
  9. protected void initStrategies(ApplicationContext context) {
  10. // 初始化MultipartResolver,用于处理文件上传服务,如果有文件上传,
  11. // 那么会将当前HttpServletRequest包装成DefaultMultipartHttpServletRequest
  12. initMultipartResolver(context);
  13. initLocaleResolver(context); // 初始化本地化解析器,用于处理应用的国际化问题
  14. initThemeResolver(context); // 初始化主题解析器
  15. initHandlerMappings(context); // 初始化处理器映射器
  16. initHandlerAdapters(context); // 初始化处理器适配器
  17. initHandlerExceptionResolvers(context); // 初始化处理器异常解析器
  18. initRequestToViewNameTranslator(context); // 初始化请求到视图名翻译器
  19. initViewResolvers(context); // 初始化视图解析器
  20. initFlashMapManager(context);
  21. }
  22. }

Spring MVC 定义了一套默认的组件实现类,即使在 Spring 容器中没有显示定义组件 Bean,DispatcherServlet 也会装配好一套可用的默认组件。Spring 通过在类路径下配置了一个 DispatcherServlet.properties 文件,指定了所使用的的默认组件。如果用户希望采用非默认类型的组件,则只需在 Spring 中配置自定义的组件 Bean 即可。Spring MVC 一旦发现上下文中有用户自定义的组件,就不会使用默认的组件。

有的组件允许存在多个实例,如 HandlerMapping、HandlerAdapter 等,这些组件都实现了 Ordered 接口,可通过 order 属性确定优先级顺序,值越小优先级越高。

4. 异常处理

Spring MVC 通过 HandlerExceptionResolver 处理程序的异常,包括处理器映射、数据绑定及处理器执行时发生的异常。当发生异常时,Spring MVC 将调用 resolveException() 方法,并转到 ModelAndView 对应的视图中,作为一个异常报告页面反馈给用户。

  1. public interface HandlerExceptionResolver {
  2. @Nullable
  3. ModelAndView resolveException(HttpServletRequest request,
  4. HttpServletResponse response, @Nullable Object handler, Exception ex);
  5. }

1)DefaultHandlerExceptionResolver
Spring MVC 默认装配了 DefaultHandlerExceptionResolver,它会将 Spring MVC 的异常转换为相应的响应状态码。我们可以在 web.xml 中为响应状态码配置一个对应的页面。

2)AnnotationMethodExceptionResolver
Spring MVC 默认注册了 AnnotationMethodExceptionResolver,它允许通过 @ExceptionHandler 注解指定处理特定异常的方法。标注了该注解的异常处理方法可以对同一处理类中的其他处理方法进行异常响应处理。

注解驱动的控制器

1. @RequestMapping 映射请求

通过在类定义处标注 @Controller 注解使 POJO 成为一个能处理 HTTP 请求的控制器,每个控制器可拥有多个处理请求的方法,每个方法负责不同的请求操作。

@RequestMapping 注解负责将请求映射到对应的控制器方法中。在控制器的类定义及方法定义处都可以标注该注解 ,类定义处的注解提供初步的请求映射信息,方法定义处的注解提供进一步的细分映射信息。@RequestMapping 使用 value 值指定请求的 URL,它不但支持标准的 URL,还支持 Ant 风格(?、 和 *)的和带 {XXX} 占位符的 URL。URL 中的 {XXX} 占位符可以通过 @PathVariable("XXX") 绑定到控制器处理方法的入参中。

2. 请求处理方法签名

  1. 使用 @RequestParam 绑定请求参数值,因为 Java 类反射对象默认不记录方法入参的名称,因此需要在方法入参处使用该注解指定其对应的请求参数。

  2. 使用 @CookieValue 绑定请求中的 Cookie 值。

  3. 使用 @RequestHeader 绑定请求报文头的属性值。

  4. 使用 Servlet API 对象作为入参,如 HttpServletRequest、HttpServletResponse、HttpSession 等。在使用 Servlet API 的类作为入参时,Spring MVC 会自动将 Web 层对应的 Servlet 对象传递给处理方法的入参。

    3. HttpMessageConverter

    DispatcherServlet 默认使用 RequestMappingHandlerAdapter 作为 HandlerAdapter 的组件实现类,该实现类内部通过 HttpMessageConverter 将请求信息转换为对象,或将对象转换为响应信息。那如何使用 HttpMessageConverter 将请求信息转换并绑定到处理方法的入参中呢?Spring MVC 提供了两种途径。

  • 使用 @RequestBody 、@ResponseBody 对处理方法进行标注。
  1. @Controller
  2. @RequestMapping("/user")
  3. public class UserController {
  4. // 将请求报文体转换为字符串绑定到requestBody入参中
  5. @RequestMapping("/one")
  6. public void handOne(@RequestBody String requestBody) {}
  7. // 将返回值输出到响应流中
  8. @ResponseBody
  9. @RequestMapping("/two/{imageId}")
  10. public byte[] handTwo(@PathVariable("imageId") String imageId) { return new byte[]{}; }
  11. }
  • 使用 HttpEntity、ResponseEntity 作为处理方法的入参或返回值。
  1. @Controller
  2. @RequestMapping("/user")
  3. public class UserController {
  4. // 使用StringHttpMessageConverter将请求报文体及报文头的信息绑定到httpEntity中,在方法中可对相应信息进行访问
  5. @RequestMapping("/one")
  6. public void handOne(HttpEntity<String> httpEntity) {
  7. long content = httpEntity.getHeaders().getContentLength();
  8. }
  9. // 在方法中创建HttpEntity对象并返回,输出到响应流中
  10. @RequestMapping("/two/{imageId}")
  11. public ResponseEntity<byte[]> handTwo(@PathVariable("imageId") String imageId) {
  12. return new ResponseEntity<byte[]>(new byte[]{}, HttpStatus.OK);
  13. }
  14. }

处理方法的数据绑定

Spring 会根据请求方法签名的不同,将请求消息中的信息以一定的方式转换并绑定到请求方法的入参中。当请求消息到达真正需要调用的方法时,Spring MVC 还有进行数据转换、数据格式化及数据校验等工作。

1. 数据绑定

Spring MVC 通过反射机制对目标处理方法的签名进行分析,将请求消息绑定到处理方法的入参中。数据绑定的核心部件是 DataBinder。
image.png

  1. Spring MVC 将 ServletRequest 对象及处理方法的入参对象实例传递给 DataBinder。
  2. DataBinder 首先调用装配在 Web 容器上下文中的 ConversionService 组件进行数据类型转换、数据格式化等工作,将 ServletRequest 中的消息填充到入参对象中。
  3. 然后调用 Vaildator 组件对已经绑定了请求消息数据的入参对象进行数据合法性校验,最终生成数据绑定结果 BindingResult 对象。

2. 数据校验

应用程序在执行业务逻辑前,必须通过数据校验保证接收到的输入数据是正确合法的。

2.1 JSR-303

JSR-303 是 Java 为 Bean 数据合法性校验所提供的标准框架,它定义了一套可标注在成员变量、属性方法上的校验注解。

@NotNull 被注释的元素必须不为 null
@Min(value) 被注释的元素必须是一个数字,其值必须大于等于指定的value值
@Max(value) 被注释的元素必须是一个数字,其值必须小于等于指定的value值
@Size(max, min) 被注释的元素的大小必须在指定的范围内
@Past 被注释的元素必须是一个过去的日期
@Future 被注释的元素必须是一个将来的日期

2.2 Spring 校验框架

Spring 拥有自己独立的数据校验框架,同时支持 JSR-303 标准的校验框架。Spring 的 DataBinder 在进行数据绑定时,可同时调用校验框架完成数据校验工作。在 Spring MVC 中,可直接通过注解驱动的方式进行数据校验。Spring 的 Validator 接口是校验框架的核心接口。一般通过它的实现类 LocalValidatorFactoryBean 来注入到需要数据校验的 Bean 中。

  1. public interface Validator {
  2. // 该校验器能够对clazz类型的对象进行校验
  3. boolean supports(Class<?> clazz);
  4. // 对目标类target进行校验,并将校验错误记录在errors中
  5. void validate(Object target, Errors errors);
  6. }

2.3 Spring MVC 数据校验

会默认装配一个 LocalValidatorFactoryBean,通过在处理方法的入参上标注 @Valid 注解,即可让 Spring MVC 在完成数据绑定后执行数据校验工作。

文件上传

Spring MVC 为文件上传提供了直接支持,这种支持是通过 MultipartResolver 实现的。Spring 提供了一个实现类:CommonMultipartResolver。在 Spring MVC 上下文中默认没有装配 MultipartResolver,因此如果想使用 Spring 的文件上传功能,则需要先在上下文中进行配置。

  1. @Bean
  2. public CommonsMultipartResolver multipartResolver() {
  3. CommonsMultipartResolver resolver = new CommonsMultipartResolver();
  4. resolver.setDefaultEncoding("UTF-8");
  5. resolver.setMaxUploadSize(1024 * 1024 * 5);
  6. return resolver;
  7. }

当我们在编写方法时,可以设置一个方法参数类型为 MultipartFile 的参数,Spring MVC 会将上传文件绑定到该对象中。MultipartFile 提供了获取上传文件内容、文件名等常用方法。注意,负责上传文件的表单的编码类型必须是 multipart/form-data 类型。

WebSocket

WebSocket 最为常见的应用场景是实现服务器和基于浏览器的应用之间的通信。在浏览器中使用 JS 开启一个到达服务器的连接,服务器通过这个连接发送更新到浏览器中,相对于传统的轮询,这种方式更加高效。

Spring 提供了一个 WebSocketHandler 接口,该接口定义了 5 个 WebSocket 相关的接口方法。Spring 拥有一个该接口的抽象实现类 AbstractWebSocketHandler,继承该类便可以选择性地实现感兴趣的方法。

  1. public interface WebSocketHandler {
  2. // 在WebSocket协商成功并且WebSocket连接已打开并可以使用后调用
  3. void afterConnectionEstablished(WebSocketSession session) throws Exception;
  4. // 当新的WebSocket消息到达时调用
  5. void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception;
  6. // 处理来自基础WebSocket消息传输的错误
  7. void handleTransportError(WebSocketSession session, Throwable exception) throws Exception;
  8. // 在任一侧关闭WebSocket连接之后或发生传输错误之后调用
  9. void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception;
  10. // WebSocketHandler是否处理部分消息
  11. boolean supportsPartialMessages();
  12. }