從源碼層面上對Spring mvc的初始化過程進行分析,一起揭開Spring mvc的真實面紗,也許我們都已經學會使用spring mvc,或者說對spring mvc的原理在理論上已經能倒背如流。在開始之前,這可能需要你掌握Java EE的一些基本知識,比如說我們要先學會Java EE 的Servlet技術規範,因為Spring mvc框架實現,底層是遵循Servlet規範的。
一、前置知識
大家都知道,我們在使用spring mvc時通常會在 web.xml 文件中做如下配置:
web.xml
<web-app version="3.0" xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">
<context-param>
<param-name>contextConfigLocationparam-name >
<param-value>
classpath:applicationContext.xml
param-value>
context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListenerlistener-class>
listener>
<servlet>
<servlet-name>dispatcherservlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet
servlet-class><init-param>
<param-name>contextConfigLocationparam-name>
<param-value>classpath:applicationContext-mvc.xmlparam-value>
init-param>
<load-on-startup>1load-on-startup>
servlet>
<servlet-mapping>
<servlet-name>dispatcherservlet-name>
<url-pattern>/url-pattern>
servlet-mapping>
web-app>
上面的配置總結起來有幾點內容,分別是:
DispatcherServlet
當我們將spring mvc應用部署到tomcat時,當你不配置任何的 context-param 和 listener參數,只配置一個 DispatcherServlet 時,那麼tomcat在啟動的時候是不會初始化spring web上下文的,換句話說,tomcat是不會初始化spring框架的,因為你並沒有告訴它們spring的配置文件放在什麼地方,以及怎麼去加載。所以 listener 監聽器幫了我們這個忙,那麼為什麼配置監聽器之後就可以告訴tomcat怎麼去加載呢?因為 listener 是實現了servlet技術規範的監聽器組件,tomcat在啟動時會先加載 web.xml 中是否有servlet監聽器存在,有則啟動它們。 ContextLoaderListener 是spring框架對servlet監聽器的一個封裝,本質上還是一個servlet監聽器,所以會被執行,但由於 ContextLoaderListener 源碼中是基於 contextConfigLocation 和 contextClass 兩個配置參數去加載相應配置的,因此就有了我們配置的 context-param 參數了, servlet 標籤裡的初始化參數也是同樣的道理,即告訴web服務器在啟動的同時把spring web上下文( WebApplicationContext )也給初始化了。
上面講了下tomcat加載spring mvc應用的大致流程,接下來將從源碼入手分析啟動原理。
二、Spring MVC web 上下文啟動源碼分析
假設現在我們把上面 web.xml 文件中的
我們已經知道spring web是基於 servlet 標準去封裝的,那麼很明顯,servlet怎麼初始化, WebApplicationContext web上下文就應該怎麼初始化。我們先看看 ContextLoaderListener 的源碼是怎樣的。
public class ContextLoaderListener extends ContextLoader implements ServletContextListener {
// 初始化方法
@Override
public void contextInitialized(ServletContextEvent event) {
initWebApplicationContext(event.getServletContext());
}
// 銷燬方法
@Override
public void contextDestroyed(ServletContextEvent event) {
closeWebApplicationContext(event.getServletContext());
ContextCleanupListener.cleanupAttributes(event.getServletContext());
}
}
ContextLoaderListener 類實現了 ServletContextListener ,本質上是一個servlet監聽器,tomcat將會優先加載servlet監聽器組件,並調用 contextInitialized 方法,在 contextInitialized 方法中調用 initWebApplicationContext 方法初始化Spring web上下文,看到這煥然大悟,原來Spring mvc的入口就在這裡,哈哈~~~趕緊跟進去 initWebApplicationContext 方法看看吧!
initWebApplicationContext() 方法:
// 創建web上下文,默認是XmlWebApplicationContext
if (this.context == null) {
this.context = createWebApplicationContext(servletContext);
}
if (
this.context instanceof ConfigurableWebApplicationContext) {ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) this.context;
// 如果該容器還沒有刷新過
if (!cwac.isActive()) {
if (cwac.getParent() == null) {
ApplicationContext parent = loadParentContext(servletContext);
cwac.setParent(parent);
}
// 配置並刷新容器
configureAndRefreshWebApplicationContext(cwac, servletContext);
}
}
上面的方法只做了兩件事:
- 1、如果spring web容器還沒有創建,那麼就創建一個全新的spring web容器,並且該容器為root根容器,下面第三節講到的servlet spring web容器是在此根容器上創建起來的
- 2、配置並刷新容器
上面代碼註釋說到默認創建的上下文容器是 XmlWebApplicationContext ,為什麼不是其他web上下文呢?為啥不是下面上下文的任何一種呢?
我們可以跟進去 createWebApplicationContext 後就可以發現默認是從一個叫 ContextLoader.properties 文件加載配置的,該文件的內容為:
org.springframework.web.context.WebApplicationContext=org.springframework.web.context.support.XmlWebApplicationContext
具體實現為:
protected Class> determineContextClass(ServletContext servletContext) {
// 自定義上下文,否則就默認創建XmlWebApplicationContext
String contextClassName = servletContext.getInitParameter(CONTEXT_CLASS_PARAM);
if (contextClassName != null) {
try {
return ClassUtils.forName(contextClassName, ClassUtils.getDefaultClassLoader());
}
catch (ClassNotFoundException ex) {
throw new ApplicationContextException(
"Failed to load custom context class [" + contextClassName + "]", ex);
}
}
else {
// 從屬性文件中加載類名,也就是org.springframework.web.context.support.XmlWebApplicationContext
contextClassName = defaultStrategies.getProperty(WebApplicationContext.class.getName());
try {
return ClassUtils.forName(contextClassName, ContextLoader.class.getClassLoader());
}
catch (ClassNotFoundException ex) {
throw new ApplicationContextException(
"Failed to load default context class [" + contextClassName + "]", ex);
}
}
}
上面可以看出其實我們也可以自定義spring web的上下文的,那麼怎麼去指定我們自定義的上下文呢?答案是通過在 web.xml 中指定 contextClass 參數,因此第一小結結尾時說 contextClass 參數和 contextConfigLocation 很重要~~至於 contextConfigLocation 參數,我們跟進 configureAndRefreshWebApplicationContext 即可看到,如下圖:
總結:
spring mvc啟動流程大致就是從一個叫 ContextLoaderListener 開始的,它是一個servlet監聽器,能夠被web容器發現並加載,初始化監聽器 ContextLoaderListener 之後,接著就是根據配置如 contextConfigLocation 和 contextClass 創建web容器了,如果你不指定 contextClass 參數值,則默認創建的spring web容器類型為 XmlWebApplicationContext ,最後一步就是根據你配置的 contextConfigLocation 文件路徑去配置並刷新容器了。
三、 DispatcherServlet 控制器的初始化
好了,上面我們簡單地分析了Spring mvc容器初始化的源碼,我們永遠不會忘記,我們默認創建的容器類型為 XmlWebApplicationContext ,當然我們也不會忘記,在 web.xml 中,我們還有一個重要的配置,那就是 DispatcherServlet 。下面我們就來分析下 DispatcherServlet 的初始化過程。
DispatcherServlet ,就是一個servlet,一個用來處理request請求的servlet,它是spring mvc的核心,所有的請求都經過它,並由它指定後續操作該怎麼執行,咋一看像一扇門,因此我管它叫“閘門”。在我們繼續之前,我們應該共同遵守一個常識,那就是-------無論是監聽器還是servlet,都是servlet規範組件,web服務器都可以發現並加載它們。
下面我們先看看 DispatcherServlet 的繼承關係:
看到這我們是不是一目瞭然了, DispatcherServlet 繼承了 HttpServlet 這個類, HttpServlet 是servlet技術規範中專門用於處理http請求的servlet,這就不難解釋為什麼spring mvc會將 DispatcherServlet 作為統一請求入口了。
因為一個servlet的生命週期是 init() -> service() -> destory() ,那麼 DispatcherServlet 怎麼初始化呢?看上面的繼承圖,我們進到 HttpServletBean 去看看。
果不其然, HttpServletBean 類中有一個 init() 方法, HttpServletBean 是一個抽象類, init() 方法如下:
可以看出方法採用 final 修飾,因為 final 修飾的方法是不能被子類繼承的,也就是子類沒有同樣的 init() 方法了,這個 init 方法就是 DispatcherServlet 的初始化入口了。
接著我們跟進 FrameworkServlet 的 initServletBean() 方法:
在方法中將會初始化不同於第一小節的web容器,請記住,這個新的spring web 容器是專門為 dispactherServlet 服務的,而且這個新容器是在第一小節根ROOT容器的基礎上創建的,我們在
至此, DispatcherSevlet 的初始化完成了,聽著有點矇蔽,但其實也是這樣,上面的分析僅僅只圍繞一個方法,它叫 init() ,所有的servlet初始化都將調用該方法。
總結:
dispactherServlet 的初始化做了兩件事情,第一件事情就是根據根web容器,也就是我們第一小節創建的 XmlWebApplicationContext ,然後創建一個專門為 dispactherServlet服務的web容器,第二件事情就是將你在web.xml文件中對 dispactherServlet 進行的相關配置加載到新容器當中。
三、每個request調用請求經歷了哪些過程
其實說到這才是 dispatcherServlet 控制器的核心所在,因為web框架無非就是接受請求,處理請求,然後響應請求。當然了,如果 dispactherServlet 只是單純地接受處理然後響應請求,那未免太弱了,因此spring設計者加入了許許多多的新特性,比如說攔截器、消息轉換器、請求處理映射器以及各種各樣的 Resolver ,因此spring mvc非常強大。
dispatcherServlet 類不做相關源碼分析,因為它就是一個固定的執行步驟,什麼意思呢?一個request進來,大致就經歷這樣的過程:
接受請求 -----> 是否有各種各樣的處理器 Handler -------> 是否有消息轉換器 HandlerAdapter --------> 響應請求
上面每一步如果存在相應的組件,當然前提是你在項目中有做相關的配置,則會執行你配置的組件,最後響應請求。因此明白大致的流程之後,如果你想調試一個request,那麼你完全可以在 dispatcherServlet 類的 doDispatch 方法中打個斷點,跟完代碼之後你就會發現其實大致流程就差不多了。
四、後話
本文的工程是基於傳統的web.xml加載web項目,當然在spring mvc中我們也可以完全基於註解的方式進行配置,我們可以通過實現 WebApplicationInitializer 來創建自己的web啟動器,也可以通過繼承 AbstractAnnotationConfigDispatcherServletInitializer 來創建相應的spring web容器(包括上面說到的根容器和servlet web容器),最後通過繼承 WebMvcConfigurationSupport 再一步進行自定義配置(相關攔截器,bean等)
感謝你的閱讀,期待與你共同進步,歡迎下方發表評論~~~
閱讀更多 Java高級架構技術 的文章