本文结合源码,讲解了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:
- interceptorList按正序执行preHandle;
- 当某个interceptor.preHandle返回false时,将直接触发执行afterCompletion;跳过剩余的interceptor及postHandle;
- 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接口,大体逻辑如下:
- 遍历Spring容器,查找带有@RequestMapping的bean即handler;
- 查找这样的bean中带有@RequestMapping的方法,将URI与method作为映射,注册到mappingRegistry中;
- 注册时,先将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中,完成了以下事情:
- 创建并注册dispatcherServlet,设置loadOnStartup、mapping
- 注册所有Filter
在父类AbstractContextLoaderInitializer中,完成了ContextLoaderListener的注册。
Tomcat在启动时会通过SPI注册 ContextLoaderListener和DispatcherServlet对象:
- 创建在ContextLoaderListener初始化时创建父容器,并执行refresh
- 在DispatcherServlet初始化时创建子容器。
2.2.2 创建父容器
外置Web容器(如Tomcat)启动时,会调用ContextLoaderListener#contextInitialized,创建了父容器。
2.2.3 创建子容器
我们已向Web容器如Tomcat注册了dispatcherServlet,Tomcat将会调用HttpServletBean#init,进而调用FrameworkServlet#initServletBean
2.3 为什么使用父子容器
有以下原因:
- 父子容器的主要作用,是早期Spring为了划分框架边界。service、dao层由spring框架来管理,而controller 层交给servlet-api的实现方管理;
- 规范整体架构,spring容器中service无法访问MVC容器中的controller,而MVC容器中controller可以访问到父容器中的service、dao;
- 方便切换web层框架。如果想把项目中web层框架从spring mvc替换成Struts,那么只需要将springmvc的配置,替换成Struts配置即可。而Spring容器配置无需任何改变。
总之,就是为了划分框架边界和系统解耦。其实,不用子父容器也可以实现所需功能(SpringBoot中就没用子父容器) 。