今天小編嘗試從原始碼層面上對Spring mvc的初始化過程進行分析,一起揭開Spring mvc的真實面紗,也許我們都已經學會使用spring mvc,或者說對spring mvc的原理在理論上已經能倒背如流。在開始之前,這可能需要你掌握Java EE的一些基本知識,比如說我們要先學會Java EE 的Servlet技術規範,因為Spring mvc框架實現,底層是遵循Servlet規範的。
在開始原始碼分析之前,我們可能需要一個簡單的案例工程,不慌,小編已經安排好了:
樣例工程下載地址 : github.com/SmallerCode…
那下面就讓我們開始吧!
一、前置知識
大家都知道,我們在使用spring mvc時通常會在web.xml
檔案中做如下配置:
web.xml
<?xml version="1.0" encoding="UTF-8"?>
<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>contextConfigLocation</param-name>
<param-value>
classpath:applicationContext.xml
</param-value>
</context-param>
<!-- 監聽器配置 -->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!-- 前端控制器配置 -->
<servlet>
<servlet-name>dispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:applicationContext-mvc.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>dispatcher</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
複製程式碼
上面的配置總結起來有幾點內容,分別是:
- 1、配置Spring Web上下文監聽器,該監聽器同時是Spring mvc啟動的入口,至於為什麼,後面第二節將會講到
- 2、前端控制器
DispatcherServlet
,該控制器是Spring mvc處理各種請求的入口及處理器
當我們將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
檔案中的<load-on-startup>1</load-on-startup>
給去掉,那麼預設tomcat啟動時只會初始化spring web上下文,也就是說只會載入到applicationContext.xml
這個檔案,對於applicationContext-mvc.xml
這個配置檔案是載入不到的,<load-on-startup>1</load-on-startup>
的意思就是讓DispatcherServlet
延遲到使用的時候(也就是處理請求的時候
)再做初始化。
我們已經知道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容器的基礎上建立的,我們在<servlet>
標籤中配置的初始化引數被加入到新容器中去。
至此,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等)
感謝你的閱讀,期待與你共同進步,歡迎下方發表評論~~~