1. 程式人生 > >面試高頻題:說一說對Spring和SpringMvc父子容器的理解?

面試高頻題:說一說對Spring和SpringMvc父子容器的理解?

# 引言 以前寫了幾篇關於`SpringBoot`的文章[《面試高頻題:springBoot自動裝配的原理你能說出來嗎》](https://mp.weixin.qq.com/s/TnofWzuaH-WDcfYVfp-UoA)、[《保姆級教程,手把手教你實現一個SpringBoot的starter》](https://mp.weixin.qq.com/s/_BDYap83dvg-kxca1_EiPQ),這幾天突然有個讀者問:能說一說`Spring`的父子容器嗎?說實話這其實也是`Spring`八股文裡面一個比較常見的問題。在我的印象裡面`Spring`就是父容器,`SpringMvc`就是子容器,子容器可以訪問父容器的內容,父容器不能訪問子容器的東西。有點類似java裡面的繼承的味道,子類可以繼承父類共有方法和變數,可以訪問它們,父類不可以訪問子類的方法和變數。在這裡就會衍生出幾個比較經典的問題: - 為什麼需要父子容器? - 是否可以把所有類都通過`Spring`容器來管理?(`Spring`的`applicationContext.xml`中配置全域性掃描) - 是否可以把我們所需的類都放入`Spring-mvc`子容器裡面來管理(`springmvc`的`spring-servlet.xml`中配置全域性掃描)? - 同時通過兩個容器同時來管理所有的類? 如果能夠把上面這四個問題可以說個所以然來,個人覺得`Spring`的父子容器應該問題不大了。 我們可以看下官網提供的父子容器的圖片 ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20210328224743508.png) 上圖中顯示了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`專案這個就不介紹了,搭建好專案我們執行起來可以看到控制檯會輸出如下日誌: ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20210331141312320.png) 日誌裡面分別打印出了父容器和子容器分別的一個耗時。 # 如何驗證是有兩個容器? 我們只需要`Controller`與我們的`Service`中實現`ApplicationContextAware`介面,就可已得知對應的管理容器: 在`Service`所屬的父容器裡面我們可以看到父容器對應的物件是`XmlWebApplicationContext@3972` ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20210331161637229.png) 在`Controller`中對應的容器物件是`XmlWebApplicationContext@4114` ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20210331162046445.png) 由此可見它們是兩個不同的容器。 # 原始碼分析 我們知道`SpringServletContainerInitializer`從 `servlet 3.0` 開始,`Tomcat` 啟動時會自動載入實現了 `ServletContainerInitializer` 介面的類(需要在 `META-INF/services` 目錄下新建配置檔案)也稱為 `SPI(Service Provider Interface)` 機制,`SPI`的應用還是挺廣的比如我們的`JDBC`、還有`Dubbo`框架裡面都有用到,如果還有不是很瞭解`SPI`機制的 可以去學習下。所以我們的入口就是`SpringServletContainerInitializer`的`onStartup`方法,這也應該是web容器啟動呼叫`Spring`相關的第一個方法。 ### 初始化SpringIoc 如果實在找不到入口的話,我們可以 根據控制檯列印的日誌,然後拿著日誌進行反向查詢這應該總能找到開始載入父容器的地方。啟動的時候控制檯應該會打印出“`Root WebApplicationContext: initialization started`” 我們拿著這個日誌就能定位到程式碼了 ```java 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`類 ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20210331232022355.png) 上述程式碼是不是會有個疑問我們怎麼就會執行`FrameworkServlet`的`initServletBean`方法。 這是由於我們在`web.xml` 裡面配置了`DispatcherServlet`,然後web容器就會去呼叫`DispatcherServlet`的`init`方法,並且這個方法只會被執行一次。通過init方法就會去執行到`initWebApplicationContext`這個方法了,這就是web子容器的一個啟動執行順序。 ```java ``` 大概流程如下: ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20210401115544969.png) 從上述程式碼我們可以發現子容器是自己重新通過反射`new`了一個新的容器作為子容器, 並且設定自己的父容器為`Spring` 初始化建立的`WebApplicationContext`。然後就是去載入我們在`web.xml` 裡面配置的`Springmvc` 的配置檔案,然後通過建立的子容器去執行`refresh`方法,這個方法我相信很多人應該都比較清楚了。 # 問題解答 我們知道了`Sping`父容器以及`SpingMvc`子容器的一個啟動過程,以及每個容器都分別幹了什麼事情現在再回過頭來看看上述四個問題。 - **為什麼需要父子容器?** 父子容器的主要作用應該是劃分框架邊界。有點單一職責的味道。 在`J2EE`三層架構中,在`service`層我們一般使用`spring`框架來管理, 而在`web`層則有多種選擇,如`spring mvc、struts`等。因此,通常對於`web`層我們會使用單獨的配置檔案。例如在上面的案例中,一開始我們使用`spring-servlet.xml`來配置web層,使用applicationContext.xml來配置`service`、`dao`層。如果現在我們想把`web`層從`spring mvc`替換成`struts`,那麼只需要將`spring-servlet.xml`替換成`Struts`的配置檔案`struts.xml`即可,而`applicationContext.xml`不需要改變。 - **是否可以把所有類都通過Spring父容器來管理?(Spring的applicationContext.xml中配置全域性掃描)** 所有的類都通過父容器來管理的配置就是如下: ``` ``` 然後在`SpringMvc`的配置裡面不配置掃描包路勁。很顯然這種方式是行不通的,這樣會導致我們請求介面的時候產生`404`。因為在解析@ReqestMapping註解的過程中`initHandlerMethods`()函式只是對`Spring MVC` 容器中的`bean`進行處理的,並沒有去查詢父容器的`bean`, 因此不會對父容器中含有`@RequestMapping`註解的函式進行處理,更不會生成相應的`handler`。所以當請求過來時找不到處理的`handler`,導致404。 ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20210401142712993.png) - **是否可以把我們所需的類都放入Spring-mvc子容器裡面來管理(springmvc的spring-servlet.xml中配置全域性掃描)?** 這個是把包的掃描配置`spring-servlet.xml`中這個是可行的。為什麼可行因為無非就是把所有的東西全部交給子容器來管理了,子容器執行了`refresh`方法,把在它的配置檔案裡面的東西全部載入管理起來來了。雖然可以這麼做不過一般應該是不推薦這麼去做的,一般人也不會這麼幹的。**如果你的專案裡有用到事物、或者aop記得也需要把這部分配置需要放到Spring-mvc子容器的配置檔案來,不然一部分內容在子容器和一部分內容在父容器,可能就會導致你的事物或者AOP不生效**。(這裡不就有個經典的八股文嗎?**你有遇到事物不起作用的時候**,其實這也是一種情況) - **同時通過兩個容器同時來管理所有的類?** 這個問題應該是比較好回答了,肯定不會通過這種方式來的,先不說會不會引發其他問題,首先兩個容器裡面都放一份一樣的物件,造成了記憶體浪費。再者的話子容器會覆蓋父容器載入,本來可能父容器配置了事物生成的是代理物件,但是被子容器一覆蓋,又成了原生物件。這就導致了你的事物不起作用了。 在補充一個問題:**SpringBoot 裡面是否還有父子容器?**我們下篇再見! # 總結 - 其實父子容器對於程式設計師來說是無感的,是一個並沒有什麼用的知識點,都是`Spring`幫我們處理了,但是我們還是需要知道有這麼個東西,不然我們有可能遇到問題的時候可能不知道如何下手。比如為啥我這個事物不起作用了,我這個`aop`怎麼也不行了,網上都是這麼配置的。 ### 結束 - 由於自己才疏學淺,難免會有紕漏,假如你發現了錯誤的地方,還望留言給我指出來,我會對其加以修正。 - 如果你覺得文章還不錯,你的轉發、分享、讚賞、點贊、留言就是對我最大的鼓勵。 - 感謝您的閱讀,十分歡迎並感謝您的關注。 站在巨人的肩膀上摘蘋果: https://www.cnblogs.com/grasp/p/11042580.html