以前写了几篇对于 SpringBoot
的文章《面试高频题:springBoot 主动拆卸的原理你能说进去吗》、《保姆级教程,手把手教你实现一个 SpringBoot 的 starter》,这几天忽然有个读者问:能说一说 Spring
的父子容器吗?说实话这其实也是 Spring
八股文外面一个比拟常见的问题。在我的印象外面 Spring
就是父容器,SpringMvc
就是子容器,子容器能够拜访父容器的内容,父容器不能拜访子容器的货色。有点相似 java 外面的继承的滋味,子类能够继承父类共有办法和变量,能够拜访它们,父类不能够拜访子类的办法和变量。在这里就会衍生出几个比拟经典的问题:
- 为什么须要父子容器?
- 是否能够把所有类都通过
Spring
容器来治理?(Spring
的applicationContext.xml
中配置全局扫描) - 是否能够把咱们所需的类都放入
Spring-mvc
子容器外面来治理(springmvc
的spring-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@3972
在Controller
中对应的容器对象是 XmlWebApplicationContext@4114
由此可见它们是两个不同的容器。
源码剖析
咱们晓得 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
”咱们拿着这个日志就能定位到代码了
`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
类上述代码是不是会有个疑难咱们怎么就会执行 FrameworkServlet
的initServletBean
办法。这是因为咱们在 web.xml
外面配置了DispatcherServlet
,而后 web 容器就会去调用DispatcherServlet
的init
办法,并且这个办法只会被执行一次。通过 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 来配置service
、dao
层。如果当初咱们想把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
怎么也不行了,网上都是这么配置的。
完结
- 因为本人满腹经纶,难免会有纰漏,如果你发现了谬误的中央,还望留言给我指出来, 我会对其加以修改。
- 如果你感觉文章还不错,你的转发、分享、赞叹、点赞、留言就是对我最大的激励。
- 感谢您的浏览, 非常欢送并感谢您的关注。
-
站在伟人的肩膀上摘苹果: https://www.cnblogs.com/grasp…
https://javajr.cn
往期精选
* 举荐👍:Java 高并发编程根底三大利器之 CyclicBarrier*
举荐👍:Java 高并发编程根底三大利器之 CountDownLatch
举荐👍:Java 高并发编程根底三大利器之 Semaphore
举荐👍:Java 高并发编程根底之 AQS
举荐👍:可恶的爬虫间接把生产 6 台机器爬挂了!