阿里一面:Spring和SpringMvc父子容器你能說清楚嗎

java金融發表於2021-04-02

以前寫了幾篇關於SpringBoot的文章《面試高頻題:springBoot自動裝配的原理你能說出來嗎》《保姆級教程,手把手教你實現一個SpringBoot的starter》,這幾天突然有個讀者問:能說一說Spring的父子容器嗎?說實話這其實也是Spring八股文裡面一個比較常見的問題。在我的印象裡面Spring就是父容器,SpringMvc就是子容器,子容器可以訪問父容器的內容,父容器不能訪問子容器的東西。有點類似java裡面的繼承的味道,子類可以繼承父類共有方法和變數,可以訪問它們,父類不可以訪問子類的方法和變數。在這裡就會衍生出幾個比較經典的問題:

  • 為什麼需要父子容器?
  • 是否可以把所有類都透過Spring容器來管理?(SpringapplicationContext.xml中配置全域性掃描)
  • 是否可以把我們所需的類都放入Spring-mvc子容器裡面來管理(springmvcspring-servlet.xml中配置全域性掃描)?
  • 同時透過兩個容器同時來管理所有的類?如果能夠把上面這四個問題可以說個所以然來,個人覺得Spring的父子容器應該問題不大了。我們可以看下官網提供的父子容器的圖片上圖中顯示了2個WebApplicationContext例項,為了進行區分,分別稱之為:Servlet WebApplicationContext(子容器)、Root WebApplicationContext(父容器)。
  • Servlet WebApplicationContext:這是對J2EE三層架構中的web層進行配置,如控制器(controller)、檢視解析器(view resolvers)等相關的bean。透過spring mvc中提供的DispatchServlet來載入配置,通常情況下,配置檔案的名稱為spring-servlet.xml。
  • Root WebApplicationContext:這是對J2EE三層架構中的service層、dao層進行配置,如業務bean,資料來源(DataSource)等。通常情況下,配置檔案的名稱為applicationContext.xml。在web應用中,其一般透過ContextLoaderListener來載入。

Spring的啟動

要想很好的理解它們之間的關係,我們就有必要先弄清楚Spring的啟動流程。要弄清楚這個啟動流程我們就需要搭建一個SpringMvc專案,說句實話,用慣了SpringBooot開箱即用,突然在回過頭來搭建一個SpringMvc專案還真有點不習慣,一大堆的配置檔案。(雖然也可以用註解來實現)具體怎麼搭建SpringMvc專案這個就不介紹了,搭建好專案我們執行起來可以看到控制檯會輸出如下日誌:日誌裡面分別列印出了父容器和子容器分別的一個耗時。

如何驗證是有兩個容器?

我們只需要Controller與我們的Service中實現ApplicationContextAware介面,就可以得知對應的管理容器:在Service所屬的父容器裡面我們可以看到父容器對應的物件是XmlWebApplicationContext@3972Controller中對應的容器物件是XmlWebApplicationContext@4114由此可見它們是兩個不同的容器。

原始碼分析

我們知道SpringServletContainerInitializerservlet 3.0 開始,Tomcat 啟動時會自動載入實現了 ServletContainerInitializer
介面的類(需要在 META-INF/services 目錄下新建配置檔案)也稱為 SPI(Service Provider Interface) 機制,SPI的應用還是挺廣的比如我們的JDBC、還有Dubbo框架裡面都有用到,如果還有不是很瞭解SPI機制的 可以去學習下。所以我們的入口就是SpringServletContainerInitializeronStartup方法,這也應該是web容器啟動呼叫Spring相關的第一個方法。

初始化SpringIoc

如果實在找不到入口的話,我們可以 根據控制檯列印的日誌,然後拿著日誌進行反向查詢這應該總能找到開始載入父容器的地方。啟動的時候控制檯應該會列印出“Root WebApplicationContext: initialization started” 我們拿著這個日誌就能定位到程式碼了

`public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {`
 `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.`
 `if (this.context == null) {`
 `// 透過反射去建立context` 
 `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.`
 `ApplicationContext parent = loadParentContext(servletContext);`
 `cwac.setParent(parent);`
 `}`
 `// IOC容器初始化`
 `configureAndRefreshWebApplicationContext(cwac, servletContext);`
 `}`
 `}`
 `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;`
 `}`
 `}`

這段程式碼就是建立父容器的地方。

初始化 Spring MVC

接著我們再來看看建立子容器的地方:在FrameworkServlet上述程式碼是不是會有個疑問我們怎麼就會執行FrameworkServletinitServletBean方法。這是由於我們在web.xml 裡面配置了DispatcherServlet,然後web容器就會去呼叫DispatcherServletinit方法,並且這個方法只會被執行一次。透過init方法就會去執行到initWebApplicationContext這個方法了,這就是web子容器的一個啟動執行順序。

`<servlet>`
 `<servlet-name>dispatcher</servlet-name>`
 `<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>`
 `// 如果不配置這個load-on-startup 1 不會再專案啟動的時候執行inti方法。而是首次訪問再啟動`
 `<load-on-startup>1</load-on-startup>`
 `</servlet>`

大概流程如下:從上述程式碼我們可以發現子容器是自己重新透過反射new了一個新的容器作為子容器, 並且設定自己的父容器為Spring 初始化建立的WebApplicationContext。然後就是去載入我們在web.xml 裡面配置的Springmvc 的配置檔案,然後透過建立的子容器去執行refresh方法,這個方法我相信很多人應該都比較清楚了。

問題解答

我們知道了Sping父容器以及SpingMvc子容器的一個啟動過程,以及每個容器都分別幹了什麼事情現在再回過頭來看看上述四個問題。

  • 為什麼需要父子容器?父子容器的主要作用應該是劃分框架邊界。有點單一職責的味道。在J2EE三層架構中,在service層我們一般使用spring框架來管理, 而在web層則有多種選擇,如spring mvc、struts等。因此,通常對於web層我們會使用單獨的配置檔案。例如在上面的案例中,一開始我們使用spring-servlet.xml來配置web層,使用applicationContext.xml來配置servicedao層。如果現在我們想把web層從spring mvc替換成struts,那麼只需要將spring-servlet.xml替換成Struts的配置檔案struts.xml即可,而applicationContext.xml不需要改變。
  • 是否可以把所有類都透過Spring父容器來管理?(Spring的applicationContext.xml中配置全域性掃描)所有的類都透過父容器來管理的配置就是如下:
`<context:component-scan  use-default-filters="false"  base-package="cn.javajr">`
 `<context:include-filter type="annotation" expression="org.springframework.stereotype.Service" />`
 `<context:include-filter type="annotation" expression="org.springframework.stereotype.Component" />`
 `<context:include-filter type="annotation" expression="org.springframework.stereotype.Repository" />`
 `<context:include-filter type="annotation" expression="org.springframework.stereotype.Controller" />`
 `</context:component-scan>`

然後在SpringMvc的配置裡面不配置掃描包路徑。很顯然這種方式是行不通的,這樣會導致我們請求介面的時候產生404。因為在解析@ReqestMapping註解的過程中initHandlerMethods()函式只是對Spring MVC 容器中的bean進行處理的,並沒有去查詢父容器的bean, 因此不會對父容器中含有@RequestMapping註解的函式進行處理,更不會生成相應的handler。所以當請求過來時找不到處理的handler,導致404。

  • 是否可以把我們所需的類都放入Spring-mvc子容器裡面來管理(springmvc的spring-servlet.xml中配置全域性掃描)?這個是把包的掃描配置spring-servlet.xml中這個是可行的。為什麼可行因為無非就是把所有的東西全部交給子容器來管理了,子容器執行了refresh方法,把在它的配置檔案裡面的東西全部載入管理起來來了。雖然可以這麼做不過一般應該是不推薦這麼去做的,一般人也不會這麼幹的。如果你的專案裡有用到事物、或者aop記得也需要把這部分配置需要放到Spring-mvc子容器的配置檔案來,不然一部分內容在子容器和一部分內容在父容器,可能就會導致你的事物或者AOP不生效。(這裡不就有個經典的八股文嗎?你有遇到事物不起作用的時候,其實這也是一種情況)
  • 同時透過兩個容器同時來管理所有的類?這個問題應該是比較好回答了,肯定不會透過這種方式來的,先不說會不會引發其他問題,首先兩個容器裡面都放一份一樣的物件,造成了記憶體浪費。再者的話子容器會覆蓋父容器載入,本來可能父容器配置了事物生成的是代理物件,但是被子容器一覆蓋,又成了原生物件。這就導致了你的事物不起作用了。在補充一個問題:SpringBoot 裡面是否還有父子容器?我們下篇再見!

總結

  • 其實父子容器對於程式設計師來說是無感的,是一個並沒有什麼用的知識點,都是Spring幫我們處理了,但是我們還是需要知道有這麼個東西,不然我們有可能遇到問題的時候可能不知道如何下手。比如為啥我這個事物不起作用了,我這個aop怎麼也不行了,網上都是這麼配置的。

結束

  • 由於自己才疏學淺,難免會有紕漏,假如你發現了錯誤的地方,還望留言給我指出來,我會對其加以修正。
  • 如果你覺得文章還不錯,你的轉發、分享、讚賞、點贊、留言就是對我最大的鼓勵。
  • 感謝您的閱讀,十分歡迎並感謝您的關注。

往期精選

*推薦? :Java高併發程式設計基礎三大利器之CyclicBarrier*

推薦? :Java高併發程式設計基礎三大利器之CountDownLatch

推薦? :Java高併發程式設計基礎三大利器之Semaphore

推薦? :Java高併發程式設計基礎之AQS

推薦? :可惡的爬蟲直接把生產6臺機器爬掛了!

相關文章