多 IOC 容器整合

为了实现更好的解耦,我们在实际开发中往往还是需要将数据源、Service、Dao 等组件配置到传统的 Spring 配置文件中,并通过ContextLoaderListener启动这个 IOC 容器。 而在表述层负责处理请求的 handler 组件则使用 SpringMVC 自己来启动。

这会导致一个问题:同样的组件会被创建两次。

1、多个 IOC 容器之间的关系

① 启动日志中的关键信息

五月 13, 2021 9:06:39 上午 org.springframework.web.context.ContextLoader initWebApplicationContext
信息: Root WebApplicationContext: initialization started
五月 13, 2021 9:06:39 上午 org.springframework.web.context.support.XmlWebApplicationContext prepareRefresh
信息: Refreshing Root WebApplicationContext: startup date [Thu May 13 09:06:39 CST 2021]; root of context hierarchy
五月 13, 2021 9:06:39 上午 org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
信息: Loading XML bean definitions from class path resource [spring-ioc.xml]
五月 13, 2021 9:06:40 上午 org.springframework.web.context.ContextLoader initWebApplicationContext
信息: Root WebApplicationContext: initialization completed in 804 ms
五月 13, 2021 9:06:40 上午 org.springframework.web.servlet.DispatcherServlet initServletBean
信息: FrameworkServlet ‘dispatcherServlet’: initialization started
五月 13, 2021 9:06:40 上午 org.springframework.web.context.support.XmlWebApplicationContext prepareRefresh
信息: Refreshing WebApplicationContext for namespace ‘dispatcherServlet-servlet’: startup date [Thu May 13 09:06:40 CST 2021]; parent: Root WebApplicationContext
五月 13, 2021 9:06:40 上午 org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
信息: Loading XML bean definitions from class path resource [spring-mvc.xml]
五月 13, 2021 9:06:40 上午 org.springframework.web.servlet.DispatcherServlet initServletBean
信息: FrameworkServlet ‘dispatcherServlet’: initialization completed in 544 ms

结论:ContextLoaderListener 创建的是父容器;DispatcherServlet 创建的是子容器。

② 源码

[1]设置父容器的源码位置

org.springframework.web.servlet.FrameworkServlet 类

createWebApplicationContext(ApplicationContext parent)方法

关键代码:wac.setParent(parent);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
protected WebApplicationContext createWebApplicationContext(ApplicationContext parent) {
Class<?> contextClass = this.getContextClass();
if (this.logger.isDebugEnabled()) {
this.logger.debug("Servlet with name '" + this.getServletName() + "' will try to create custom WebApplicationContext context of class '" + contextClass.getName() + "'" + ", using parent context [" + parent + "]");
}

if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {
throw new ApplicationContextException("Fatal initialization error in servlet with name '" + this.getServletName() + "': custom WebApplicationContext class [" + contextClass.getName() + "] is not of type ConfigurableWebApplicationContext");
} else {
ConfigurableWebApplicationContext wac = (ConfigurableWebApplicationContext)BeanUtils.instantiateClass(contextClass);
wac.setEnvironment(this.getEnvironment());
wac.setParent(parent);
wac.setConfigLocation(this.getContextConfigLocation());
this.configureAndRefreshWebApplicationContext(wac);
return wac;
}
}

[2]将根级别容器存入应用域

ContextLoaderListener 创建 IOC 容器对象后,将 IOC 容器对象存入了 ServletContext 的属性域

属性名是 org.springframework.web.context.WebApplicationContext 类中定义的常量:ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE

源码证据:

所在类:org.springframework.web.context.ContextLoader

所在方法:initWebApplicationContext(ServletContext servletContext)

关键代码:servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);

[3]把父容器赋值给 parent 属性

所在类:org.springframework.context.support.AbstractApplicationContext

所在方法:setParent(ApplicationContext parent)

关键代码:this.parent = parent;

结论:SpringMVC 的 IOC 容器对象把 Spring 的 IOC 容器对象赋值给了自己的 parent 属性。也就是说通过 parent 属性能够获取到父容器对象的引用。当有需要获取父容器时,调用 getParent()方法即可

③ 子容器访问父容器中资源

结论:子容器能够访问到父容器中的资源(bean);父容器没法访问到子容器中的资源。

原因:子容器可以通过自己的 parent 属性找到父容器对象,但是父容器对象并没有属性存放所有子容器。

2、重复创建对象

① 证据

在各个组件的无参构造器中,编写打印代码,然后查看启动日志:

信息: Loading XML bean definitions from class path resource [spring-ioc.xml]
EmpControler 对象创建了
EmpService 对象创建了
EmpDao 对象创建了

……

信息: Loading XML bean definitions from class path resource [spring-mvc.xml]
EmpControler 对象创建了
EmpService 对象创建了
EmpDao 对象创建了

② 重复创建对象的危害

  • 危害 1:浪费内存
  • 危害 2:由 SpringMVC 的 IOC 容器创建的 EmpService 是没有事务的。而此时 EmpController 会就近装配这个没有事务的 EmpService,从而导致程序运行时调用是 Service 是一个没有事务功能的 Service

③ 解决方案 1[建议]

[1]Spring 扫描的包

1
2
3
<!-- 配置自动扫描的包 -->
<!-- 重复创建对象的解决方案一:两个IOC容器各扫描各的 -->
<context:component-scan base-package="com.atguigu.ioc.component.dao,com.atguigu.ioc.component.service"/>

[2]SpringMVC 扫描的包

1
2
3
<!-- 配置自动扫描的包 -->
<!-- 重复创建对象的解决方案一:两个IOC容器各扫描各的 -->
<context:component-scan base-package="com.atguigu.ioc.component.controller"/>

④ 解决方案 2

[1]Spring 扫描的包

1
2
3
4
5
6
7
<!-- 重复创建对象的解决方案二:在两个IOC容器都扫描同一个包的前提下,Spring的IOC容器排除@Controller注解标记的类 -->
<context:component-scan base-package="com.atguigu.ioc.component">
<!-- context:exclude-filter配置排除规则 -->
<context:exclude-filter
type="annotation"
expression="org.springframework.stereotype.Controller"/>
</context:component-scan>

[2]SpringMVC 扫描的包

1
2
3
4
5
6
7
8
9
<!-- 重复创建对象的解决方案二:在两个IOC容器都扫描同一个包的前提下,SpringMVC的IOC容器仅扫描@Controller注解标记的类 -->
<!-- 使用use-default-filters="false"关闭默认规则 -->
<context:component-scan base-package="com.atguigu.ioc.component" use-default-filters="false">

<!-- context:include-filter标签表示在原有规则的基础上追加规则 -->
<context:include-filter
type="annotation"
expression="org.springframework.stereotype.Controller"/>
</context:component-scan>

注意:两种解决方案只可以选择其中的一种,推荐解决方案 1。