??基于最新Spring 5.x,詳細介紹了Spring MVC 初始化流程的原始碼,主要包括ContextLoaderListener與根背景關系容器的初始化流程的原始碼,以及web.xml檔案加載流程,
??此前的一系列專欄文章中:Spring MVC 5.x 學習,我們對Spring MVC 5.x的重要特性進行了學習,基本掌握了Spring MVC的基本使用,現在我們一起來嘗試學習Spring MVC的原始碼,嘗試從原始碼的角度再次理解Spring MVC的整體執行流程,體會組件式架構的巧妙之處!
??Spring MVC同樣依賴于Spring,關于容器初始化、bean注冊、物件創建等基礎功能的具體原始碼,我們在此前的Spring原始碼學習部分已經花了幾十萬字詳細講解過了,在此不再贅述,在學習Spring MVC的原始碼之前建議大概了解Spring的原始碼,
??本次主要學習web.xml檔案加載流程以及ContextLoaderListener監聽器的加載,即根背景關系容器的初始化流程的原始碼,
??下面的原始碼版本基于5.2.8.RELEASE,
Spring MVC原始碼 系列文章
Spring MVC 初始化原始碼(1)—ContextLoaderListener與根背景關系容器的初始化
文章目錄
- Spring MVC原始碼 系列文章
- 1 web.xml檔案加載流程
- 2 ContextLoaderListener根背景關系容器初始化
- 2.1 initWebApplicationContext初始化根背景關系容器
- 2.1.1 createWebApplicationContext創建新WebApplicationContext
- 2.1.1.1 determineContextClass獲取背景關系的Class
- 2.1.1.2 XmlWebApplicationContext
- 2.1.2 configureAndRefreshWebApplicationContext配置并重繪容器
- 2.1.2.1 組態檔路徑
- 2.1.2.2 StandardServletEnvironment環境物件
- 2.1.2.3 initPropertySources初始化Servlet屬性源
- 2.1.2.4 customizeContext應用ApplicationContextInitializer擴展
- 2.1.2.4.1 determineContextInitializerClasses確定初始化器的Class
- 3 總結
1 web.xml檔案加載流程
??引入Spring MVC之后就Java專案就成為了一個web專案,專案啟動的流程相較于此前學習的本地Spring專案變得更加復雜,我們必須找到此時的專案初始化的入口,才能更好的進行分析,
??我們的web專案實際上是一個非常被動的存在,里面沒有main方法(非Spring Boot專案),它并不會自己啟動,所謂的啟動專案,是指的我的啟動tomcat服務器,然后由tomcat服務器來對里面的web專案啟動并且進行一系列初始化操作的,而tomcat服務器是通過加載專案的web.xml組態檔來啟動整個專案的,因此,Spring MVC專案的啟動流程可以從web.xml組態檔的加載程序中略知一二!
??無論是原始Servlet的web專案,還是SSM的web專案,tomcat加載web.xml組態檔的程序和順序都是一樣的,常見組件的通用的加載順序如下(部分順序涉及到tomcat的原始碼,后面有機會我們在學習tomcat的原始碼):
tomcat服務器首先會初始化該專案的Context容器StandardContext,代表該web應用,并且會掃描web.xml檔案中標簽的資料并存入該容器的對應屬性中,包括<context-param/>標簽表示的容器常量,- 根據掃描結果初始化
web.xml中所有的定義的Listener實體, - 初始化專案中使用的(代碼獲取到的)
ServletContext容器,實際型別是一個ApplicationContextFacade(基于外觀模式),其內部封裝了一個ApplicationContext實體,ApplicationContext內部封裝<context-param>常量,還封裝了tomcat內部的Context容器實體StandardContext,可以獲取tomcat內部注冊的Servlet等資訊, - 創建
ServletContextEvent事件,其內部包含了ApplicationContextFacade容器,隨后發布該事件,即對所有的ServletContextListener實體呼叫contextInitialized方法,可以從ServletContextEvent中獲取容器初始化引數資訊, - 根據掃描結果初始化
web.xml中所有的定義的Filter實體,并呼叫對應實體的init方法初始化filter, - 加載和初始化
load-on-startup屬性大于等于0的Servelet,按照屬性值的大小從小到大依次加載和初始化,隨后呼叫init方法初始化Servlet,引數ServletConfig實際是一個StandardWrapperFacade類的物件,該類主要包含兩個屬性,config對應ServletConfig, 存盤的實體為StandardWrapper,context對應ServletContext,存盤的實際為ApplicationContext,
??簡單地說,主要加載順序就是: Listener – SrvletContext – listener#contextInitialized(ServletContextEvent)– Filter – filter#init(FilterConfig) – 加載load-on-startup屬性大于等于0的Servlet – Servlet#init(ServletConfig),
??下面就是一個基于Spring MVC的web.xml一種最常見配置,可以對應著上面的流程看看這些標簽的加載順序,
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<display-name>Archetype Created Web Application</display-name>
<!--配置contextConfigLocation初始化引數,指定父容器Root WebApplicationContext的組態檔 -->
<context-param>
<param-name>contextConfigLocation</param-name>
<!--加載全部組態檔-->
<param-value>classpath:spring-config.xml</param-value>
</context-param>
<!--監聽contextConfigLocation引數并初始化父容器-->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<filter>
<filter-name>encodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<!--設定編碼-->
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
<!--對于request和response是否強制使用指定的編碼-->
<init-param>
<param-name>forceEncoding</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>encodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<servlet>
<servlet-name>dispatcherServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet
</servlet-class>
<!--Servlet WebApplicationContext子容器的配置-->
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring-mvc-config.xml</param-value>
</init-param>
<load-on-startup>0</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>dispatcherServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
??實際上web.xml組態檔的<web-app/>標簽下可以配置很多子標簽,這些標簽在決議時都會被加載,但是很多標簽我們都是用不到的,因此我們僅僅介紹這些常見標簽的加載!
2 ContextLoaderListener根背景關系容器初始化
??Spring MVC專案支持父子容器,DispatcherServlet中初始化的容器作為子容器,通常用于存放三層架構中的表現層的bean,比如Controller,以及Spring MVC相關的組件bean實體,比如ViewResolver、HandlerMapping等,“子容器”一定會存在,
??而父容器通常包含web應用中的基礎結構 bean,例如需要跨多個Servlet實體共享的Dao、資料庫配置bean、Service等服務bean,也就是三層架構中的業務層和持久層的bean,這些 bean可以在特定Servlet 的子 WebApplicationContext 中重寫(即重新宣告),ContextLoaderListener這個監聽器可以配置,也可以不配置,通常情況下,該標簽就被用于初始化一個父容器,
??如果配置了ContextLoaderListener監聽器,那么將會有一個父容器被先初始化,我們來看看它的具體流程原始碼,

??在ServletContext容器初始化之后,將會發出容器創建事件,隨即觸發ContextLoaderListener#contextInitialized(ServletContextEvent event)方法呼叫,該方法就是我們的學習Sring MVC原始碼的入口:
/**
1. ContextLoaderListener的方法,原始碼的入口
2. <p>
3. 初始化一個 root WebApplicationContext
*/
@Override
public void contextInitialized(ServletContextEvent event) {
//呼叫ContextLoader的initWebApplicationContext方法
initWebApplicationContext(event.getServletContext());
}
??其內部呼叫的就是ContextLoader的initWebApplicationContext方法,該方法執行完畢,則專案的Root WebApplicationContext初始化完畢,
2.1 initWebApplicationContext初始化根背景關系容器
??該方法還是很簡單的,相比于單體專案的IOC容器的初始化,多了決議一些全域屬性以及呼叫ApplicationContextInitializer擴展點的邏輯,大概邏輯如下:
- 校驗如果背景關系中的"
org.springframework.web.context.WebApplicationContext.ROOT"屬性值不為null的話,那么直接拋出例外,如果不為null,說明此前已經創建過root application context容器了,不能再次創建, - 如果此ContextLoader的context屬性為
null,那么呼叫createWebApplicationContext方法初始化一個WebApplicationContext,默認為XmlWebApplicationContext, - 呼叫
configureAndRefreshWebApplicationContext方法配置并重繪新建的root WebApplicationContext,該方法中會決議容器配置位置屬性、初始化并呼叫ApplicationContextInitializer的擴展點(用于自定義root context)、執行refresh重繪容器的方法, - 將當前新建的
Root WebApplicationContext存入servletContext的屬性中,屬性名為"org.springframework.web.context.WebApplicationContext.ROOT",這也是開頭校驗的屬性,該屬性不為null就說明當前專案已經初始化好了Root WebApplicationContext,
//ContextLoader的相關屬性
/**
* 此加載程式管理的Root WebApplicationContext實體
*/
@Nullable
private WebApplicationContext context;
/**
* 如果當前初始化執行緒的ClassLoader本身就是ContextLoader,則將新建的容器背景關系設定為當前的WebApplicationContext
*/
@Nullable
private static volatile WebApplicationContext currentContext;
/**
* ContextLoader的方法
* <p>
* 使用在構建時提供的ApplicationContext或根據contextClass和contextConfigLocation引數創建一個新的ApplicationContext,
* 為給定的ServletContext初始化Spring WebApplicationContext
*
* @param servletContext 當前ServletContext
* @return 新的 WebApplicationContext
*/
public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
/*
* 如果背景關系中的"org.springframework.web.context.WebApplicationContext.ROOT"屬性值不為null的話,那么直接拋出例外
* 如果不為null,說明此前已經創建過root application context容器了,不能再次創建
*/
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!");
}
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 {
// 將背景關系存盤在本地實體變數中,以確保它在ServletContext關閉時可用,
/*
* 1 如果context屬性為null,那么初始化一個WebApplicationContext,默認為XmlWebApplicationContext
*/
if (this.context == null) {
this.context = createWebApplicationContext(servletContext);
}
//如果屬于ConfigurableWebApplicationContext型別,默認屬于
if (this.context instanceof ConfigurableWebApplicationContext) {
//強制轉換為cwac
ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) this.context;
//確定此應用程式背景關系是否處于活動狀態,即,是否至少重繪一次并且尚未關閉,
//如果背景關系尚未重繪->提供諸如設定父背景關系,設定應用程式背景關系ID等服務,
if (!cwac.isActive()) {
// 如果父背景關系為null
if (cwac.getParent() == null) {
// 確定根Web應用程式背景關系的父級,,
//一般來說沒有父背景關系,因為ContextLoader的loadParentContext方法默認直接就是回傳null的
ApplicationContext parent = loadParentContext(servletContext);
cwac.setParent(parent);
}
/*
* 2 配置并重繪新建的WebApplicationContext,這是核心方法
*/
configureAndRefreshWebApplicationContext(cwac, servletContext);
}
}
/*
* 3 將當前新建的Root WebApplicationContext 存入servletContext的屬性中
* 屬性名為"org.springframework.web.context.WebApplicationContext.ROOT"
*
* 這也是開頭校驗的屬性,該屬性不為null就說明當前專案已經初始化好了Root WebApplicationContext
*/
servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);
//獲取當前初始化執行緒的ClassLoader,這個classLoader一般都是WebappClassLoader
//WebappClassLoader是tomcat提供的,每個web應用程式都有自己專用的WebappClassLoader,用于隔離web應用之間的class的影響
ClassLoader ccl = Thread.currentThread().getContextClassLoader();
//如果當前初始化執行緒的ClassLoader本身就是ContextLoader的ClassLoader
//ContextLoader的ClassLoader同樣也是WebappClassLoader,這也是tomcat設定的
if (ccl == ContextLoader.class.getClassLoader()) {
//則將新建的容器背景關系設定為當前的WebApplicationContext
currentContext = this.context;
//如果不是并且不為null,那么將classLoader和context設定給一個map快取
} 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");
}
//最后回傳創建、初始化完畢的root 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;
}
}
2.1.1 createWebApplicationContext創建新WebApplicationContext
??如果當前ContextLoaderListener實體的context屬性為null,那么呼叫createWebApplicationContext方法初始化一個WebApplicationContext,型別可以是默認背景關系類XmlWebApplicationContext,也可以是自定義背景關系類(如果已指定),
??初始化容器時,呼叫的是無參構造器,此時可以說是僅僅創建了一個WebApplicationContext物件,并沒有進行一系列的初始化操作,
/**
* ContextLoader的方法
* <p>
* 實體化此加載器的root WebApplicationContext,型別可以是默認背景關系類,也可以是自定義背景關系類(如果已指定),
* <p>
* 指定的背景關系類期望是實作了ConfigurableWebApplicationContext介面
* 另外,在重繪背景關系之前會呼叫customContext方法,從而允許子類對背景關系執行自定義修改,
*
* @param sc 當前ServletContext
* @return root WebApplicationContext
*/
protected WebApplicationContext createWebApplicationContext(ServletContext sc) {
//回傳要使用的WebApplicationContext實作類的Class
Class<?> contextClass = determineContextClass(sc);
//如果對應的Class不是ConfigurableWebApplicationContext型別,那么拋出例外
if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {
throw new ApplicationContextException("Custom context class [" + contextClass.getName() +
"] is not of type [" + ConfigurableWebApplicationContext.class.getName() + "]");
}
//反射呼叫無參構造器初始化WebApplicationContext的實體并回傳
return (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);
}
2.1.1.1 determineContextClass獲取背景關系的Class
??該方法獲取要使用的背景關系的Class,首先采用自定義的WebApplicationContext型別,這是通過名為"contextClass"的<context-param>全域初始化引數指定的,如果沒有該引數,那么將使用默認的WebApplicationContext,即org.springframework.web.context.support.XmlWebApplicationContext,這是在ContextLoader同路徑下的ContextLoader.properties組態檔中定義的,
??也就是說,我們可以通過在web.xml檔案中定義一個param-name為contextClass的<context-param/>標簽來指定自定義的容器型別,param-value就是自定義的容器的全路徑名字串,
//----------ContextLoader的屬性和靜態塊-----------
/**
* 要使用的root WebApplicationContext實作類的配置引數:"contextClass"
* 通過該全域引數可以指定一個自定義WebApplicationContext實作類的全路徑名
*/
public static final String CONTEXT_CLASS_PARAM = "contextClass";
/**
* 定義ContextLoader的默認策略名稱的類路徑資源的名稱(相對于ContextLoader類),
*/
private static final String DEFAULT_STRATEGIES_PATH = "ContextLoader.properties";
/**
* 默認WebApplicationContext策略組態檔的屬性集合
*/
private static final Properties defaultStrategies;
static {
// 從屬性檔案加載默認策略實作,
// 當前這嚴格是內部的檔案,并不意味著應由應用程式開發人員自定義,
try {
//加載ContextLoader類路徑下的ContextLoader.properties組態檔的鍵值對到defaultStrategies集合中
//該組態檔中定義了默ContextLoader的默認WebApplicationContext實作類
//默認實作類就是: org.springframework.web.context.support.XmlWebApplicationContext
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());
}
}
/**
1. ContextLoader的方法
2. <p>
3. 回傳要使用的WebApplicationContext實作類
4. 如果未指定,則為默認XmlWebApplicationContext,或者是自定義的背景關系類,
5. 6. @param servletContext 當前ServletContext
7. @return 使用的WebApplicationContext實作類
*/
protected Class<?> determineContextClass(ServletContext servletContext) {
//獲取名為"contextClass"的全域引數,該引數用于指定自定義的WebApplicationContext
//一般都是沒有指定的,即獲取結果為null
String contextClassName = servletContext.getInitParameter(CONTEXT_CLASS_PARAM);
//如果不為null,說明指定了該引數
if (contextClassName != null) {
try {
//獲取自定義的WebApplicationContext的Class
return ClassUtils.forName(contextClassName, ClassUtils.getDefaultClassLoader());
} catch (ClassNotFoundException ex) {
throw new ApplicationContextException(
"Failed to load custom context class [" + contextClassName + "]", ex);
}
}
//如果為null,說明沒有指定該引數,將使用默認WebApplicationContext實作類
else {
//從defaultStrategies集合中獲取名為org.springframework.web.context.WebApplicationContext的屬性值
//默認實作類就是: org.springframework.web.context.support.XmlWebApplicationContext
contextClassName = defaultStrategies.getProperty(WebApplicationContext.class.getName());
try {
//獲取默認的XmlWebApplicationContext的Class
return ClassUtils.forName(contextClassName, ContextLoader.class.getClassLoader());
} catch (ClassNotFoundException ex) {
throw new ApplicationContextException(
"Failed to load default context class [" + contextClassName + "]", ex);
}
}
}
??ContextLoader.properties如下:

2.1.1.2 XmlWebApplicationContext
??XmlWebApplicationContext是org.springframework.web.context.WebApplicationContext的一種實作,該實作從XML檔案獲取配置,它的uml類圖如下:

??在最開始學習原始碼的時候我們就已經介紹了Spring的ApplicationContext體系,在此對于學習過的類不再贅述,
??WebApplicationContext繼承了ApplicationContext介面,來自于spring-web依賴,實作該介面的容器專門用于基于Servlet的web專案,該介面相比于ApplicationContext,多了一個getServletContext方法,即獲取Servlet背景關系,因此,除了標準的ApplicationContext生命周期功能外,WebApplicationContext的實作還需要檢測ServletContextAware Bean并相應地呼叫setServletContext方法,

??可配置的WebApplicationContext需要實作ConfigurableWebApplicationContext的介面,該介面提供了設定ServletContext、ServletConfig、Namespace、ConfigLocation的方法,可以對背景關系進行自定義配置,

??AbstractRefreshableWebApplicationContext是ConfigurableWebApplicationContext的骨干實作,提供了各種屬性用來保存配置的資料,

??同時它還繼承了AbstractRefreshableConfigApplicationContext,因此是一個可重繪的ApplicationContext,繼承了此前講過的容器初始化的所有的功能,
??XmlWebApplicationContext是org.springframework.web.context.WebApplicationContext的一種可用實作,該實作從XML檔案獲取配置,默認情況下,將從“/WEB-INF/applicationContext.xml”獲取根背景關系的配置路徑,從“/WEB-INF/test-servlet.xml”獲取具有“test-servlet” 名稱空間的子背景關系的配置路徑(例如servlet-name為“test”的DispatcherServlet實體),
??可以通過org.springframework.web.context.ContextLoader的contextConfigLocation背景關系引數(即全域<context-param/>配置的contextConfigLocation引數)和org.springframework.web.servlet.FrameworkServlet的contextConfigLocation引數(即servlet內部的<init-param/>配置的contextConfigLocation引數)覆寫配置位置的默認值,配置的位置值可以表示“/WEB-INF/context.xml”之類的某個具體檔案,也可以使用“/WEB-INF/*-context.xml”之類的Ant樣式的模式匹配多個檔案,
??如果有多個配置位置,則較新的Bean定義將覆寫較早加載的檔案中的定義,可以利用它來通過一個額外的XML檔案有意覆寫某些bean定義,
2.1.2 configureAndRefreshWebApplicationContext配置并重繪容器
??在創建了空容器之后(默認是XmlWebApplicationContext),將會呼叫configureAndRefreshWebApplicationContext方法配置并重繪該容器,
??該方法執行完畢,則新建的WebApplicationContext容器配置并初始化完畢,
??主要有如下步驟:
- 設定該應用程式
背景關系的id(一般用不到),默認id就是“org.springframework.web.context.WebApplicationContext:”+專案路徑,可以通過在web.xml中配置名為contextId的<context-param/>全域引數來自定義Root容器id, - 將
ServletContext設定給該容器的servletContext屬性, - 設定容器配置資訊,首先獲取名為
contextConfigLocation的全域屬性,如果配置了該屬性,那么該屬性的值將作為組態檔的路徑,隨后就呼叫setConfigLocation方法決議傳入的配置值,用以設定容器配置資訊(配置值支持按照",; \t\n"來拆分), - 獲取容器的
Environment環境變數物件,隨后呼叫initPropertySources方法手動初始化Servlet屬性源,該方法在refresh()重繪容器的方法之前執行,以確保servlet屬性源已準備就緒,可以被refresh()方法正常使用, - 呼叫
customizeContext方法用于對容器執行自定義操作,默認實作是通過web.xml中配置的全域引數contextInitializerClasses和globalInitializerClasses來確定指定了哪些ApplicationContextInitializer類,并執行初始化,隨后使用AnnotationAwareOrderComparator排序(支持PriorityOrdered介面、Ordered介面、@Ordered注解、@Priority注解的排序),最后按照排序優先級從高到低一次呼叫每一個實體的initialize方法來初始化給定的servletContext, - 呼叫容器的
refresh方法執行重繪操作,這是核心方法,我們在此前IoC容器初始化原始碼部分已經著重講解了,該方法將會初始化容器,包括決議組態檔,創建Spring bean實體,執行各種回呼方法等等……操作(原始碼非常多),
//ContextLoader的常量屬性
/**
* Root WebApplicationContext ID的配置引數,用作基礎BeanFactory的序列化ID:"contextId",
*/
public static final String CONTEXT_ID_PARAM = "contextId";
/**
* 指定Root WebApplicationContext的組態檔位置的servletContext引數的名稱
* 即"contextConfigLocation",如果不存在該引數,則查找默認值,
*/
public static final String CONFIG_LOCATION_PARAM = "contextConfigLocation";
/**
* ContextLoader的方法
* <p>
* 配置并重繪給定的WebApplicationContext
*
* @param wac WebApplicationContext
* @param sc ServletContext
*/
protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac, ServletContext sc) {
/*
* 1 如果wac的全路徑identity字串形式等于wac的id,那么設定應用程式背景關系的id
*/
//創建WebApplicationContext時,id默認就是ObjectUtils.identityToString的值,因此一般都相等
if (ObjectUtils.identityToString(wac).equals(wac.getId())) {
//獲取名為contextId的全域屬性值
String idParam = sc.getInitParameter(CONTEXT_ID_PARAM);
if (idParam != null) {
//如果設定了該屬性,那么將該屬性的值設定為id
wac.setId(idParam);
} else {
//如果沒有設定該屬性,那么設定默認id: 全路徑字串+":"+專案路徑,比如專案路徑是"/mvc"
//那么id就是,org.springframework.web.context.WebApplicationContext:/mvc
// Generate default id...
wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX +
ObjectUtils.getDisplayString(sc.getContextPath()));
}
}
/*
* 2 將ServletContext設定給servletContext屬性
*/
wac.setServletContext(sc);
/*
* 3 設定容器配置資訊
*/
//獲取名為contextConfigLocation的全域屬性值
String configLocationParam = sc.getInitParameter(CONFIG_LOCATION_PARAM);
if (configLocationParam != null) {
/*
* 如果具有該屬性,那么該屬性的值將作為組態檔的路徑
* 呼叫setConfigLocation方法設定容器配置資訊,該方法的原始碼我們在IOC原始碼的開頭就講過了
*
* 該方法中將會通過getEnvironment方法初始化當前背景關系的可配置的環境變數物件environment,
* 實際型別為StandardServletEnvironment
*/
wac.setConfigLocation(configLocationParam);
}
/*
* 4 獲取容器的Environment環境變數物件,隨后呼叫initPropertySources方法手動初始化屬性源
* 該方法在refresh()重繪容器的方法之前執行,以確保servlet屬性源已準備就緒,可以被正常使用
*/
ConfigurableEnvironment env = wac.getEnvironment();
if (env instanceof ConfigurableWebEnvironment) {
//由于是Root Context,因此只初始化ServletContext屬性源
((ConfigurableWebEnvironment) env).initPropertySources(sc, null);
}
/*
* 5 自定義容器
*
* 對當前的WebApplicationContext實體應用給定的ApplicationContextInitializer,以實作自定義背景關系的邏輯
*/
customizeContext(sc, wac);
/*
* 6 重繪(初始化)容器
* 這是核心方法,我們在此前IoC容器初始化原始碼部分已經著重講解了
*/
wac.refresh();
}
2.1.2.1 組態檔路徑
??configureAndRefreshWebApplicationContext方法中會決議自定義的組態檔路徑(如果配置了contextConfigLocation屬性),而在后續loadBeanDefinitions加載bean定義的方法中將會呼叫getConfigLocations方法獲取組態檔路徑,
??如果配置了contextConfigLocation屬性,那么就是根據該屬性的值指定的組態檔路徑來初始化,
??如果沒有配置該屬性,那么就會呼叫getDefaultConfigLocations方法獲取默認路徑:
- 對于
Root WebApplicationContext,默認路徑為"/WEB-INF/applicationContext.xml", - 對于
DispatcherServlet系結的子WebApplicationContext,默認路徑為"/WEB-INF/"+容器nameSpace+ ".xml", Root容器沒有nameSpace(為null),MVC子容器則擁有,DispatcherServlet對應的子容器的nameSpace就等于DispatcherServlet的namespace,可以通過設定Servlet的nameSpace屬性手動指定名稱空間,如未指定,那么默認名稱空間為servletName+"-servlet",即如果此servlet的servlet-name為"test",則該servlet使用的默認名稱空間將決議為"test-servlet",如果也未指定該Servlet的contextConfigLocation屬性,那么最終的默認配置路徑就是"/WEB-INF/test-servlet.xml",
??是不是和其他文章中常說的默認路徑有些不一樣 ?還有些復雜,是的,真正的規則就是這樣的!
//XmlWebApplicationContext中的常量屬性
/**
* root context的默認配置位置
*/
public static final String DEFAULT_CONFIG_LOCATION = "/WEB-INF/applicationContext.xml";
/**
* 用于為namespace構建配置位置的默認前綴
*/
public static final String DEFAULT_CONFIG_LOCATION_PREFIX = "/WEB-INF/";
/**
* 用于為namespace構建配置位置的默認后綴
*/
public static final String DEFAULT_CONFIG_LOCATION_SUFFIX = ".xml";
/**
* XmlWebApplicationContext的方法
* <p>
* 如果沒有指定配置路徑,那么使用默認配置路徑
* <p>
* root context的默認路徑是"/WEB-INF/applicationContext.xml"
* DispatcherServlet context的默認路徑是"/WEB-INF/"+nameSpace+".xml"
* root context沒有nameSpace屬性(為null),該屬性只有 DispatcherServlet關聯的容器會配置
*/
@Override
protected String[] getDefaultConfigLocations() {
if (getNamespace() != null) {
return new String[]{DEFAULT_CONFIG_LOCATION_PREFIX + getNamespace() + DEFAULT_CONFIG_LOCATION_SUFFIX};
} else {
return new String[]{DEFAULT_CONFIG_LOCATION};
}
}
??DispatcherServlet對應的容器的nameSpace就等于DispatcherServlet的namespace,可以通過設定Servlet的nameSpace屬性手動指定名稱空間,如未指定,那么默認名稱空間為servletName+"-servlet",即如果此servlet的servlet-name為"test",則該servlet使用的默認名稱空間將決議為"test-servlet",如果也未指定該Servlet的contextConfigLocation屬性,那么最終的默認配置路徑就是"/WEB-INF/test-servlet.xml",
/**
* FrameworkServlet中的常量屬性
* <p>
* WebApplicationContext名稱空間的后綴,
* 如果此類的servlet在背景關系中被命名為"test",則servlet使用的默認名稱空間將決議為"test-servlet",
*/
public static final String DEFAULT_NAMESPACE_SUFFIX = "-servlet";
/**
* FrameworkServlet的方法
* <p>
* 獲取此nameSpace
* <p>
* 回傳此servlet的名稱空間,如果未設定自定義名稱空間,則回傳默認方案:
* 即默認nameSpace為servletName+"-servlet",也可以通過設定Servlet的nameSpace屬性手動指定名稱空間
*/
public String getNamespace() {
return (this.namespace != null ? this.namespace : getServletName() + DEFAULT_NAMESPACE_SUFFIX);
}
2.1.2.2 StandardServletEnvironment環境物件
??setConfigLocation方法的原始碼我們在IOC原始碼的開頭文章中就講過了,該方法中將會通過getEnvironment方法初始化當前背景關系的可配置的環境變數物件environment,實際型別為StandardServletEnvironment,另外,就算沒有設定contextConfigLocation方法,也會在之后的getEnvironment方法中初始化,
??StandardServletEnvironment是基于Servlet的Web應用程式將使用的環境實作,默認情況下,所有與Web相關的(基于Servlet的)ApplicationContext類都將初始化一個實體,它的uml類圖如下:

??StandardServletEnvironment繼承了StandardEnvironment,同時實作了ConfigurableWebEnvironment介面,該介面提供了一個initPropertySources方法,用于初始化ServletContext和ServletConfig中提供的屬性源,
??StandardServletEnvironment創建的時候,在呼叫父類AbstractEnvironment的構造器的時候,將會執行customizePropertySources方法,該方法被StandardServletEnvironment重寫,用于初始化默認的一系列屬性源!相比于父類StandardEnvironment,除了JVM系統屬性源和系統環境屬性源之外,額外提供了ServletConfig,ServletContext和基于JNDI的屬性源,
//StandardServletEnvironment的屬性常量
/**
* ServletContext初始化引數屬性源名稱
*/
public static final String SERVLET_CONTEXT_PROPERTY_SOURCE_NAME = "servletContextInitParams";
/**
* ServletConfig初始化引數屬性源名稱
*/
public static final String SERVLET_CONFIG_PROPERTY_SOURCE_NAME = "servletConfigInitParams";
/**
* JNDI屬性源名稱
*/
public static final String JNDI_PROPERTY_SOURCE_NAME = "jndiProperties";
/**
* 使用父類提供的屬性源以及適用于基于標準servlet的環境的屬性源來定制屬性源集合:
* "servletConfigInitParams"
* "servletContextInitParams"
* "jndiProperties"
* <p>
* "servletConfigInitParams"中存在的屬性將優先于"servletContextInitParams"中的屬性,
* 而在上述任何一個中找到的屬性都將優先于在"jndiProperties"中找到的屬性,
* 而以上任何一項中的屬性都將優先于StandardEnvironment超類提供的JVM系統屬性源和系統環境屬性源,
* <p>
* 與Servlet相關的屬性源在此階段將被添加一個空的屬性源,并且在實際的ServletContext物件可用時將被完全初始化,
*/
@Override
protected void customizePropertySources(MutablePropertySources propertySources) {
//空的servletConfigInitParams屬性源添加到集合尾部
propertySources.addLast(new StubPropertySource(SERVLET_CONFIG_PROPERTY_SOURCE_NAME));
//空的servletContextInitParams屬性源添加到集合尾部
propertySources.addLast(new StubPropertySource(SERVLET_CONTEXT_PROPERTY_SOURCE_NAME));
//空的jndiProperties屬性源添加到集合尾部
if (JndiLocatorDelegate.isDefaultJndiEnvironmentAvailable()) {
propertySources.addLast(new JndiPropertySource(JNDI_PROPERTY_SOURCE_NAME));
}
//呼叫父類StandardEnvironment的方法繼續添加systemProperties和systemEnvironment屬性源到尾部
super.customizePropertySources(propertySources);
}
//StandardEnvironment的屬性常量
/**
* 系統環境屬性源名稱
*/
public static final String SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME = "systemEnvironment";
/**
* JVM系統屬性屬性源名稱
*/
public static final String SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME = "systemProperties";
/**
* 使用適用于任何標準Java環境的屬性定制屬性源集:
* "systemProperties"
* "systemEnvironment"
* <p>
* "systemProperties"中存在的屬性將優先于"systemEnvironment"中的同名屬性,
*/
@Override
protected void customizePropertySources(MutablePropertySources propertySources) {
//首先添加systemProperties屬性
propertySources.addLast(
new PropertiesPropertySource(SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME, getSystemProperties()));
//其次添加systemEnvironment屬性
propertySources.addLast(
new SystemEnvironmentPropertySource(SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, getSystemEnvironment()));
}
??對此,我們知道,基于Servlet的web專案中的默認屬性源的查找優先級為:ServletConfig初始化引數屬性源 > ServletContext初始化引數屬性源 > JNDI屬性源 > JVM系統屬性屬性源 > 系統環境屬性源,如果存在同名屬性,那么優先級最高的屬性源中的屬性值將被使用,
2.1.2.3 initPropertySources初始化Servlet屬性源
??該方法用于初始化Servlet相關的屬性源,實際上就是對這錢加入的空資料源替換為真正包含資料的屬性源,順序沒有變,
/**
* StandardServletEnvironment的方法
* <p>
* 初始化Servlet屬性源
*
* @param servletContext servletContext
* @param servletConfig servletConfig
*/
@Override
public void initPropertySources(@Nullable ServletContext servletContext, @Nullable ServletConfig servletConfig) {
WebApplicationContextUtils.initServletPropertySources(getPropertySources(), servletContext, servletConfig);
}
/**
* WebApplicationContextUtils
* <p>
* 將基于Servlet的空屬性源替換為使用給定ServletContext和ServletConfig物件填充的實際屬性源實體,
* <p>
* 此方法可以呼叫任意次,它是冪等的,因為將用其相應的實際屬性源只會執行一次且僅一次的空屬性源替換,
*
* @param sources 需要初始化的屬性源集合
* @param servletContext 當前的ServletContext(如果為null或servlet背景關系屬性源已經初始化,則將其忽略)
* @param servletConfig 當前的ServletConfig(如果為null或servlet config屬性源已經初始化,則將其忽略)
*/
public static void initServletPropertySources(MutablePropertySources sources,
@Nullable ServletContext servletContext, @Nullable ServletConfig servletConfig) {
Assert.notNull(sources, "'propertySources' must not be null");
//初始化servletContextInitParams屬性源
String name = StandardServletEnvironment.SERVLET_CONTEXT_PROPERTY_SOURCE_NAME;
if (servletContext != null && sources.get(name) instanceof StubPropertySource) {
//存入ServletContextPropertySource
sources.replace(name, new ServletContextPropertySource(name, servletContext));
}
//初始化servletConfigInitParams屬性源
name = StandardServletEnvironment.SERVLET_CONFIG_PROPERTY_SOURCE_NAME;
if (servletConfig != null && sources.get(name) instanceof StubPropertySource) {
//存入ServletConfigPropertySource
sources.replace(name, new ServletConfigPropertySource(name, servletConfig));
}
}
2.1.2.4 customizeContext應用ApplicationContextInitializer擴展
??在refresh方法執行之前,會呼叫customizeContext方法用于對IOC容器執行自定義操作,
??默認實作是通過背景關系初始化引數contextInitializerClasses和globalInitializerClasses來確定指定了哪些ApplicationContextInitializer類,并執行初始化,隨后使用AnnotationAwareOrderComparator排序(支持PriorityOrdered介面、Ordered介面、@Ordered注解、@Priority注解的排序),最后按照排序優先級從高到低一次呼叫每一個實體的initialize方法來初始化給定的servletContext,
/**
* ContextLoader的屬性
* 應用于背景關系的實際ApplicationContextInitializer實體
*/
private final List<ApplicationContextInitializer<ConfigurableApplicationContext>> contextInitializers =
new ArrayList<>();
/**
* ContextLoader的方法
* <p>
* 在將設定組態檔位置之后,重繪背景關系之前,可以自定義由此ContextLoader創建的ConfigurableWebApplicationContext,
* <p>
* 默認實作通過背景關系初始化引數contextInitializerClasses和globalInitializerClasses來確定指定了哪些ApplicationContextInitializer
* 類,并執行初始化,隨后排序并呼叫每一個實體的initialize方法來初始化給定的servlet context
*
* @param sc 當前 servlet context
* @param wac 新創建的應用程式背景關系
*/
protected void customizeContext(ServletContext sc, ConfigurableWebApplicationContext wac) {
/*
* 1 獲取ServletContext中通過指定屬性定義的ApplicationContextInitializer的全路徑類名,并且轉換為Class集合
*/
List<Class<ApplicationContextInitializer<ConfigurableApplicationContext>>> initializerClasses =
determineContextInitializerClasses(sc);
/*
* 2 初始化全部的ApplicationContextInitializer并存入contextInitializers快取集合中
*/
for (Class<ApplicationContextInitializer<ConfigurableApplicationContext>> initializerClass : initializerClasses) {
Class<?> initializerContextClass =
GenericTypeResolver.resolveTypeArgument(initializerClass, ApplicationContextInitializer.class);
if (initializerContextClass != null && !initializerContextClass.isInstance(wac)) {
throw new ApplicationContextException(String.format(
"Could not apply context initializer [%s] since its generic parameter [%s] " +
"is not assignable from the type of application context used by this " +
"context loader: [%s]", initializerClass.getName(), initializerContextClass.getName(),
wac.getClass().getName()));
}
this.contextInitializers.add(BeanUtils.instantiateClass(initializerClass));
}
/*
* 3 對該集合進行Order排序,可以支持PriorityOrdered介面、Ordered介面、@Ordered注解、@Priority注解的排序
* 比較優先級為PriorityOrdered>Ordered>@Ordered>@Priority,排序規則是order值越小排序越靠前,優先級越高
* 沒有order值則默認排在尾部,優先級最低,
*/
AnnotationAwareOrderComparator.sort(this.contextInitializers);
/*
* 4 按照優先級從高到低依次呼叫ApplicationContextInitializer的initialize方法,傳遞的引數就是當前的應用程式背景關系容器
*/
for (ApplicationContextInitializer<ConfigurableApplicationContext> initializer : this.contextInitializers) {
initializer.initialize(wac);
}
}
2.1.2.4.1 determineContextInitializerClasses確定初始化器的Class
??該方法通過獲取servletContext中的contextInitializerClasses和globalInitializerClasses屬性來確定指定的ApplicationContextInitializer的Class,
??也就是說,我們可以通過在web.xml中定義名為contextInitializerClasses和globalInitializerClasses的<context-param/>全域引數來指定自定義的ApplicationContextInitializer的全路徑名,從而實作對Root Context的自定義的邏輯,并且支持使用",; \t\n"來分隔,
??這些屬性用于自定義擴展的邏輯,一般來說都是很少有人配置的,
//ContextLoader的常量屬性
/**
* ApplicationContextInitializer類的配置引數,用于初始化root Web應用程式背景關系:contextInitializerClasses
*/
public static final String CONTEXT_INITIALIZER_CLASSES_PARAM = "contextInitializerClasses";
/**
* 全域的ApplicationContextInitializer類的配置引數,用于初始化當前應用程式中的所有Web應用程式背景關系:globalInitializerClasses
*/
public static final String GLOBAL_INITIALIZER_CLASSES_PARAM = "globalInitializerClasses";
/**
* 在單個init-param字串值中,任意數量的這些字符都被視為多個值之間的定界符,
* 即在init-param的值中支持使用下面的字符來區別多個字串
*/
private static final String INIT_PARAM_DELIMITERS = ",; \t\n";
/**
* ContextLoader的方法
* <p>
* 回傳ApplicationContextInitializer的實作類的Class集合
* <p>
* 我們可以通過定義名為contextInitializerClasses和globalInitializerClasses的全域屬性來
* 決議自定義的ApplicationContextInitializer的實作類,值要求是類的全路徑名
*
* @param servletContext 當前 servlet context
*/
protected List<Class<ApplicationContextInitializer<ConfigurableApplicationContext>>>
determineContextInitializerClasses(ServletContext servletContext) {
List<Class<ApplicationContextInitializer<ConfigurableApplicationContext>>> classes =
new ArrayList<>();
//從servletContext中嘗試獲取globalInitializerClasses全域引數的值
String globalClassNames = servletContext.getInitParameter(GLOBAL_INITIALIZER_CLASSES_PARAM);
if (globalClassNames != null) {
//根據",; \t\n"拆分值字串
for (String className : StringUtils.tokenizeToStringArray(globalClassNames, INIT_PARAM_DELIMITERS)) {
//根據全路徑名獲取對應的Class并存入classes集合中
classes.add(loadInitializerClass(className));
}
}
//從servletContext中嘗試獲取contextInitializerClasses全域屬性的值
String localClassNames = servletContext.getInitParameter(CONTEXT_INITIALIZER_CLASSES_PARAM);
if (localClassNames != null) {
//根據",; \t\n"拆分值字串
for (String className : StringUtils.tokenizeToStringArray(localClassNames, INIT_PARAM_DELIMITERS)) {
//根據全路徑名獲取對應的Class并存入classes集合中
classes.add(loadInitializerClass(className));
}
}
//回傳最終結果集
return classes;
}
3 總結
??本文是Spring MVC初始化原始碼的第一篇文章,主要講解了兩部分:
- 首先簡單介紹了
web.xml檔案的加載流程,從該流程中可以找到Spring MVC專案的初始化流程,主要配置的家在順序為:Listener – SrvletContext – listener#contextInitialized(ServletContextEvent)– Filter – filter#init(FilterConfig) – 加載load-on-startup屬性大于等于0的Servlet – Servlet#init(ServletConfig), - 還學習了Spring MVC的
ContextLoaderListener監聽器的加載流程,這個監聽器實際上就是用于加載Spring MVC的根背景關系容器的,主要的相關全域屬性就是contextConfigLocation屬性,該屬性用于指定父容器Root WebApplicationContext的組態檔的位置,默認路徑是/WEB-INF/applicationContext.xml,另外還介紹了一些其他步驟,比如屬性源初始化,
相關文章:
??https://spring.io/
??Spring Framework 5.x 學習
??Spring MVC 5.x 學習
??Spring Framework 5.x 原始碼
如有需要交流,或者文章有誤,請直接留言,另外希望點贊、收藏、關注,我將不間斷更新各種Java學習博客!
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/286647.html
標籤:其他
上一篇:字符和字串的庫函式【C進階】
