該系列文件是本人在學習 Spring MVC 的原始碼過程中總結下來的,可能對讀者不太友好,請結合我的原始碼註釋 Spring MVC 原始碼分析 GitHub 地址 進行閱讀
Spring 版本:5.2.4.RELEASE
該系列其他文件請檢視:《精盡 Spring MVC 原始碼分析 - 文章導讀》
隨著 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 的核心類,相當於一個排程者,請求的處理過程都是通過它排程各個元件來完成的,在後續的文章中進行分析
參考文章:芋道原始碼《精盡 Spring MVC 原始碼分析》