今天小編嘗試從源碼層面上對spring mvc的初始化過程進行分析,一起揭開spring mvc的真實面紗,也許我們都已經學會使用spring mvc,或者說對spring mvc的原理在理論上已經能倒背如流。在開始之前,這可能需要你掌握java ee的一些基本知識,比如說我們要先學會java ee 的servlet技術規范,因為spring mvc框架實現,底層是遵循servlet規范的。
在開始源碼分析之前,我們可能需要一個簡單的案例工程,不慌,小編已經安排好了:
樣例工程下載地址 : https://github.com/smallercoder/spring-mvc-test
那下面就讓我們開始吧!
一、前置知識
大家都知道,我們在使用spring mvc時通常會在 web.xml
文件中做如下配置:
web.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
<?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> |
上面的配置總結起來有幾點內容,分別是: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
文件中的 <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
的源碼是怎樣的。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
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()
方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
// 創建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
文件加載配置的,該文件的內容為:
具體實現為:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
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等)
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持服務器之家。
原文鏈接:https://juejin.im/post/5b207dc86fb9a01e49294f42