其他分享
首页 > 其他分享> > 面试高频题:说一说对Spring和SpringMvc父子容器的理解?

面试高频题:说一说对Spring和SpringMvc父子容器的理解?

作者:互联网

引言

以前写了几篇关于SpringBoot的文章《面试高频题:springBoot自动装配的原理你能说出来吗》《保姆级教程,手把手教你实现一个SpringBoot的starter》,这几天突然有个读者问:能说一说Spring的父子容器吗?说实话这其实也是Spring八股文里面一个比较常见的问题。在我的印象里面Spring就是父容器,SpringMvc就是子容器,子容器可以访问父容器的内容,父容器不能访问子容器的东西。有点类似java里面的继承的味道,子类可以继承父类共有方法和变量,可以访问它们,父类不可以访问子类的方法和变量。在这里就会衍生出几个比较经典的问题:

Spring的启动

要想很好的理解它们之间的关系,我们就有必要先弄清楚Spring的启动流程。要弄清楚这个启动流程我们就需要搭建一个SpringMvc项目,说句实话,用惯了SpringBooot开箱即用,突然在回过头来搭建一个SpringMvc项目还真有点不习惯,一大堆的配置文件。(虽然也可以用注解来实现)具体怎么搭建SpringMvc项目这个就不介绍了,搭建好项目我们运行起来可以看到控制台会输出如下日志:
在这里插入图片描述
日志里面分别打印出了父容器和子容器分别的一个耗时。

如何验证是有两个容器?

我们只需要Controller与我们的Service中实现ApplicationContextAware接口,就可已得知对应的管理容器:
Service所属的父容器里面我们可以看到父容器对应的对象是XmlWebApplicationContext@3972
在这里插入图片描述
Controller中对应的容器对象是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子容器的一个启动过程,以及每个容器都分别干了什么事情现在再回过头来看看上述四个问题。

<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。
在这里插入图片描述

总结

结束

标签:xml,容器,说一说,web,SpringMvc,Spring,context,WebApplicationContext
来源: https://www.cnblogs.com/root429/p/14608955.html