面試高頻題:Spring和SpringMvc父子容器你能說(shuō)清楚嗎
點(diǎn)擊上方“Java金融”,選擇“設(shè)為星標(biāo)”
后臺(tái)回復(fù)"888"獲取bat面試題集
引言
以前寫了幾篇關(guān)于SpringBoot的文章《面試高頻題:springBoot自動(dòng)裝配的原理你能說(shuō)出來(lái)嗎》、《保姆級(jí)教程,手把手教你實(shí)現(xiàn)一個(gè)SpringBoot的starter》,這幾天突然有個(gè)讀者問(wèn):能說(shuō)一說(shuō)Spring的父子容器嗎?說(shuō)實(shí)話這其實(shí)也是Spring八股文里面一個(gè)比較常見的問(wèn)題。在我的印象里面Spring就是父容器,SpringMvc就是子容器,子容器可以訪問(wèn)父容器的內(nèi)容,父容器不能訪問(wèn)子容器的東西。有點(diǎn)類似java里面的繼承的味道,子類可以繼承父類共有方法和變量,可以訪問(wèn)它們,父類不可以訪問(wèn)子類的方法和變量。在這里就會(huì)衍生出幾個(gè)比較經(jīng)典的問(wèn)題:
為什么需要父子容器? 是否可以把所有類都通過(guò) Spring容器來(lái)管理?(Spring的applicationContext.xml中配置全局掃描)是否可以把我們所需的類都放入 Spring-mvc子容器里面來(lái)管理(springmvc的spring-servlet.xml中配置全局掃描)?同時(shí)通過(guò)兩個(gè)容器同時(shí)來(lái)管理所有的類?如果能夠把上面這四個(gè)問(wèn)題可以說(shuō)個(gè)所以然來(lái),個(gè)人覺(jué)得 Spring的父子容器應(yīng)該問(wèn)題不大了。我們可以看下官網(wǎng)提供的父子容器的圖片
上圖中顯示了2個(gè)WebApplicationContext實(shí)例,為了進(jìn)行區(qū)分,分別稱之為:Servlet WebApplicationContext(子容器)、Root WebApplicationContext(父容器)。Servlet WebApplicationContext:這是對(duì)J2EE三層架構(gòu)中的 web層進(jìn)行配置,如控制器(controller)、視圖解析器(view resolvers)等相關(guān)的bean。通過(guò)spring mvc中提供的DispatchServlet來(lái)加載配置,通常情況下,配置文件的名稱為spring-servlet.xml。Root WebApplicationContext:這是對(duì)J2EE三層架構(gòu)中的 service層、dao層進(jìn)行配置,如業(yè)務(wù)bean,數(shù)據(jù)源(DataSource)等。通常情況下,配置文件的名稱為applicationContext.xml。在web應(yīng)用中,其一般通過(guò)ContextLoaderListener來(lái)加載。
Spring的啟動(dòng)
要想很好的理解它們之間的關(guān)系,我們就有必要先弄清楚Spring的啟動(dòng)流程。要弄清楚這個(gè)啟動(dòng)流程我們就需要搭建一個(gè)SpringMvc項(xiàng)目,說(shuō)句實(shí)話,用慣了SpringBooot開箱即用,突然在回過(guò)頭來(lái)搭建一個(gè)SpringMvc項(xiàng)目還真有點(diǎn)不習(xí)慣,一大堆的配置文件。(雖然也可以用注解來(lái)實(shí)現(xiàn))具體怎么搭建SpringMvc項(xiàng)目這個(gè)就不介紹了,搭建好項(xiàng)目我們運(yùn)行起來(lái)可以看到控制臺(tái)會(huì)輸出如下日志:
日志里面分別打印出了父容器和子容器分別的一個(gè)耗時(shí)。
如何驗(yàn)證是有兩個(gè)容器?
我們只需要Controller與我們的Service中實(shí)現(xiàn)ApplicationContextAware接口,就可以得知對(duì)應(yīng)的管理容器:在Service所屬的父容器里面我們可以看到父容器對(duì)應(yīng)的對(duì)象是XmlWebApplicationContext@3972
在Controller中對(duì)應(yīng)的容器對(duì)象是XmlWebApplicationContext@4114
由此可見它們是兩個(gè)不同的容器。
源碼分析
我們知道SpringServletContainerInitializer從 servlet 3.0 開始,Tomcat 啟動(dòng)時(shí)會(huì)自動(dòng)加載實(shí)現(xiàn)了 ServletContainerInitializer
接口的類(需要在 META-INF/services 目錄下新建配置文件)也稱為 SPI(Service Provider Interface) 機(jī)制,SPI的應(yīng)用還是挺廣的比如我們的JDBC、還有Dubbo框架里面都有用到,如果還有不是很了解SPI機(jī)制的
可以去學(xué)習(xí)下。所以我們的入口就是SpringServletContainerInitializer的onStartup方法,這也應(yīng)該是web容器啟動(dòng)調(diào)用Spring相關(guān)的第一個(gè)方法。
初始化SpringIoc
如果實(shí)在找不到入口的話,我們可以 根據(jù)控制臺(tái)打印的日志,然后拿著日志進(jìn)行反向查找這應(yīng)該總能找到開始加載父容器的地方。啟動(dòng)的時(shí)候控制臺(tái)應(yīng)該會(huì)打印出“Root WebApplicationContext: initialization started”
我們拿著這個(gè)日志就能定位到代碼了
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) {
// 通過(guò)反射去創(chuàng)建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;
}
}
這段代碼就是創(chuàng)建父容器的地方。
初始化 Spring MVC
接著我們?cè)賮?lái)看看創(chuàng)建子容器的地方:在FrameworkServlet類
上述代碼是不是會(huì)有個(gè)疑問(wèn)我們?cè)趺淳蜁?huì)執(zhí)行FrameworkServlet的initServletBean方法。這是由于我們?cè)?code style="font-size: 14px;overflow-wrap: break-word;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;color: rgb(30, 107, 184);background-color: rgba(27, 31, 35, 0.05);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;">web.xml 里面配置了DispatcherServlet,然后web容器就會(huì)去調(diào)用DispatcherServlet的init方法,并且這個(gè)方法只會(huì)被執(zhí)行一次。通過(guò)init方法就會(huì)去執(zhí)行到initWebApplicationContext這個(gè)方法了,這就是web子容器的一個(gè)啟動(dòng)執(zhí)行順序。
<servlet>
<servlet-name>dispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
// 如果不配置這個(gè)load-on-startup 1 不會(huì)再項(xiàng)目啟動(dòng)的時(shí)候執(zhí)行inti方法。而是首次訪問(wèn)再啟動(dòng)
<load-on-startup>1</load-on-startup>
</servlet>
大概流程如下:
從上述代碼我們可以發(fā)現(xiàn)子容器是自己重新通過(guò)反射new了一個(gè)新的容器作為子容器,
并且設(shè)置自己的父容器為Spring 初始化創(chuàng)建的WebApplicationContext。然后就是去加載我們?cè)?code style="font-size: 14px;overflow-wrap: break-word;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;color: rgb(30, 107, 184);background-color: rgba(27, 31, 35, 0.05);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;">web.xml 里面配置的Springmvc 的配置文件,然后通過(guò)創(chuàng)建的子容器去執(zhí)行refresh方法,這個(gè)方法我相信很多人應(yīng)該都比較清楚了。
問(wèn)題解答
我們知道了Sping父容器以及SpingMvc子容器的一個(gè)啟動(dòng)過(guò)程,以及每個(gè)容器都分別干了什么事情現(xiàn)在再回過(guò)頭來(lái)看看上述四個(gè)問(wèn)題。
為什么需要父子容器?父子容器的主要作用應(yīng)該是劃分框架邊界。有點(diǎn)單一職責(zé)的味道。在 J2EE三層架構(gòu)中,在service層我們一般使用spring框架來(lái)管理, 而在web層則有多種選擇,如spring mvc、struts等。因此,通常對(duì)于web層我們會(huì)使用單獨(dú)的配置文件。例如在上面的案例中,一開始我們使用spring-servlet.xml來(lái)配置web層,使用applicationContext.xml來(lái)配置service、dao層。如果現(xiàn)在我們想把web層從spring mvc替換成struts,那么只需要將spring-servlet.xml替換成Struts的配置文件struts.xml即可,而applicationContext.xml不需要改變。是否可以把所有類都通過(guò)Spring父容器來(lái)管理?(Spring的applicationContext.xml中配置全局掃描)所有的類都通過(guò)父容器來(lái)管理的配置就是如下:
<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的配置里面不配置掃描包路徑。很顯然這種方式是行不通的,這樣會(huì)導(dǎo)致我們請(qǐng)求接口的時(shí)候產(chǎn)生404。因?yàn)樵诮馕鯜ReqestMapping注解的過(guò)程中initHandlerMethods()函數(shù)只是對(duì)Spring MVC 容器中的bean進(jìn)行處理的,并沒(méi)有去查找父容器的bean, 因此不會(huì)對(duì)父容器中含有@RequestMapping注解的函數(shù)進(jìn)行處理,更不會(huì)生成相應(yīng)的handler。所以當(dāng)請(qǐng)求過(guò)來(lái)時(shí)找不到處理的handler,導(dǎo)致404。
是否可以把我們所需的類都放入Spring-mvc子容器里面來(lái)管理(springmvc的spring-servlet.xml中配置全局掃描)?這個(gè)是把包的掃描配置 spring-servlet.xml中這個(gè)是可行的。為什么可行因?yàn)闊o(wú)非就是把所有的東西全部交給子容器來(lái)管理了,子容器執(zhí)行了refresh方法,把在它的配置文件里面的東西全部加載管理起來(lái)來(lái)了。雖然可以這么做不過(guò)一般應(yīng)該是不推薦這么去做的,一般人也不會(huì)這么干的。如果你的項(xiàng)目里有用到事物、或者aop記得也需要把這部分配置需要放到Spring-mvc子容器的配置文件來(lái),不然一部分內(nèi)容在子容器和一部分內(nèi)容在父容器,可能就會(huì)導(dǎo)致你的事物或者AOP不生效。(這里不就有個(gè)經(jīng)典的八股文嗎?你有遇到事物不起作用的時(shí)候,其實(shí)這也是一種情況)同時(shí)通過(guò)兩個(gè)容器同時(shí)來(lái)管理所有的類?這個(gè)問(wèn)題應(yīng)該是比較好回答了,肯定不會(huì)通過(guò)這種方式來(lái)的,先不說(shuō)會(huì)不會(huì)引發(fā)其他問(wèn)題,首先兩個(gè)容器里面都放一份一樣的對(duì)象,造成了內(nèi)存浪費(fèi)。再者的話子容器會(huì)覆蓋父容器加載,本來(lái)可能父容器配置了事物生成的是代理對(duì)象,但是被子容器一覆蓋,又成了原生對(duì)象。這就導(dǎo)致了你的事物不起作用了。在補(bǔ)充一個(gè)問(wèn)題:SpringBoot 里面是否還有父子容器?我們下篇再見!
總結(jié)
其實(shí)父子容器對(duì)于程序員來(lái)說(shuō)是無(wú)感的,是一個(gè)并沒(méi)有什么用的知識(shí)點(diǎn),都是 Spring幫我們處理了,但是我們還是需要知道有這么個(gè)東西,不然我們有可能遇到問(wèn)題的時(shí)候可能不知道如何下手。比如為啥我這個(gè)事物不起作用了,我這個(gè)aop怎么也不行了,網(wǎng)上都是這么配置的。
結(jié)束
由于自己才疏學(xué)淺,難免會(huì)有紕漏,假如你發(fā)現(xiàn)了錯(cuò)誤的地方,還望留言給我指出來(lái),我會(huì)對(duì)其加以修正。 如果你覺(jué)得文章還不錯(cuò),你的轉(zhuǎn)發(fā)、分享、贊賞、點(diǎn)贊、留言就是對(duì)我最大的鼓勵(lì)。 感謝您的閱讀,十分歡迎并感謝您的關(guān)注。 站在巨人的肩膀上摘蘋果: https://www.cnblogs.com/grasp/p/11042580.html https://javajr.cn
往期精選
推薦?? :Java高并發(fā)編程基礎(chǔ)三大利器之CyclicBarrier
推薦?? :Java高并發(fā)編程基礎(chǔ)三大利器之CountDownLatch
推薦?? :Java高并發(fā)編程基礎(chǔ)三大利器之Semaphore
推薦?? :Java高并發(fā)編程基礎(chǔ)之AQS
推薦?? :可惡的爬蟲直接把生產(chǎn)6臺(tái)機(jī)器爬掛了!
最近面試BAT,整理一份面試資料《Java面試BATJ通關(guān)手冊(cè)》,覆蓋了Java核心技術(shù)、JVM、Java并發(fā)、SSM、微服務(wù)、數(shù)據(jù)庫(kù)、數(shù)據(jù)結(jié)構(gòu)、等等。獲取方式:點(diǎn)“在看”,關(guān)注公眾號(hào)并回復(fù) 666 領(lǐng)取,更多內(nèi)容陸續(xù)奉上。
文章有幫助的話,在看,轉(zhuǎn)發(fā)吧。
謝謝支持喲 (*^__^*)
