乐趣区

关于spring:阿里一面Spring和SpringMvc父子容器你能说清楚吗

以前写了几篇对于 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 怎么也不行了,网上都是这么配置的。

完结

  • 因为本人满腹经纶,难免会有纰漏,如果你发现了谬误的中央,还望留言给我指出来, 我会对其加以修改。
  • 如果你感觉文章还不错,你的转发、分享、赞叹、点赞、留言就是对我最大的激励。
  • 感谢您的浏览, 非常欢送并感谢您的关注。
  • 站在伟人的肩膀上摘苹果: https://www.cnblogs.com/grasp…

    https://javajr.cn

往期精选

* 举荐👍:Java 高并发编程根底三大利器之 CyclicBarrier*

举荐👍:Java 高并发编程根底三大利器之 CountDownLatch

举荐👍:Java 高并发编程根底三大利器之 Semaphore

举荐👍:Java 高并发编程根底之 AQS

举荐👍:可恶的爬虫间接把生产 6 台机器爬挂了!

退出移动版