该系列文档是本人在学习 Spring MVC 的源码过程中总结下来的,可能对读者不太友好,请结合我的源码注释 Spring MVC 源码分析 GitHub 地址 进行阅读
Spring 版本:5.2.4.RELEASE
随着 Spring Boot
和 Spring Cloud
在许多中大型企业中被普及,可能你已经忘记当年经典的 Servlet + Spring MVC 的组合,是否还记得那个 web.xml
配置文件。在开始本文之前,请先抛开 Spring Boot
到一旁,回到从前,一起来看看 Servlet 是怎么和 Spring MVC 集成,怎么来初始化 Spring 容器的,在开始阅读本文之前,最好有一定的 Servlet 和 Spring IOC 容器方面的知识,比较容易理解
概述
在开始看具体的源码实现之前,我们先一起来看看现在“陌生”的 web.xml
文件,可以查看我的另一篇 MyBatis 使用手册 文档中集成 Spring小节涉及到的 web.xml
的文件,部分内容如下:
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/javaee"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0">
<display-name>Archetype Created Web Application</display-name>
<!-- 【1】 Spring 配置 -->
<!-- 在容器(Tomcat、Jetty)启动时会被 ContextLoaderListener 监听到,
从而调用其 contextInitialized() 方法,初始化 Root WebApplicationContext 容器 -->
<!-- 声明 Spring Web 容器监听器 -->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!-- Spring 和 MyBatis 的配置文件 -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring-mybatis.xml</param-value>
</context-param>
<!-- 【2】 Spring MVC 配置 -->
<!-- 1.SpringMVC 配置 前置控制器(SpringMVC 的入口)
DispatcherServlet 是一个 Servlet,所以可以配置多个 DispatcherServlet -->
<servlet>
<!-- 在 DispatcherServlet 的初始化过程中,框架会在 web 应用 的 WEB-INF 文件夹下,
寻找名为 [servlet-name]-servlet.xml 的配置文件,生成文件中定义的 Bean. -->
<servlet-name>SpringMVC</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<!-- 配置需要加载的配置文件 -->
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring-mvc.xml</param-value>
</init-param>
<!-- 程序运行时从 web.xml 开始,加载顺序为:context-param -> Listener -> Filter -> Structs -> Servlet
设置 web.xml 文件启动时加载的顺序(1 代表容器启动时首先初始化该 Servlet,让这个 Servlet 随 Servlet 容器一起启动)
load-on-startup 是指这个 Servlet 是在当前 web 应用被加载的时候就被创建,而不是第一次被请求的时候被创建 -->
<load-on-startup>1</load-on-startup>
<async-supported>true</async-supported>
</servlet>
<servlet-mapping>
<!-- 这个 Servlet 的名字是 SpringMVC,可以有多个 DispatcherServlet,是通过名字来区分的
每一个 DispatcherServlet 有自己的 WebApplicationContext 上下文对象,同时保存在 ServletContext 中和 Request 对象中
ApplicationContext(Spring 容器)是 Spring 的核心
Context 我们通常解释为上下文环境,Spring 把 Bean 放在这个容器中,在需要的时候,可以 getBean 方法取出-->
<servlet-name>SpringMVC</servlet-name>
<!-- Servlet 拦截匹配规则,可选配置:*.do、*.action、*.html、/、/xxx/* ,不允许:/* -->
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
【1】
处,配置了 org.springframework.web.context.ContextLoaderListener
对象,它实现了 Servlet 的 javax.servlet.ServletContextListener
接口,能够监听 ServletContext 对象的生命周期,也就是监听 Web 应用的生命周期,当 Servlet 容器启动或者销毁时,会触发相应的 ServletContextEvent 事件,ContextLoaderListener 监听到启动事件,则会初始化一个Root Spring WebApplicationContext 容器,监听到销毁事件,则会销毁该容器
【2】
处,配置了 org.springframework.web.servlet.DispatcherServlet
对象,它继承了 javax.servlet.http.HttpServlet
抽象类,也就是一个 Servlet。Spring MVC 的核心类,处理请求,会初始化一个属于它的 Spring WebApplicationContext 容器,并且这个容器是以 【1】
处的 Root 容器作为父容器
- 为什么有了
【2】
创建了容器,还需要【1】
创建 Root 容器呢?因为可以配置多个【2】
呀,当然,实际场景下,不太会配置多个【2】
?? - 再总结一次,
【1】
和【2】
分别会创建其对应的 Spring WebApplicationContext 容器,并且它们是父子容器的关系
Root WebApplicationContext 容器
在概述的 web.xml
中,我们已经看到,Root WebApplicationContext 容器的初始化,通过 ContextLoaderListener
来实现。在 Servlet 容器启动时,例如 Tomcat、Jetty 启动后,则会被 ContextLoaderListener
监听到,从而调用 contextInitialized(ServletContextEvent event)
方法,初始化 Root WebApplicationContext 容器
而 ContextLoaderListener 的类图如下:
ContextLoaderListener
org.springframework.web.context.ContextLoaderListener
类,实现 javax.servlet.ServletContextListener
接口,继承 ContextLoader 类,实现 Servlet 容器启动和关闭时,分别初始化和销毁 WebApplicationContext 容器,代码如下:
public class ContextLoaderListener extends ContextLoader implements ServletContextListener {
public ContextLoaderListener() {
}
/**
* As of Spring 3.1, supports injecting the root web application context
*/
public ContextLoaderListener(WebApplicationContext context) {
super(context);
}
/**
* Initialize the root web application context.
*/
@Override
public void contextInitialized(ServletContextEvent event) {
// <1> 初始化 Root WebApplicationContext
initWebApplicationContext(event.getServletContext());
}
/**
* Close the root web application context.
*/
@Override
public void contextDestroyed(ServletContextEvent event) {
// <2> 销毁 Root WebApplicationContext
closeWebApplicationContext(event.getServletContext());
ContextCleanupListener.cleanupAttributes(event.getServletContext());
}
}
- 监听到 Servlet 容器启动事件,则调用父类 ContextLoader 的
initWebApplicationContext(ServletContext servletContext)
方法,初始化 WebApplicationContext 容器 - 监听到 Servlet 销毁启动事件,则调用父类 ContextLoader 的
closeWebApplicationContext(ServletContext servletContext)
方法,销毁 WebApplicationContext 容器
ContextLoader
org.springframework.web.context.ContextLoader
类,真正实现初始化和销毁 WebApplicationContext 容器的逻辑的类
静态代码块
public class ContextLoader {
/**
* Name of the class path resource (relative to the ContextLoader class)
* that defines ContextLoader‘s default strategy names.
*/
private static final String DEFAULT_STRATEGIES_PATH = "ContextLoader.properties";
/**
* 默认的配置 Properties 对象
*/
private static final Properties defaultStrategies;
static {
// Load default strategy implementations from properties file.
// This is currently strictly internal and not meant to be customized by application developers.
try {
ClassPathResource resource = new ClassPathResource(DEFAULT_STRATEGIES_PATH, ContextLoader.class);
defaultStrategies = PropertiesLoaderUtils.loadProperties(resource);
}
catch (IOException ex) {
throw new IllegalStateException("Could not load ‘ContextLoader.properties‘: " + ex.getMessage());
}
}
}
从 ContextLoader.properties
中,读取默认的配置 Properties 对象。实际上,正如 Load default strategy implementations from properties file. This is currently strictly internal and not meant to be customized by application developers.
所注释,这是一个应用开发者无需关心的配置,而是 Spring 框架自身所定义的
打开来该文件瞅瞅,代码如下:
# Default WebApplicationContext implementation class for ContextLoader.
# Used as fallback when no explicit context implementation has been specified as context-param.
# Not meant to be customized by application developers.
org.springframework.web.context.WebApplicationContext=org.springframework.web.context.support.XmlWebApplicationContext
这意味着什么呢?如果我们没有在 <context-param />
标签中指定 WebApplicationContext,则默认使用 XmlWebApplicationContext 类,我们在使用 Spring 的过程中一般情况下不会主动指定
构造方法
public class ContextLoader {
/**
* Name of servlet context parameter (i.e., {@value}) that can specify the
* config location for the root context, falling back to the implementation‘s default otherwise.
* @see org.springframework.web.context.support.XmlWebApplicationContext#DEFAULT_CONFIG_LOCATION
*/
public static final String CONFIG_LOCATION_PARAM = "contextConfigLocation";
/** Map from (thread context) ClassLoader to corresponding ‘current‘ WebApplicationContext. */
private static final Map<ClassLoader, WebApplicationContext> currentContextPerThread = new ConcurrentHashMap<>(1);
/** The ‘current‘ WebApplicationContext, if the ContextLoader class is deployed in the web app ClassLoader itself. */
@Nullable
private static volatile WebApplicationContext currentContext;
/** The root WebApplicationContext instance that this loader manages. */
@Nullable
private WebApplicationContext context;
/**
* Create a new {@code ContextLoader} that will create a web application context
* based on the "contextClass" and "contextConfigLocation" servlet context-params.
* See class-level documentation for details on default values for each.
*/
public ContextLoader() {
}
/**
* Create a new {@code ContextLoader} with the given application context.
* This constructor is useful in Servlet 3.0+ environments where instance-based
* registration of listeners is possible through the {@link ServletContext#addListener} API.
*/
public ContextLoader(WebApplicationContext context) {
this.context = context;
}
// ... 省略其他相关配置属性
}
- 在概述的
web.xml
文件中可以看到定义的contextConfigLocation
参数为 spring-mybatis.xml 配置文件路径 -
currentContextPerThread
:用于保存当前 ClassLoader 类加载器与 WebApplicationContext 对象的映射关系 -
currentContext
:如果当前线程的类加载器就是 ContextLoader 类所在的类加载器,则该属性用于保存 WebApplicationContext 对象 -
context
:WebApplicationContext 实例对象
关于类加载器涉及到 JVM 的“双亲委派机制”,在《精尽MyBatis源码分析 - 基础支持层》 有简单的讲述到,可以参考一下
initWebApplicationContext
initWebApplicationContext(ServletContext servletContext)
方法,初始化 WebApplicationContext 对象,代码如下:
public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
// <1> 若已经存在 ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE 对应的 WebApplicationContext 对象,则抛出 IllegalStateException 异常。
// 例如,在 web.xml 中存在多个 ContextLoader
if (servletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE) != null) {
throw new IllegalStateException(
"Cannot initialize context because there is already a root application context present - " +
"check whether you have multiple ContextLoader* definitions in your web.xml!");
}
// <2> 打印日志
servletContext.log("Initializing Spring root WebApplicationContext");
Log logger = LogFactory.getLog(ContextLoader.class);
if (logger.isInfoEnabled()) {
logger.info("Root WebApplicationContext: initialization started");
}
// 记录开始时间
long startTime = System.currentTimeMillis();
try {
// Store context in local instance variable, to guarantee that
// it is available on ServletContext shutdown.
if (this.context == null) {
// <3> 初始化 context ,即创建 context 对象
this.context = createWebApplicationContext(servletContext);
}
// <4> 如果是 ConfigurableWebApplicationContext 的子类,如果未刷新,则进行配置和刷新
if (this.context instanceof ConfigurableWebApplicationContext) {
ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) this.context;
if (!cwac.isActive()) { // <4.1> 未刷新( 激活 )
// The context has not yet been refreshed -> provide services such as
// setting the parent context, setting the application context id, etc
if (cwac.getParent() == null) { // <4.2> 无父容器,则进行加载和设置。
// The context instance was injected without an explicit parent ->
// determine parent for root web application context, if any.
ApplicationContext parent = loadParentContext(servletContext);
cwac.setParent(parent);
}
// <4.3> 配置 context 对象,并进行刷新
configureAndRefreshWebApplicationContext(cwac, servletContext);
}
}
// <5> 记录在 servletContext 中
servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);
// <6> 记录到 currentContext 或 currentContextPerThread 中
ClassLoader ccl = Thread.currentThread().getContextClassLoader();
if (ccl == ContextLoader.class.getClassLoader()) {
currentContext = this.context;
}
else if (ccl != null) {
currentContextPerThread.put(ccl, this.context);
}
if (logger.isInfoEnabled()) {
long elapsedTime = System.currentTimeMillis() - startTime;
logger.info("Root WebApplicationContext initialized in " + elapsedTime + " ms");
}
// <7> 返回 context
return this.context;
}
catch (RuntimeException | Error ex) {
logger.error("Context initialization failed", ex);
servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, ex);
throw ex;
}
}
-
若 ServletContext(Servlet 的上下文)已存在 Root WebApplicationContext 对象,则抛出异常,因为不能再初始化该对象
-
打印日志,在启动 SSM 项目的时候,是不是都会看到这个日志“Initializing Spring root WebApplicationContext”
-
如果
context
为空,则调用createWebApplicationContext(ServletContext sc)
方法,初始化一个 Root WebApplicationContext 对象,方法如下:protected WebApplicationContext createWebApplicationContext(ServletContext sc) { // <1> 获得 context 的类(默认情况是从 ContextLoader.properties 配置文件读取的,为 XmlWebApplicationContext) Class<?> contextClass = determineContextClass(sc); // <2> 判断 context 的类,是否符合 ConfigurableWebApplicationContext 的类型 if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) { throw new ApplicationContextException("Custom context class [" + contextClass.getName() + "] is not of type [" + ConfigurableWebApplicationContext.class.getName() + "]"); } // <3> 创建 context 的类的对象 return (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass); }
-
如果是 ConfigurableWebApplicationContext 的子类,并且未刷新,则进行配置和刷新
- 如果未刷新(激活),默认情况下,是符合这个条件的,所以会往下执行
- 如果无父容器,则进行加载和设置。默认情况下,
loadParentContext(ServletContext servletContext)
方法返回一个空对象,也就是没有父容器了 - 调用
configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac, ServletContext sc)
方法,配置context
对象,并进行刷新
-
将
context
对象保存在 ServletContext 中 -
将
context
对象设置到currentContext
或者currentContextPerThread
对象中,差异就是类加载器是否相同,具体用途目前不清楚?? -
返回已经初始化的
context
对象
configureAndRefreshWebApplicationContext
configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac, ServletContext sc)
方法,配置 ConfigurableWebApplicationContext 对象,并进行刷新,方法如下:
protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac, ServletContext sc) {
// <1> 如果 wac 使用了默认编号,则重新设置 id 属性
if (ObjectUtils.identityToString(wac).equals(wac.getId())) {
// The application context id is still set to its original default value
// -> assign a more useful id based on available information
// 情况一,使用 contextId 属性
String idParam = sc.getInitParameter(CONTEXT_ID_PARAM);
if (idParam != null) {
wac.setId(idParam);
}
else { // 情况二,自动生成
// Generate default id...
wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX +
ObjectUtils.getDisplayString(sc.getContextPath()));
}
}
// <2>设置 context 的 ServletContext 属性
wac.setServletContext(sc);
// <3> 设置 context 的配置文件地址
String configLocationParam = sc.getInitParameter(CONFIG_LOCATION_PARAM);
if (configLocationParam != null) {
wac.setConfigLocation(configLocationParam);
}
// The wac environment‘s #initPropertySources will be called in any case when the context
// is refreshed; do it eagerly here to ensure servlet property sources are in place for
// use in any post-processing or initialization that occurs below prior to #refresh
ConfigurableEnvironment env = wac.getEnvironment();
if (env instanceof ConfigurableWebEnvironment) {
((ConfigurableWebEnvironment) env).initPropertySources(sc, null);
}
// <4> 对 context 进行定制化处理
customizeContext(sc, wac);
// <5> 刷新 context ,执行初始化
wac.refresh();
}
-
如果
wac
使用了默认编号,则重新设置id
属性。默认情况下,我们不会对wac
设置编号,所以会执行进去。而实际上,id
的生成规则,也分成使用contextId
在<context-param />
标签中由用户配置,和自动生成两种情况。?? 默认情况下,会走第二种情况 -
设置
wac
的 ServletContext 属性 -
【关键】设置
context
的配置文件地址。例如我们在概述中的web.xml
中所看到的<!-- Spring 和 MyBatis 的配置文件 --> <context-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:spring-mybatis.xml</param-value> </context-param>
-
对
wac
进行定制化处理,暂时忽略 -
【关键】触发
wac
的刷新事件,执行初始化。此处,就会进行一些的 Spring 容器的初始化工作,涉及到 Spring IOC 相关内容
closeWebApplicationContext
closeWebApplicationContext(ServletContext servletContext)
方法,关闭 WebApplicationContext 容器对象,方法如下:
public void closeWebApplicationContext(ServletContext servletContext) {
servletContext.log("Closing Spring root WebApplicationContext");
try {
// 关闭 context
if (this.context instanceof ConfigurableWebApplicationContext) {
((ConfigurableWebApplicationContext) this.context).close();
}
}
finally {
// 移除 currentContext 或 currentContextPerThread
ClassLoader ccl = Thread.currentThread().getContextClassLoader();
if (ccl == ContextLoader.class.getClassLoader()) {
currentContext = null;
}
else if (ccl != null) {
currentContextPerThread.remove(ccl);
}
// 从 ServletContext 中移除
servletContext.removeAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE);
}
}
在 Servlet 容器销毁时被调用,用于关闭 WebApplicationContext 对象,以及清理相关资源对象
Servlet WebApplicationContext 容器
在概述的 web.xml
中,我们已经看到,除了会初始化一个 Root WebApplicationContext 容器外,还会往 Servlet 容器的 ServletContext 上下文中注入一个 DispatcherServlet
对象,初始化该对象的过程也会初始化一个 Servlet WebApplicationContext 容器
DispatcherServlet 的类图如下:
可以看到 DispatcherServlet
是一个 Servlet 对象,在注入至 Servlet 容器会调用其 init
方法,完成一些初始化工作
-
HttpServletBean ,负责将 ServletConfig 设置到当前 Servlet 对象中,它的 Java doc:
/** * Simple extension of {@link javax.servlet.http.HttpServlet} which treats * its config parameters ({@code init-param} entries within the * {@code servlet} tag in {@code web.xml}) as bean properties. */
-
FrameworkServlet ,负责初始化 Spring Servlet WebApplicationContext 容器,同时该类覆写了 doGet、doPost 等方法,并将所有类型的请求委托给 doService 方法去处理,doService 是一个抽象方法,需要子类实现,它的 Java doc:
/** * Base servlet for Spring‘s web framework. Provides integration with * a Spring application context, in a JavaBean-based overall solution. */
-
DispatcherServlet ,负责初始化 Spring MVC 的各个组件,以及处理客户端的请求,协调各个组件工作,它的 Java doc:
/** * Central dispatcher for HTTP request handlers/controllers, e.g. for web UI controllers * or HTTP-based remote service exporters. Dispatches to registered handlers for processing * a web request, providing convenient mapping and exception handling facilities. */
每一层的 Servlet 实现类,负责执行相应的逻辑,条理清晰,我们逐个来看
HttpServletBean
org.springframework.web.servlet.HttpServletBean
抽象类,实现 EnvironmentCapable、EnvironmentAware 接口,继承 HttpServlet 抽象类,负责将 ServletConfig 集成到 Spring 中
构造方法
public abstract class HttpServletBean extends HttpServlet implements EnvironmentCapable, EnvironmentAware {
@Nullable
private ConfigurableEnvironment environment;
/**
* 必须配置的属性的集合,在 {@link ServletConfigPropertyValues} 中,会校验是否有对应的属性
* 默认为空
*/
private final Set<String> requiredProperties = new HashSet<>(4);
protected final void addRequiredProperty(String property) {
this.requiredProperties.add(property);
}
/**
* 实现了 EnvironmentAware 接口,自动注入 Environment 对象
*/
@Override
public void setEnvironment(Environment environment) {
Assert.isInstanceOf(ConfigurableEnvironment.class, environment, "ConfigurableEnvironment required");
this.environment = (ConfigurableEnvironment) environment;
}
/**
* 实现了 EnvironmentAware 接口,返回 Environment 对象
*/
@Override
public ConfigurableEnvironment getEnvironment() {
if (this.environment == null) {
// 如果 Environment 为空,则创建 StandardServletEnvironment 对象
this.environment = createEnvironment();
}
return this.environment;
}
/**
* Create and return a new {@link StandardServletEnvironment}.
*/
protected ConfigurableEnvironment createEnvironment() {
return new StandardServletEnvironment();
}
}
关于 xxxAware接口,在 Spring 初始化该 Bean 的时候会调用其setXxx方法来注入一个对象,本文暂不分析
init方法
init()
方法,重写 GenericServlet 中的方法,负责将 ServletConfig 设置到当前 Servlet 对象中,方法如下:
@Override
public final void init() throws ServletException {
// Set bean properties from init parameters.
// <1> 解析 <init-param /> 标签,封装到 PropertyValues pvs 中
PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties);
if (!pvs.isEmpty()) {
try {
// <2.1> 将当前的这个 Servlet 对象,转化成一个 BeanWrapper 对象。从而能够以 Spring 的方式来将 pvs 注入到该 BeanWrapper 对象中
BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
ResourceLoader resourceLoader = new ServletContextResourceLoader(getServletContext());
// <2.2> 注册自定义属性编辑器,一旦碰到 Resource 类型的属性,将会使用 ResourceEditor 进行解析
bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, getEnvironment()));
// <2.3> 空实现,留给子类覆盖,目前没有子类实现
initBeanWrapper(bw);
// <2.4> 以 Spring 的方式来将 pvs 注入到该 BeanWrapper 对象中
bw.setPropertyValues(pvs, true);
}
catch (BeansException ex) {
if (logger.isErrorEnabled()) {
logger.error("Failed to set bean properties on servlet ‘" + getServletName() + "‘", ex);
}
throw ex;
}
}
// Let subclasses do whatever initialization they like.
// 交由子类去实现,查看 FrameworkServlet#initServletBean() 方法
initServletBean();
}
-
解析 Servlet 配置的
<init-param />
标签,封装成 PropertyValuespvs
对象。其中,ServletConfigPropertyValues 是 HttpServletBean 的私有静态类,继承 MutablePropertyValues 类,ServletConfig 的 封装实现类,该类的代码如下:private static class ServletConfigPropertyValues extends MutablePropertyValues { public ServletConfigPropertyValues(ServletConfig config, Set<String> requiredProperties) throws ServletException { // 获得缺失的属性的集合 Set<String> missingProps = (!CollectionUtils.isEmpty(requiredProperties) ? new HashSet<>(requiredProperties) : null); // <1> 遍历 ServletConfig 的初始化参数集合,添加到 ServletConfigPropertyValues 中,并从 missingProps 移除 Enumeration<String> paramNames = config.getInitParameterNames(); while (paramNames.hasMoreElements()) { String property = paramNames.nextElement(); Object value = config.getInitParameter(property); // 添加到 ServletConfigPropertyValues 中 addPropertyValue(new PropertyValue(property, value)); // 从 missingProps 中移除 if (missingProps != null) { missingProps.remove(property); } } // Fail if we are still missing properties. if (!CollectionUtils.isEmpty(missingProps)) { throw new ServletException("..."); } } }
在它的构造方法中可以看到,将
<init-param />
标签定义的一些配置项解析成 PropertyValue 对象,例如在前面概述的web.xml
中的配置,如下:<init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:spring-mvc.xml</param-value> </init-param>
-
如果存在
<init-param />
初始化参数- 将当前的这个 Servlet 对象,转化成一个 BeanWrapper 对象。从而能够以 Spring 的方式来将
pvs
注入到该 BeanWrapper 对象中。简单来说,BeanWrapper 是 Spring 提供的一个用来操作 Java Bean 属性的工具,使用它可以直接修改一个对象的属性 - 注册自定义属性编辑器,一旦碰到 Resource 类型的属性,将会使用 ResourceEditor 进行解析
- 调用
initBeanWrapper(BeanWrapper bw)
方法,可初始化当前这个 Servlet 对象,空实现,留给子类覆盖,目前好像还没有子类实现 - 遍历
pvs
中的属性值,注入到该 BeanWrapper 对象中,也就是设置到当前 Servlet 对象中,例如 FrameworkServlet 中的contextConfigLocation
属性则会设置为上面的classpath:spring-mvc.xml
值了
- 将当前的这个 Servlet 对象,转化成一个 BeanWrapper 对象。从而能够以 Spring 的方式来将
-
【关键】调用
initServletBean()
方法,空实现,交由子类去实现,完成自定义初始化逻辑,查看FrameworkServlet#initServletBean()
方法
FrameworkServlet
org.springframework.web.servlet.FrameworkServlet
抽象类,实现 ApplicationContextAware 接口,继承 HttpServletBean 抽象类,负责初始化 Spring Servlet WebApplicationContext 容器
构造方法
public abstract class FrameworkServlet extends HttpServletBean implements ApplicationContextAware {
// ... 省略部分属性
/** Default context class for FrameworkServlet. */
public static final Class<?> DEFAULT_CONTEXT_CLASS = XmlWebApplicationContext.class;
/** WebApplicationContext implementation class to create. */
private Class<?> contextClass = DEFAULT_CONTEXT_CLASS;
/** Explicit context config location. 配置文件的地址 */
@Nullable
private String contextConfigLocation;
/** Should we publish the context as a ServletContext attribute?. */
private boolean publishContext = true;
/** Should we publish a ServletRequestHandledEvent at the end of each request?. */
private boolean publishEvents = true;
/** WebApplicationContext for this servlet. */
@Nullable
private WebApplicationContext webApplicationContext;
/** 标记是否是通过 {@link #setApplicationContext} 注入的 WebApplicationContext */
private boolean webApplicationContextInjected = false;
/** 标记已经是否接收到 ContextRefreshedEvent 事件,即 {@link #onApplicationEvent(ContextRefreshedEvent)} */
private volatile boolean refreshEventReceived = false;
/** Monitor for synchronized onRefresh execution. */
private final Object onRefreshMonitor = new Object();
public FrameworkServlet() {
}
public FrameworkServlet(WebApplicationContext webApplicationContext) {
this.webApplicationContext = webApplicationContext;
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) {
if (this.webApplicationContext == null && applicationContext instanceof WebApplicationContext) {
this.webApplicationContext = (WebApplicationContext) applicationContext;
this.webApplicationContextInjected = true;
}
}
}
-
contextClass
属性:创建的 WebApplicationContext 类型,默认为 XmlWebApplicationContext.class,在 Root WebApplicationContext 容器的创建过程中也是它 -
contextConfigLocation
属性:配置文件的地址,例如:classpath:spring-mvc.xml
-
webApplicationContext
属性:WebApplicationContext 对象,即本文的关键,Servlet WebApplicationContext 容器,有四种创建方式- 通过上面的构造方法
- 实现了 ApplicationContextAware 接口,通过 Spring 注入,也就是
setApplicationContext(ApplicationContext applicationContext)
方法 - 通过
findWebApplicationContext()
方法,下文见 - 通过
createWebApplicationContext(WebApplicationContext parent)
方法,下文见
initServletBean
initServletBean()
方法,重写父类的方法,在 HttpServletBean 的 init() 方法的最后一步会调用,进一步初始化当前 Servlet 对象,当前主要是初始化Servlet WebApplicationContext 容器,代码如下:
@Override
protected final void initServletBean() throws ServletException {
getServletContext().log("Initializing Spring " + getClass().getSimpleName() + " ‘" + getServletName() + "‘");
if (logger.isInfoEnabled()) {
logger.info("Initializing Servlet ‘" + getServletName() + "‘");
}
long startTime = System.currentTimeMillis();
try {
// <1> 初始化 WebApplicationContext 对象
this.webApplicationContext = initWebApplicationContext();
// <2> 空实现,留给子类覆盖,目前没有子类实现
initFrameworkServlet();
}
catch (ServletException | RuntimeException ex) {
logger.error("Context initialization failed", ex);
throw ex;
}
if (logger.isDebugEnabled()) {
String value = this.enableLoggingRequestDetails ?
"shown which may lead to unsafe logging of potentially sensitive data" :
"masked to prevent unsafe logging of potentially sensitive data";
logger.debug("enableLoggingRequestDetails=‘" + this.enableLoggingRequestDetails +
"‘: request parameters and headers will be " + value);
}
if (logger.isInfoEnabled()) {
logger.info("Completed initialization in " + (System.currentTimeMillis() - startTime) + " ms");
}
}
- 调用
initWebApplicationContext()
方法,初始化 Servlet WebApplicationContext 对象 - 调用
initFrameworkServlet()
方法,可对当前 Servlet 对象进行自定义操作,空实现,留给子类覆盖,目前好像还没有子类实现
initWebApplicationContext
initWebApplicationContext()
方法【核心】,初始化 Servlet WebApplicationContext 对象,方法如下:
protected WebApplicationContext initWebApplicationContext() {
// <1> 获得根 WebApplicationContext 对象
WebApplicationContext rootContext = WebApplicationContextUtils.getWebApplicationContext(getServletContext());
// <2> 获得 WebApplicationContext wac 对象
WebApplicationContext wac = null;
// 第一种情况,如果构造方法已经传入 webApplicationContext 属性,则直接使用
if (this.webApplicationContext != null) {
// A context instance was injected at construction time -> use it
wac = this.webApplicationContext;
// 如果是 ConfigurableWebApplicationContext 类型,并且未激活,则进行初始化
if (wac instanceof ConfigurableWebApplicationContext) {
ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac;
if (!cwac.isActive()) { // 未激活
// The context has not yet been refreshed -> provide services such as
// setting the parent context, setting the application context id, etc
if (cwac.getParent() == null) {
// The context instance was injected without an explicit parent -> set
// the root application context (if any; may be null) as the parent
cwac.setParent(rootContext);
}
// 配置和初始化 wac
configureAndRefreshWebApplicationContext(cwac);
}
}
}
// 第二种情况,从 ServletContext 获取对应的 WebApplicationContext 对象
if (wac == null) {
// No context instance was injected at construction time -> see if one
// has been registered in the servlet context. If one exists, it is assumed
// that the parent context (if any) has already been set and that the
// user has performed any initialization such as setting the context id
wac = findWebApplicationContext();
}
// 第三种,创建一个 WebApplicationContext 对象
if (wac == null) {
// No context instance is defined for this servlet -> create a local one
wac = createWebApplicationContext(rootContext);
}
// <3> 如果未触发刷新事件,则主动触发刷新事件
if (!this.refreshEventReceived) {
// Either the context is not a ConfigurableApplicationContext with refresh
// support or the context injected at construction time had already been
// refreshed -> trigger initial onRefresh manually here.
synchronized (this.onRefreshMonitor) {
onRefresh(wac);
}
}
// <4> 将 context 设置到 ServletContext 中
if (this.publishContext) {
// Publish the context as a servlet context attribute.
String attrName = getServletContextAttributeName();
getServletContext().setAttribute(attrName, wac);
}
return wac;
}
-
调用
WebApplicationContextUtils#getWebApplicationContext((ServletContext sc)
方法,从 ServletContext 中获得 Root WebApplicationContext 对象,可以回到ContextLoader#initWebApplicationContext方法中的第5
步,你会觉得很熟悉 -
获得 WebApplicationContext
wac
对象,有三种情况-
如果构造方法已经传入 webApplicationContext 属性,则直接引用给
wac
,也就是上面构造方法中提到的第 1、2 种创建方式如果
wac
是 ConfigurableWebApplicationContext 类型,并且未刷新(未激活),则调用configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac)
方法,进行配置和刷新,下文见如果父容器为空,则设置为上面第
1
步获取到的 Root WebApplicationContext 对象 -
调用
findWebApplicationContext()
方法,从 ServletContext 获取对应的 WebApplicationContext 对象,也就是上面构造方法中提到的第 3 种创建方式@Nullable protected WebApplicationContext findWebApplicationContext() { String attrName = getContextAttribute(); // 需要配置了 contextAttribute 属性下,才会去查找,一般我们不会去配置 if (attrName == null) { return null; } // 从 ServletContext 中,获得属性名对应的 WebApplicationContext 对象 WebApplicationContext wac = WebApplicationContextUtils.getWebApplicationContext(getServletContext(), attrName); // 如果不存在,则抛出 IllegalStateException 异常 if (wac == null) { throw new IllegalStateException("No WebApplicationContext found: initializer not registered?"); } return wac; }
一般不会这样做
-
调用
createWebApplicationContext(@Nullable WebApplicationContext parent)
方法,创建一个 WebApplicationContext 对象protected WebApplicationContext createWebApplicationContext(@Nullable ApplicationContext parent) { // <a> 获得 context 的类,XmlWebApplicationContext.class Class<?> contextClass = getContextClass(); // 如果非 ConfigurableWebApplicationContext 类型,抛出 ApplicationContextException 异常 if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) { throw new ApplicationContextException( "Fatal initialization error in servlet with name ‘" + getServletName() + "‘: custom WebApplicationContext class [" + contextClass.getName() + "] is not of type ConfigurableWebApplicationContext"); } // <b> 创建 context 类的对象 ConfigurableWebApplicationContext wac = (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass); // <c> 设置 environment、parent、configLocation 属性 wac.setEnvironment(getEnvironment()); wac.setParent(parent); String configLocation = getContextConfigLocation(); if (configLocation != null) { wac.setConfigLocation(configLocation); } // <d> 配置和初始化 wac configureAndRefreshWebApplicationContext(wac); return wac; }
<a>
获得context
的 Class 对象,默认为 XmlWebApplicationContext.class,如果非 ConfigurableWebApplicationContext 类型,则抛出异常<b>
创建context
的实例对象<c>
设置environment
、parent
、configLocation
属性。其中,configLocation
是个重要属性<d>
调用configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac)
方法,进行配置和刷新,下文见
-
-
如果未触发刷新事件,则调用
onRefresh(ApplicationContext context)
方法,主动触发刷新事件,该方法为空实现,交由子类 DispatcherServlet 去实现 -
将
context
设置到 ServletContext 中
configureAndRefreshWebApplicationContext
configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac)
方法,配置和初始化 wac
对象,方法如下:
protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac) {
// <1> 如果 wac 使用了默认编号,则重新设置 id 属性
if (ObjectUtils.identityToString(wac).equals(wac.getId())) {
// The application context id is still set to its original default value
// -> assign a more useful id based on available information
// 情况一,使用 contextId 属性
if (this.contextId != null) {
wac.setId(this.contextId);
}
// 情况二,自动生成
else {
// Generate default id...
wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX +
ObjectUtils.getDisplayString(getServletContext().getContextPath()) + ‘/‘ + getServletName());
}
}
// <2> 设置 wac 的 servletContext、servletConfig、namespace 属性
wac.setServletContext(getServletContext());
wac.setServletConfig(getServletConfig());
wac.setNamespace(getNamespace());
// <3> 添加监听器 SourceFilteringListener 到 wac 中
wac.addApplicationListener(new SourceFilteringListener(wac, new ContextRefreshListener()));
// The wac environment‘s #initPropertySources will be called in any case when the context
// is refreshed; do it eagerly here to ensure servlet property sources are in place for
// use in any post-processing or initialization that occurs below prior to #refresh
// <4>
ConfigurableEnvironment env = wac.getEnvironment();
if (env instanceof ConfigurableWebEnvironment) {
((ConfigurableWebEnvironment) env).initPropertySources(getServletContext(), getServletConfig());
}
// <5> 执行处理完 WebApplicationContext 后的逻辑。目前是个空方法,暂无任何实现
postProcessWebApplicationContext(wac);
// <6> 执行自定义初始化 context
applyInitializers(wac);
// <7> 刷新 wac ,从而初始化 wac
wac.refresh();
}
实际上,处理逻辑和ContextLoader#configureAndRefreshWebApplicationContext
方法差不多
- 如果
wac
使用了默认编号,则重新设置id
属性 - 设置
wac
的 servletContext、servletConfig、namespace 属性 - 添加监听器 SourceFilteringListener 到
wac
中 - 配置 Environment 对象,暂时忽略
- 执行处理完 WebApplicationContext 后的逻辑,空方法,暂无任何实现
- 对
wac
进行定制化处理,暂时忽略 -
【关键】触发
wac
的刷新事件,执行初始化。此处,就会进行一些的 Spring 容器的初始化工作,涉及到 Spring IOC 相关内容
onRefresh
onRefresh(ApplicationContext context)
方法,当 Servlet WebApplicationContext 刷新完成后,触发 Spring MVC 组件的初始化,方法如下:
/**
* Template method which can be overridden to add servlet-specific refresh work.
* Called after successful context refresh.
* <p>This implementation is empty.
* @param context the current WebApplicationContext
* @see #refresh()
*/
protected void onRefresh(ApplicationContext context) {
// For subclasses: do nothing by default.
}
这是一个空方法,具体的实现,在子类 DispatcherServlet 中,代码如下:
// DispatcherServlet.java
@Override
protected void onRefresh(ApplicationContext context) {
initStrategies(context);
}
/**
* Initialize the strategy objects that this servlet uses.
* <p>May be overridden in subclasses in order to initialize further strategy objects.
*/
protected void initStrategies(ApplicationContext context) {
// 初始化 MultipartResolver
initMultipartResolver(context);
// 初始化 LocaleResolver
initLocaleResolver(context);
// 初始化 ThemeResolver
initThemeResolver(context);
// 初始化 HandlerMappings
initHandlerMappings(context);
// 初始化 HandlerAdapters
initHandlerAdapters(context);
// 初始化 HandlerExceptionResolvers
initHandlerExceptionResolvers(context);
// 初始化 RequestToViewNameTranslator
initRequestToViewNameTranslator(context);
// 初始化 ViewResolvers
initViewResolvers(context);
// 初始化 FlashMapManager
initFlashMapManager(context);
}
初始化九个组件,这里只是先提一下,在后续的文档中会进行分析
onRefresh
方法的触发有两种方式:
- 方式一:如果
refreshEventReceived
为false
,也就是未接收到刷新事件(防止重复初始化相关组件),则在initWebApplicationContext
方法中直接调用 - 方式二:通过在
configureAndRefreshWebApplicationContext
方法中,触发wac
的刷新事件
为什么上面的方式二可以触发这个方法的调用呢?
先看到 configureAndRefreshWebApplicationContext
方法的第 3
步,添加了一个 SourceFilteringListener 监听器,如下:
// <3> 添加监听器 SourceFilteringListener 到 wac 中
wac.addApplicationListener(new SourceFilteringListener(wac, new ContextRefreshListener()));
监听到相关事件后,会委派给 ContextRefreshListener
进行处理,它是 FrameworkServlet 的私有内部类,来看看它又是怎么处理的,代码如下:
private class ContextRefreshListener implements ApplicationListener<ContextRefreshedEvent> {
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
FrameworkServlet.this.onApplicationEvent(event);
}
}
直接将该事件委派给了 FrameworkServlet 的 onApplicationEvent
方法,如下:
public void onApplicationEvent(ContextRefreshedEvent event) {
// 标记 refreshEventReceived 为 true
this.refreshEventReceived = true;
synchronized (this.onRefreshMonitor) {
// 处理事件中的 ApplicationContext 对象,空实现,子类 DispatcherServlet 会实现
onRefresh(event.getApplicationContext());
}
}
先设置 refreshEventReceived
为 true
,表示已接收到刷新时间,然后再调用 onRefresh
方法,回到上面的方式一和方式二,是不是连通起来了,所以说该方法是一定会被触发的
总结
本分对 Spring MVC 两种容器的创建过程进行分析,分别为 Root WebApplicationContext 和 Servlet WebApplicationContext 容器,它们是父子关系,创建过程并不是很复杂。前置是在 Tomcat 或者 Jetty 等 Servlet 容器启动后,由 ContextLoaderListener
监听到相应事件而创建的,后者是在 DispatcherServlet
初始化的过程中创建的,因为它是一个 HttpServlet 对象,会调用其 init
方法,完成初始化相关工作
DispatcherServlet
是 Spring MVC 的核心类,相当于一个调度者,请求的处理过程都是通过它调度各个组件来完成的,在后续的文章中进行分析
参考文章:芋道源码《精尽 MyBatis 源码分析》