引言

  • Spring MVC 的本质是基于 tomcat 等 web 容器客户端请求进行更为强大的逻辑处理,如校验,拦截(AOP思想),后期渲染等,使得程序员更加专注于业务的开发;
  • springMVC 的请求处理流程

tomcat 与 spring-mvc - 图1

  • SpringMVC 项目中一般会引入 applicationContext.xml 和 dispatcher-servlet.xml 两个配置文件

    • 这些配置文件的作用:

      • applicationContext.xml 是用于生成 spring 的根上下文(如果没有必要一般不定义);
      • dispatcher-servlet.xml 是某个 servlet 私有的上下文,且将根上下文作为 parent;
        • 方法:WebApplicationContext.ROOTWEBAPPLICATIONCONTEXTATTRIBUTE 方法;
      • 注意:在 applicationContext 和 dispatcher-servlet 定义的 bean 最好不要重复, dispatcher-servlet最好只是定义 controller 类型的 bean;
        1. Spring lets you define multiple contexts in a parent-child hierarchy.
        2. The applicationContext.xml defines the beans for the "root webapp context", i.e. the context associated with the webapp.
        3. The dispatch-servlet.xml (or whatever else you call it) defines the beans for one servlet's app context. There can be many of these in a webapp, one per Spring servlet (e.g. spring1-servlet.xml for servlet spring1, spring2-servlet.xml for servlet spring2).
        4. Beans in spring-servlet.xml can reference beans in applicationContext.xml, but not vice versa.
        5. All Spring MVC controllers must go in the spring-servlet.xml context.
        6. In most simple cases, the applicationContext.xml context is unnecessary. It is generally used to contain beans that are shared between all servlets in a webapp. If you only have one servlet, then there's not really much point, unless you have a specific use for it.
    • 这些配置文件在以 Tomcat 为基础的 JavaWeb 开发中是如何被调用的呢,这就涉及到两个关键的类:

      • ContextLoaderListenner
      • DispatcherServlet

        1. Spring 的 WebApplicationContext

        1.1 如何创建 Spring 上下文

        1.1.1 Tomcat 中 web 应用的上下文 ServletContext

  • web 容器会为每个部署在其中的 web 应用相应提供一个全局的上下文环境,即 ServletContext;

    • 每个 web 应用有且仅创建一个 ServletContext
    • WEB-INF 下的 web.xml 中可配置多个 servlet,一个 web 应用中的所有 servlet 共享一个 ServletContext 对象,以实现 servlet 之间的通讯;
    • 作用
      • 用于在 web 应用范围内存取共享数据,如setAttribute(String name, Object object),getAttribute();
      • 获取当前 web 应用资源,如 getContextPath();
      • 获取服务器端的文件系统资源,如 getResourceAsStream();
      • 输出日志,如log(String msg) : 向Servlet的日志文件中写日志;
      • 在具体 ServletContext 实现中,提供了添加 Servlet,Filter,Listener 到 ServletContext 里面的方法;
    • 如何获得 ServletContext 对象(三种方式)
      • 通过 ServletConfig(一对一的父子关系)

image.png

  - 用域对象获得域对象(平级关系)
session.getServletContext();
request.getServletContext();
  - 通过包装的 ServletContextEvent(多对一的衍生关系)
  • ServletContext 对象 与 ServletContextListener 对象
    • 项目启动后,会唤醒/调用注册在 servletContext 对象中的 ServletContextListener;

tomcat 与 spring-mvc - 图3

1.1.2 Spring 中的 ConetextLoaderListenner 接口

  • Spring 实现了 Tomcat 的 ServletContextListener 接口,即 ConetextLoaderListenne,一旦项目启动,会触发 ContextLoaderListener 中的 contextInitialized 方法;
    • 注意:Tomcat 在解析 web.xml 过程中反射创建 ContextLoaderListener,且通过静态代码块调用 registerLIstener 注册进 ServletContext 中;

tomcat 与 spring-mvc - 图4

  • 利用 web.xml 中 中配置的 spring 根上下文路径, 读取、解析并初始化 ioc 容器;

tomcat 与 spring-mvc - 图5

1.1.3 tomcat 与 spring 在 context 上的关系

  • 正向 tomcat ->spring
    • servletContext 初始化时调用 ContextLoaderListenner 创建 Spring IoC 容器;
  • 反向 spring ->tomcat

    • Spring 以 WebApplicationContext.ROOTWEBAPPLICATIONCONTEXTATTRIBUTE 为属性 Key,将 ApplicationContext 存储到 ServletContext 中;
      • ServletContext 为 spring IoC 容器提供宿主环境;
      • 注意:ServletContext 与 spring 根上下文 ApplicationContext 为一对一关系;
    • 关系图tomcat 与 spring-mvc - 图6

      1.1.4 Spring 启动过程(源码)

      image.png
      image.png
      image.png
  • 补充:这里的 cwac 即为由 DisptcherServlet 创建的 ApplicationContext,而 parent 则为由ContextLoaderListener 创建的 ApplicationContext;

image.png

  • 注意:如果 web.xml 中配置了实现 ConfigurableWebApplicationContext 的 contextClass 类型就用那个参数,否则使用默认的 XmlWebApplicationContext;

    1.2 Spring 容器

  • spring 容器可以理解为生产对象(Object)的地方,容器的功能不仅是创建对象,而且也负责对象的整个生命周期,即创建、装配和销毁的整个过程;

  • BeanFactory 和 ApplicationContext 是 Spring 的两大核心接口,均可作为 Spring 容器;

    - ApplicationContext 是 BeanFactory的子接口。它们都可以当做Spring的容器,[理解Spring容器、BeanFactory和ApplicationContext](https://www.jianshu.com/p/2854d8984dfc)
    

    2. SpringMVC 与 DispatcherServlet

    2.1 tomcat 中的 servlet

    2.1.1 Servlet 规范及与 ServletContext 的关系

  • Servlet 是 JavaEE 规范的一种,主要是为了扩展 Java 作为 Web 服务的功能,统一接口,由其他内部厂商(tomcat、jetty 等)内部实现 web 的功能;

  • Servlet 与 ServletContext 的关系

tomcat 与 spring-mvc - 图11

2.1.2 JspServlet 和 DefaultServlet

  • JspServlet 和 DefaultServlet 都在 Conf/web.xml 中配置,相当于每个应用默认都配置了 JSPServlet 和DefaultServlet 来处理 JSP(动态资源) 和静态资源;

tomcat 与 spring-mvc - 图12tomcat 与 spring-mvc - 图13

  • JspServlet 和 DefaultServlet 如何被使用
  • 若在应用的 web.xml 自定义一个 Servlet,当匹配路径不同时,会发生什么?

    • 背景知识:Servlet 映射规则及匹配顺序
    • 案例说明
      • 匹配路径为 */. do 时,各个 Servlet 正常工作;
      • 匹配路径为 /,自定义的 Servlet 会覆盖掉 DefaultServlet,但自定义的 Servlet 只实现基本业务逻辑,无法处理原本需要 DefaultServlet 处理的静态资源请求;
        • 原因image.png
      • 匹配路径为 /*,自定义的这个 Servlet 会使包括 JspServlet 在内的所有 Servlet 失效,使得 JSP 无法被编译成 Servlet 输出 HTML 片段,HTML/CSS/JS/PNG 等静态资源也无法获取;
    • 如何解决这种在匹配路径为 / 或 /* 下无法读取静态资源的问题呢?就有了 spring 的 DispatcherServlet;

      2.2 DispatcherServlet

      2.2.1 Spring 中的 DispatcherServlet

  • DispatcherServlet 俗称前端控制器,springmvc 处理一切前端请求的入口所在,负责协调和组织不同组件完成请求处理并返回响应工作,与 Spring IoC容器无缝集成,从而可以获得 Spring 的所有好处;

  • DispatcherServlet 默认使用 WebApplicationContext 作为上下文,Spring 默认配置文件为“/WEB-INF/[servlet名字]-servlet.xml”
  • DispatcherServlet 本质就是一个 Servlet,但与一般 Servlet 不同,基于 HttpServlet 又封装了一条逻辑;
    • DispatcherServlet 的继承关系图

tomcat 与 spring-mvc - 图15

  • DispatcherServlet 的 doService 方法中封装了最终调用处理器的方法 doDispatch
    • doService() 获取请求,设置一些 request 的参数,然后分发给 doDispatch;

      2.2.1.1 SpringMVC DispatcherServlet 图解

      tomcat 与 spring-mvc - 图16
      tomcat 与 spring-mvc - 图17
  • DispatcherServlet 的请求处理流程
    • Tomcat 启动,对 DispatcherServlet 进行实例化,然后调用它的 init() 方法进行初始化;
    • 加载 web.xml 中初始化参数,建立 WebApplicationContext (SpringMVC的IOC容器),并初始化组件;
    • 客户端发出请求,由 Tomcat 接收到这个请求,如果匹配 DispatcherServlet 在 web.xml 中配置的映射路径,Tomcat 就将请求转交给 DispatcherServlet 处理;
    • DispatcherServlet 从容器中取出所有 HandlerMapping 实例(每个实例对应一个 HandlerMapping 接口的实现类)并遍历,每个 HandlerMapping 会根据请求信息,通过自己实现类中的方式去找到处理该请求的 Handler (执行程序,如Controller中的方法),并且将这个 Handler 与一堆 HandlerInterceptor (拦截器) 封装成一个 HandlerExecutionChain 对象,一旦有一个 HandlerMapping 可以找到 Handler 则退出循环;
    • DispatcherServlet 取出 HandlerAdapter 组件,根据已经找到的 Handler,再从所有 HandlerAdapter 中找到可以处理该 Handler 的 HandlerAdapter 对象;
    • 执行 HandlerExecutionChain 中所有拦截器的 preHandler() 方法,然后再利用 HandlerAdapter 执行 Handler ,执行完成得到 ModelAndView,再依次调用拦截器的 postHandler() 方法;
    • 利用 ViewResolver 将 ModelAndView 或是 Exception(可解析成 ModelAndView)解析成 View,然后 View 会调用 render() 方法再根据 ModelAndView 中的数据渲染出页面;
    • 最后再依次调用拦截器的 afterCompletion() 方法,一次请求结束;
  • doDispatcher() 方法的源码

       /**
       *将Handler进行分发,handler会被handlerMapping有序的获得
       *通过查询servlet安装的HandlerAdapters来获得HandlerAdapters来查找第一个支持handler的类
       *所有的HTTP的方法都会被这个方法掌控。取决于HandlerAdapters 或者handlers 他们自己去决定哪些方法是可用
       *@param request current HTTP request
       *@param response current HTTP response
       */
      protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
          /* 当前HTTP请求**/
          HttpServletRequest processedRequest = request;
          HandlerExecutionChain mappedHandler = null;
          boolean multipartRequestParsed = false;
    
          WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
    
          try {
              ModelAndView mv = null;
              Exception dispatchException = null;
    
              try {
                  //判断是否有文件上传
                  processedRequest = checkMultipart(request);
                  multipartRequestParsed = (processedRequest != request);
    
                  // getHandler中遍历HandlerMapping,获取对应的HandlerExecutionChain
                  // HandlerExecutionChain,包含HandlerIntercrptor和HandlerMethod
                  mappedHandler = getHandler(processedRequest);
                  if (mappedHandler == null || mappedHandler.getHandler() == null) {
                      noHandlerFound(processedRequest, response);
                      return;
                  }
    
                  //获得HandlerAdapter
                  HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
    
                  //获得HTTP请求方法
                  String method = request.getMethod();
                  boolean isGet = "GET".equals(method);
                  if (isGet || "HEAD".equals(method)) {
                      long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
                      if (logger.isDebugEnabled()) {
                          logger.debug("Last-Modified value for [" + getRequestUri(request) + "] is: " + lastModified);
                      }
                      if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
                          return;
                      }
                  }
                  //如果有拦截器的话,会执行拦截器的preHandler方法
                  if (!mappedHandler.applyPreHandle(processedRequest, response)) {
                      return;
                  }
    
                  //返回ModelAndView
                  mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
    
                  if (asyncManager.isConcurrentHandlingStarted()) {
                      return;
                  }
                  //当view为空时,,根据request设置默认view
                  applyDefaultViewName(processedRequest, mv);
                  //执行拦截器的postHandle
                  mappedHandler.applyPostHandle(processedRequest, response, mv);
              }
              catch (Exception ex) {
                  dispatchException = ex;
              }
              processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
          }
          catch (Exception ex) {
              triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
          }
          catch (Error err) {
              triggerAfterCompletionWithError(processedRequest, response, mappedHandler, err);
          }
          finally {
              //判断是否是异步请求
              if (asyncManager.isConcurrentHandlingStarted()) {
                  // Instead of postHandle and afterCompletion
                  if (mappedHandler != null) {
                      mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
                  }
              }
              else {
                  // Clean up any resources used by a multipart request.
                  //删除上传资源
                  if (multipartRequestParsed) {
                      cleanupMultipart(processedRequest);
                  }
              }
          }
      }
    

    2.2.1.2 几个重要的类

    image.png
    HandlerExecutionChain 源码分析
    拦截器 Interceptor

  • 作用:拦截用户的请求并进行相应的处理,也可进行权限验证,或判断用户是否登陆,日志记录等;

  • 三个重要环节
    • 请求处理之前 preHandle (HttpServletRequest request, HttpServletResponse response, Object handle)
      • 作用:设置一些前置初始化操作、预处理请求或判断请求是否继续;
    • 请求处理之后 postHandle (HttpServletRequest request, HttpServletResponse response, Object handle, ModelAndView modelAndView)
      • 调用时机:在 Controller 方法调用之后及 DispatcherServlet 进行视图返回渲染之前被调用;
      • 调用顺序:先声明的 Interceptor 的 postHandle 方法会后执行(与 preHandler 正好相反);
      • 作用:对 Controller 处理之后的 ModelAndView 对象进行操作;
    • 请求结束之后 afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handle, Exception ex)
      • 调用时机:DispatcherServlet 渲染了对应的视图之后执行;
      • 作用:进行资源清理或统计请求过程中的有关信息;
    • 实现:xml 配置或注解

image.png

2.2.2 DispatcherServlet 如何处理静态资源

  • 当 DispatcherServlet 的请求映射配置为”/“时,Spring 框架会捕获所有 URL 的请求,所以必须能够处理静态资源请求;
  • Spring 提供的两种解决方式

    • springMVC-servlet.xml 中配置
      • 此操作会在 Spring MVC 上下文中定义一个org.springframework.web.servlet.resource.DefaultServletHttpRequestHandler,像一个检查员对进入 DispatcherServlet 的 URL 进行筛查,如果发现是静态资源的请求,就将该请求转由 Web 应用服务器默认的 Servlet 处理;
      • 传统 Web 容器的静态资源只能放在 Web 容器的根路径下;
      • 使用
    • springMVC-servlet.xml 中配置
      • 由 Spring MVC 框架自己处理静态资源,并附加一些有用的功能;
      • 允许静态资源放在任何地方,如WEB-INF目录下、类路径下等,甚至可以将JavaScript 等静态文件打到 JAR 包中;
      • 通过 location 属性指定静态资源的位置,由于 location 属性是 Resources 类型,因此可以使用诸如”classpath:”等的资源前缀指定资源位置;
      • 依据当前著名的 Page Speed、YSlow 等浏览器优化原则对静态资源提供优化,如可以通过 cacheSeconds 属性指定静态资源在浏览器端的缓存时间,一般可将该时间设置为一年,以充分利用浏览器端的缓存等;
      • 使用
    • 小结:Spring 的这两种解决方法,一个是转发给能够处理静态资源的 DefaultServlet 处理,另一个是自己实现代码模仿 DefaultServlet 的处理逻辑;

      2.2.3 其他

      2.2.3.1 ContextLoaderListener 所创建的根上下文 与 DispatcherServlet 子上下文的联系

  • 联系

    • 两者都通过 ServletContext 的 setAttribute 方法放到 ServletContext 中;
    • ContextLoaderListener 先创建的 ApplicationContext 是 DispatcherServlet 的父上下文;
  • 区别

    • ContextLoaderListener 创建的 ApplicationContext 主要用于整个 Web 应用程序需要共享的一些组件,比如 DAO,数据库的 ConnectionFactory 等,而由 DispatcherServlet 创建的 ApplicationContext 主要用于和该 Servlet 相关的一些组件,比如 Controller、ViewResovler 等;
    • 在 DispatcherServlet 中可以引用由 ContextLoaderListener 所创建的 ApplicationContext,反之不行;

      2.2.3.2 DispatcherServlet 中的 Interceptor 和 Filter 的区别

  • 具体详见 过滤器(Filter)与拦截器(Interceptor )区别

  • Spring 的 Interceptor(拦截器) 与 Servlet 的 Filter 的相似之处在于两者都是 AOP 编程思想的体现,都能实现权限检查、日志记录等,区别在于: | Filter | Interceptor | Summary | | —- | —- | —- | | Filter 接口定义在 javax.servlet 包中 | 接口 HandlerInterceptor 定义在org.springframework.web.servlet 包中 | | | Filter 定义在 web.xml 中 | | | | Filter在只在 Servlet 前后起作用。Filters 通常将 请求和响应(request/response) 当做黑盒子,Filter 通常不考虑servlet 的实现。 | 拦截器能够深入到方法前后、异常抛出前后等,因此拦截器的使用具有更大的弹性。允许用户介入(hook into)请求的生命周期,在请求过程中获取信息,Interceptor 通常和请求更加耦合。 | 在Spring构架的程序中,要优先使用拦截器。几乎所有 Filter 能够做的事情, interceptor 都能够轻松的实现
    | | Filter 是 Servlet 规范规定的。 | 而拦截器既可以用于Web程序,也可以用于Application、Swing程序中。 | 使用范围不同 | | Filter 是在 Servlet 规范中定义的,是 Servlet 容器支持的。 | 而拦截器是在 Spring容器内的,是Spring框架支持的。 | 规范不同 | | Filter 不能够使用 Spring 容器资源 | 拦截器是一个Spring的组件,归Spring管理,配置在Spring文件中,因此能使用Spring里的任何资源、对象,例如 Service对象、数据源、事务管理等,通过IoC注入到拦截器即可 | Spring 中使用 interceptor 更容易 | | Filter 是被 Server(like Tomcat) 调用 | Interceptor 是被 Spring 调用 | Filter 总是优先于 Interceptor 执行 |

3. 编写一个简单的 SpringMVC 项目(toDo)

总结与思考

  • 问题
    • 为什么在 HandlerExecutionChain 类中有两个参数:HandlerInterceptor[] interceptors 和

List interceptorList 链表

Spring和SpringMVC配置中父子WebApplicationContext的关系
源码理解 spring 中的各种 context
ContextLoaderListener 详解
servlet/tomcat等容器/springMVC之间的关系
【Spring】浅谈ContextLoaderListener及其上下文与DispatcherServlet的区别
【Java】spring 工作原理之一——DispatcherServlet
Spring MVC静态资源访问