spring mvc的啟動,是跟隨著tomcat啟動的,所以要深入理解spring mvc的啟動過程與原理,需要先了解下tomcat啟動的一些關鍵過程。
1、tomcat web應用啟動及初始化過程
參考官方文檔,tomcat web應用啟動過程是這樣的:
圖1 tomcat web應用啟動過程
大概意思就是,當一個Web應用部署到容器內時,在web應用開始執行用戶請求前,會依次執行以下步驟:
- 部署描述文件web.xml中<listener>元素標記的事件監聽器會被創建和初始化;
- 對於所有事件監聽器,如果實現了ServletContextListener接口,將會執行其實現的contextInitialized()方法;
- 部署描述文件中由<filter>元素標記的過濾器會被創建和初始化,並調用其init()方法;
- 部署描述文件中由<servlet>元素標記的servlet會根據<load-on-startup>的權值按順序創建和初始化,並調用其init()方法;
通過上述文檔的描述,可知tomcat web應用啟動初始化流程是這樣的:
圖2 tomcat web應用初始化過程
可以看出,在tomcat web應用的初始化流程是,先初始化listener,接著初始化filter,最後初始化servlet。
2、spring mvc應用的啟動初始化
做過spring mvc項目開發的夥伴,都會配置一個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_2_5.xsd" version="2.5">
<!--全局變量配置-->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath*:spring/spring-main.xml
</param-value>
</context-param>
<!--監聽器-->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<listener>
<listener-class>org.springframework.web.context.Request.RequestContextListener</listener-class>
</listener>
<!--解決亂碼問題的filter-->
<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>
<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>
<!-- MVC Servlet -->
<servlet>
<servlet-name>springServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath*:spring/spring-mvc*.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>springServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
web.xml配置文件中也主要是配置了Listener,Filter,Servlet。
所以spring mvc應用啟動的時候,主要是在這三大組件初始化的過程中完成對spring及spring mvc的初始化。
3、Listener與spring的初始化過程
web.xml配置文件中首先定義了<context-param>標籤,用於配置一個全局變量,<context-param>標籤的內容讀取後會做為Web應用的全局變量使用。當Listener創建的時候,會使用到這個全局變量,因此,Web應用在容器中部署後,進行初始化時會先讀取這個全局變量,之後再進行初始化過程。
接著定義了一個ContextLoaderListener類的Listener。查看ContextLoaderListener的類聲明如圖:
圖3 ContextLoaderListener類源碼
ContextLoaderListener類繼承自ContextLoader類,並實現了ServletContextListener接口。
圖4 ServletContextListener源碼
ServletContextListener只有兩個方法,contextInitialized和contextDestroyed,當Web應用初始化或銷毀時會分別調用這兩個方法。
ContextLoaderListener實現了ServletContextListener接口,因此在Web應用初始化時會調用contextInitialized方法,該方法的具體實現如下:
圖5 contextInitialized方法
ContextLoaderListener的contextInitialized()方法直接調用了initWebApplicationContext()方法,這個方法是繼承自ContextLoader類,通過函數名可以知道,該方法是用於初始化web應用上下文,即IoC容器。
這裡是spring mvc應用啟動的第一個重點,就是在ContextLoaderListener初始化的時候,初始化了spring IOC容器。
我們繼續看ContextLoader類的initWebApplicationContext()方法。
//servletContext,servlet上下文
public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
// 首先通過WebApplicationContext.root_WEB_APPLICATION_CONTEXT_ATTRIBUTE
// 這個String類型的靜態變量獲取一個根IoC容器,根IoC容器作為全局變量
// 存儲在servletContext對象中,如果存在則有且只能有一個
// 如果在初始化根WebApplicationContext即根IoC容器時發現已經存在
// 則直接拋出異常,因此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!");
}
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.
// 如果當前成員變量中不存在WebApplicationContext則創建一個根WebApplicationContext
if (this.context == null) {
this.context = createWebApplicationContext(servletContext);
}
if (this.context instanceof ConfigurableWebApplicationContext) {
ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) this.context;
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 ->
// determine parent for root web application context, if any.
// 為根WebApplicationContext設置一個父容器
ApplicationContext parent = loadParentContext(servletContext);
cwac.setParent(parent);
}
// 配置並刷新整個根IoC容器,在這裡會進行Bean的創建和初始化
configureAndRefreshWebApplicationContext(cwac, servletContext);
}
}
// 將創建好的IoC容器放入到servletContext對象中,並設置key為WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE
// 因此,在springMVC開發中可以在jsp中通過該key在application對象中獲取到根IoC容器,進而獲取到相應的Ben
servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);
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");
}
return this.context;
}
catch (RuntimeException | Error ex) {
logger.error("Context initialization failed", ex);
servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, ex);
throw ex;
}
}
在jsp中,可以通過這兩種方法獲取到IOC容器:
WebApplicationContext applicationContext = (WebApplicationContext) servletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE);
WebApplicationContext applicationContext = WebApplicationContextUtils.getWebApplicationContext(getServletContext());
總的來說,initWebApplicationContext方法的主要目的是創建root WebApplicationContext對象,即根IOC容器。
如果整個Web應用存在根IOC容器則有且只能有一個,根IOC容器會作為全局變量存儲在ServletContext對象中。然後將根IOC容器放入到ServletContext對象之前進行了IOC容器的配置和刷新操作,即調用configureAndRefreshWebApplicationContext()方法,該方法源碼如下:
protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac, ServletContext sc) {
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
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()));
}
}
wac.setServletContext(sc);
// 獲取web.xml中<context-param>標籤配置的全局變量,其中key為CONFIG_LOCATION_PARAM
// 也就是我們配置的相應Bean的xml文件名,並將其放入到WebApplicationContext中
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);
}
customizeContext(sc, wac);
wac.refresh();
}
configureAndRefreshWebApplicationContext方法裡獲取了<context-param>標籤配置的全局變量,並且在方法最後調用了refresh()方法。
對spring容器初始化有一定了解的同學都知道,這是初始化spring容器的入口方法,其最終調用的是AbstractApplicationContext類中的refresh方法。
refresh()方法主要是創建並初始化contextConfigLocation類配置的xml文件中的Bean。
具體代碼不貼了,有興趣的同學,可以自行查閱。
到此為止,整個ContextLoaderListener類的啟動過程就結束了,可以發現,創建ContextLoaderListener是比較重要的一個步驟,主要做的事情就是創建根IOC容器,並使用特定的key將其放入到servletContext對象中,供整個Web應用使用。
由於在ContextLoaderListener類中構造的根IOC容器配置的Bean是全局共享的,因此,在<context-param>標識的contextConfigLocation的xml配置文件一般包括:資料庫DataSource、DAO層、Service層、事務等相關Bean。
4、Filter與spring的初始化
在監聽器Listener初始化完成後,接下來會進行Filter的初始化操作,Filter的創建和初始化沒有涉及IOC容器的相關操作,因此不是本文講解的重點。
5、Servlet與spring的初始化
web應用啟動的最後一個步驟就是創建和初始化相關servlet,servlet最重要的方法就是:init(),service(),destroy();
圖6 Servlet源碼
在spring mvc中,實現了一個非常重要的servlet,即DispatcherServlet。它是整個spring mvc應用的核心,用於獲取分發用戶請求並返迴響應。
圖7 DispatcherServlet類圖
DispatcherServlet本質上也是一個Servlet,其源碼實現充分利用了模板模式,將不變的部分統一實現,將變化的部分留給子類實現,上層父類不同程度的實現了相關接口的部分方法,留出了相關方法由子類覆蓋。
圖8 DispatcherServlet類初始化過程
web應用部署到容器啟動後,進行Servlet的初始化時會調用相關的init(ServletConfig)方法,因此,DispatchServlet類的初始化過程也由該方法開始。
其中比較重要的是,FrameworkServlet類中的initServletBean()方法、initWebApplicationContext()方法以及DispatcherServlet類中的onRefresh()方法。
圖9 FrameworkServlet類中的initServletBean()方法
FrameworkServlet的父類HttpServletBean中的initServletBean()方法,HttpServletBean抽象類在執行init()方法時會調用initServletBean()方法。
該方法中比較重要的是initWebApplicationContext()方法,該方法仍由FrameworkServlet抽象類實現,繼續查看其源碼如下所示:
protected WebApplicationContext initWebApplicationContext() {
// 獲取由ContextLoaderListener創建的根IoC容器
// 獲取根IoC容器有兩種方法,還可通過key直接獲取
WebApplicationContext rootContext =
WebApplicationContextUtils.getWebApplicationContext(getServletContext());
WebApplicationContext wac = null;
if (this.webApplicationContext != null) {
// A context instance was injected at construction time -> use it
wac = this.webApplicationContext;
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
// 如果當前Servelt存在一個WebApplicationContext即子IoC容器
// 並且上文獲取的根IoC容器存在,則將根IoC容器作為子IoC容器的父容器
cwac.setParent(rootContext);
}
// 配置並刷新當前的子IoC容器,功能與前面講解根IoC容器時的配置刷新一致,用於構建相關Bean
configureAndRefreshWebApplicationContext(cwac);
}
}
}
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
// 如果仍舊沒有查找到子IoC容器則創建一個子IoC容器
wac = findWebApplicationContext();
}
if (wac == null) {
// No context instance is defined for this servlet -> create a local one
wac = createWebApplicationContext(rootContext);
}
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方法完成「可變」的初始化過程
onRefresh(wac);
}
}
if (this.publishContext) {
// Publish the context as a servlet context attribute.
String attrName = getServletContextAttributeName();
getServletContext().setAttribute(attrName, wac);
}
return wac;
}
通過函數名不難發現,該方法的主要作用同樣是創建一個WebApplicationContext對象,即IOC容器,不過這裡創建的是spring mvc容器,是前面ContextLoadListener創建的IOC容器的子容器。
為什麼需要多個IOC容器?還是父子容器?這就要說明下父子IOC容器的訪問特性了。
父子容器類似於類的繼承關係,子類可以訪問父類中的成員變量,而父類不可訪問子類的成員變量,同樣的,子容器可以訪問父容器中定義的Bean,但父容器無法訪問子容器定義的Bean。
創建多個容器的目的是,父IOC容器做為全局共享的IOC容器,存放Web應用共享的Bean,比如Service,DAO。而子IOC容器根據需求的不同,放入不同的Bean,比如Conroller,這樣能夠做到隔離,保證系統的安全性。
如果你看過sprin cloud netflix系列源碼,比如spring-cloud-netflix-ribbon,spring-cloud-openfeign,就會發現它裡邊創建了很多父子容器,作用與這裡是一樣的,容器與容器相互隔離,保證系統的安全性。
我們繼續講解DispatcherServlet類的子IOC容器創建過程,如果當前Servlet存在一個IOC容器則為其設置根IOC容器作為其父類,並刷新該容器,用於初始化定義的Bean。
這裡的方法與前文講述的根IOC容器類似,同樣會讀取用戶在web.xml中配置的<servlet>中的<init-param>值,用於查找相關的xml配置文件來創建定義的Bean。如果當前Servlet不存在一個子IoC容器就去查找一個,如果沒有查找到,則調用createWebApplicationContext()方法去創建一個。
圖10 createWebApplicationContext源碼
該方法用於創建一個子IOC容器並將根IOC容器做為其父容器,接著執行配置和刷新操作構建相關的Bean。
至此,根IOC容器以及相關Servlet的子IOC容器已經初始化完成了,子容器中管理的Bean一般只被該Servlet使用,比如SpringMVC中需要的各種重要組件,包括Controller、Interceptor、Converter、ExceptionResolver等。
spring mvc父子容器之間的關係,可以用下圖來描述:
圖11 spring mvc父子容器
當IOC子容器構造完成後調用了onRefresh()方法,其實現是在子類DispatcherServlet中,查看DispatcherServletBean類的onRefresh()方法源碼如下:
圖12 onRefresh方法
onRefresh()方法調用了initStrategies()方法,通過函數名可以判斷,該方法主要初始化創建multipartResovle來支持圖片等文件的上傳、本地化解析器、主題解析器、HandlerMapping處理器映射器、HandlerAdapter處理器適配器、異常解析器、視圖解析器、flashMap管理器等。這些組件都是SpringMVC開發中的重要組件,相關組件的初始化創建過程均在此完成,所以在這裡最終完成了spring mvc組件的初始化。
至此,整個spring mvc應用啟動的原理以及過程就講完了,總的來說,spring以及spring mvc的初始化過程是跟隨在tomcat的Listener、Filter、Servlet的初始化過程完成的。
6、總結
spring以及spring mvc應用初始化過程還是比較清晰明了的,本文做了完整的分析記錄。spring mvc也支持無web.xml配置文件的方式開發web應用,其原理是類似的,只不過是用代碼的方式來創建了web.xml中需要的組件。
後續會分析spring boot應用的啟動以及初始化過程分析,spring boot是在spring及spring mvc的基礎上做了更深的封裝,所以看起來也更複雜。spring boot應用既可以以war包的方式運行在tomcat上,也可以以jar的方式運行再內嵌的tomcat上,兩種方式啟動的源碼有所不同,還需要分別分析。