SpringMVC核心组件和父子容器

0 阅读5分钟

本文结合源码,讲解了springMVC中的核心组件,并基于Servlet的SPI机制,说明了父子容器的创建过程。

本文中源码来自Spring 5.3.x分支,github源码地址:github.com/spring-proj…

一 核心组件

1.1 DispatcherServlet类

在javax.servlet.Servlet接口中,service方法声明了如何响应某个请求。

servlet-api中,提供了GenericServlet、HttpServlet抽象类。

spring-webmvc中,进一步提供了HttpServletBean、FrameworkServlet抽象实现,最终实现是DispatcherServlet。

无论哪种HttpMethod请求,都会执行FrameworkServlet#processRequest,进而执行DispatcherServlet的doService、doDispatch方法。

1.2 HandlerExecutionChain类

执行器链,包含Object类型的handler和HandlerInterceptor集合,该对象由HandlerMapping的getHandler方法创建。

// 执行器
private final Object handler;
// 拦截器链
private final List<HandlerInterceptor> interceptorList = new ArrayList<>();

private int interceptorIndex = -1;

提供了执行拦截器链的方法,如applyPreHandle:

  1. interceptorList按正序执行preHandle;
  2. 当某个interceptor.preHandle返回false时,将直接触发执行afterCompletion;跳过剩余的interceptor及postHandle;
  3. applyPreHandle返回false时,doDispatch方法将立即返回。

而applyPostHandle方法中,interceptorList按反序执行postHandle;afterCompletion也是反序执行。

1.3 HandlerAdapter

在HandlerExecutionChain中,handler是Object类型。具体有哪些形式呢?我们来看HandlerAdapter接口。

来看源码中类注释:

MVC framework SPI, allowing parameterization of the core MVC workflow. Interface that must be implemented for each handler type to handle a request. This interface is used to allow the DispatcherServlet to be indefinitely extensible.

MVC框架SPI,允许参数化核心工作流。 每个Handler类型必须实现该接口以处理请求。该接口允许DispatcherServlet无限扩展

有两个方法:

  • supports方法:判断是否支持某个handler对象;
  • handle方法:处理请求,返回ModelAndView对象;

RequestMappingHandlerAdapter

抽象父类是AbstractHandlerMethodAdapter,实现了handleInternal、supportsInternal抽象方法。

  • 支持@RequestMapping注解的方法;
  • 处理请求时,会将HandlerMethod封装为InvocableHandlerMethod对象,通过Method对象反射调用;

这种类型是开发中最常用的。

SimpleControllerHandlerAdapter

支持Controller接口的实现类;执行时直接调用Controller.handleRequest方法。

SimpleServletHandlerAdapter

支持Servlet接口的实现类;执行时直接调用Servlet.service方法。

1.4 RequestMappingHandlerMapping

该类完成了对@RequestMapping的解析,将URI到方法的映射注册到Map中——创建注册表。

它的父类AbstractHandlerMethodMapping实现了InitializingBean接口,大体逻辑如下:

  1. 遍历Spring容器,查找带有@RequestMapping的bean即handler;
  2. 查找这样的bean中带有@RequestMapping的方法,将URI与method作为映射,注册到mappingRegistry中;
  3. 注册时,先将method封装为HandlerMethod,在添加到注册表registry中。

二 父子容器

2.1 Spring整合springMVC

先来看看Spring如何整合springMVC。

当使用XML方式配置时,需要在项目的web.xml文件中配置如下内容:分别指定spring配置文件、springMVC配置文件。

<!-- 配置spring -->
<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>classpath:spring-config.xml</param-value>
</context-param>
<listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

<!-- 配置springmvc -->
<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-config.xml</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
</servlet>

<servlet-mapping>
    <servlet-name>mvc-dispatcher</servlet-name>
    <!-- 拦截路径,“/”表示匹配路径型url;“/*”会匹配所有url -->
    <url-pattern>/*</url-pattern> 
</servlet-mapping>

然后在mvc-config.xml中配置如下内容:扫描Controller、springMVC组件等。

<!-- 配置只扫描Controller -->
<context:component-scan base-package="com.example"/>
    <context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>

<!--配置Filter-->
<!--配置Listener-->
<!--配置其他组件-->

<!--静态资源过滤-->
<mvc:default-servlet-handler/>
<!--开启注解-->
<mvc:annotation-driven/>

在spring-config.xml中配置如下内容:扫描除Controller之外的组件、数据源、事务等。

<!--配置扫描除Controller之外的组件 -->
<context:component-scan base-package="com.example">
    <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>

<!-- 配置数据库连接池 -->

<!-- 配置事务 -->

很显然,mvc-config和spring-config分别配置了一次component-scan,将向项目中创建两个Ioc容器:

  • springMVC的Ioc容器会包含controller,但是不会有service、dao层的bean;
  • 而spring Ioc容器会包含service、dao层等的bean,但是不会有controller对象。

此外,spring容器是springMVC容器的parent,因此我们可以在controller中注入service,但不能在service中注入controller。

2.2 Servlet的SPI机制

在web容器启动时,为给第三方组件提供机会做一些初始化的工作,例如注册servlet、filter等,Servlet API定义了ServletContainerInitializer接口

Servlet厂商可以提供一个ServletContainerInitializer接口实现,在jar包的META-INF/services /javax.servlet.ServletContainerInitializer文件中,写明ServletContainerInitializer实现类的全类名。

当web容器如Tomcat启动时,会扫描当前应用每一个jar包中META-INF/services/javax.servlet.ServletContainerInitializer指定的实现类,执行其onStartup方法。

springMVC本质就是对Servlet API的一种实现。在spring-web模块中声明了SpringServletContainerInitializer实现类。

外置Tomcat启动时,通过SPI机制找到spring-web包/META/INF/service/javax.servlet.ServletContainerInitializer文件,创建SpringServletContainerInitializer对象,并执行onStartUp方法。

2.2.1 SpringServletContainerInitializer

该类是spring对ServletContainerInitializer接口的实现,通过@HandlesTypes指明需要处理的类型。

根据入参Set,反射创建WebApplicationInitializer实例,然后触发它们的onStartup方法。

springMVC中提供了一个子类——AbstractAnnotationConfigDispatcherServletInitializer。

  • createRootApplicationContext方法,创建父容器
  • createServletApplicationContext方法,创建子容器

在父类AbstractDispatcherServletInitializer中,完成了以下事情:

  1. 创建并注册dispatcherServlet,设置loadOnStartup、mapping
  2. 注册所有Filter

在父类AbstractContextLoaderInitializer中,完成了ContextLoaderListener的注册。

Tomcat在启动时会通过SPI注册 ContextLoaderListener和DispatcherServlet对象:

  1. 创建在ContextLoaderListener初始化时创建父容器,并执行refresh
  2. 在DispatcherServlet初始化时创建子容器。

2.2.2 创建父容器

外置Web容器(如Tomcat)启动时,会调用ContextLoaderListener#contextInitialized,创建了父容器。

2.2.3 创建子容器

我们已向Web容器如Tomcat注册了dispatcherServlet,Tomcat将会调用HttpServletBean#init,进而调用FrameworkServlet#initServletBean

2.3 为什么使用父子容器

有以下原因:

  1. 父子容器的主要作用,是早期Spring为了划分框架边界。service、dao层由spring框架来管理,而controller 层交给servlet-api的实现方管理;
  2. 规范整体架构,spring容器中service无法访问MVC容器中的controller,而MVC容器中controller可以访问到父容器中的service、dao;
  3. 方便切换web层框架。如果想把项目中web层框架从spring mvc替换成Struts,那么只需要将spring­mvc的配置,替换成Struts配置即可。而Spring容器­配置无需任何改变。

总之,就是为了划分框架边界和系统解耦。其实,不用子父容器也可以实现所需功能(SpringBoot中就没用子父容器) 。