⭐表示重要。
第一章:提出问题
- 目前情况:DispatchServlet 加载 springmvc.xml ,此时整个 WEB 应用只创建一个 IOC 容器。将来整合 Mybatis 、配置声明式事务等,全都在 springmvc.xml 配置文件中配置其实也是可以的。但是,这样会导致配置文件太长,不容易维护。
- 希望将配置文件分开:
- 处理浏览器请求相关:springmvc.xml 配置文件。
- 声明式事务和整合 Mybatis 相关:spring-persist.xml 配置文件。
- 配置文件分开之后,可以让 DispatcherServlet 加载多个配置文件:
<servlet><servlet-name>dispatcherServlet</servlet-name><servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class><init-param><param-name>contextConfigLocation</param-name><param-value>classpath:spring*.xml</param-value></init-param><load-on-startup>1</load-on-startup></servlet>
- 但是,如果希望上面的两个配置文件使用不同的机制来加载:
- DispatchServlet 加载 springmvc.xml 配置文件:处理浏览器请求相关。
- ContextLoaderListener 加载 spring-persist.xml 配置文件:不需要处理浏览器请求,需要配置持久化层相关功能。
- 此时,会带来一个新的问题:在 WEB 应用中就会出现两个 IOC 容器:
- DispatchServlet 创建一个 IOC 容器。
- ContextLoaderListener 创建一个 IOC 容器。
注意:这个技术方案并不是
『必须』这样做,而仅仅是『可以』这样做。
第二章:配置 ContextLoaderListener
2.1 创建 spring-persist.xml

2.2 配置 ContextLoaderListener
- web.xml
<!-- 配置监听器 --><listener><!-- 指定全类名 --><listener-class>org.springframework.web.context.ContextLoaderListener</listener-class></listener><!-- 通过全局初始化参数指定 Spring 配置文件的位置 --><context-param><param-name>contextConfigLocation</param-name><param-value>classpath:spring-persist.xml</param-value></context-param>
- 完整的 web.xml
<?xml version="1.0" encoding="UTF-8"?><web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns="http://xmlns.jcp.org/xml/ns/javaee"xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaeehttp://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"version="4.0"><filter><filter-name>CharacterEncodingFilter</filter-name><filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class><init-param><param-name>encoding</param-name><param-value>UTF-8</param-value></init-param><init-param><param-name>forceRequestEncoding</param-name><param-value>true</param-value></init-param><init-param><param-name>forceResponseEncoding</param-name><param-value>true</param-value></init-param></filter><filter-mapping><filter-name>CharacterEncodingFilter</filter-name><url-pattern>/*</url-pattern></filter-mapping><filter><filter-name>hiddenHttpMethodFilter</filter-name><filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class></filter><filter-mapping><filter-name>hiddenHttpMethodFilter</filter-name><url-pattern>/*</url-pattern></filter-mapping><!-- 配置监听器 --><listener><!-- 指定全类名 --><listener-class>org.springframework.web.context.ContextLoaderListener</listener-class></listener><!-- 通过全局初始化参数指定 Spring 配置文件的位置 --><context-param><param-name>contextConfigLocation</param-name><param-value>classpath:spring-persist.xml</param-value></context-param><!-- 配置SpringMVC中负责处理请求的核心Servlet,也被称为SpringMVC的前端控制器 --><servlet><servlet-name>dispatcherServlet</servlet-name><servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class><init-param><param-name>contextConfigLocation</param-name><param-value>classpath:springmvc.xml</param-value></init-param><load-on-startup>1</load-on-startup></servlet><servlet-mapping><servlet-name>dispatcherServlet</servlet-name><url-pattern>/</url-pattern></servlet-mapping></web-app>
2.3 ContextLoaderListener

| 方法名 | 执行时机 | 作用 |
|---|---|---|
| contextInitialized() | Web 应用启动时执行 | 创建并初始化 IOC 容器 |
| contextDestroyed() | Web 应用卸载时执行 | 关闭 IOC 容器 |
2.4 ContextLoader
ContextLoader 是 ContextLoaderListener 的父类。
① 指定配置文件位置的参数名:
public class ContextLoader {.../*** Name of servlet context parameter (i.e., {@value}) that can specify the* config location for the root context, falling back to the implementation's* default otherwise.* @see org.springframework.web.context.support.XmlWebApplicationContext#DEFAULT_CONFIG_LOCATION*/public static final String CONFIG_LOCATION_PARAM = "contextConfigLocation";}
- ② 初始化 IOC 容器:
public class ContextLoader {.../*** Initialize Spring's web application context for the given servlet context,* using the application context provided at construction time, or creating a new one* according to the "{@link #CONTEXT_CLASS_PARAM contextClass}" and* "{@link #CONFIG_LOCATION_PARAM contextConfigLocation}" context-params.* @param servletContext current servlet context* @return the new WebApplicationContext* @see #ContextLoader(WebApplicationContext)* @see #CONTEXT_CLASS_PARAM* @see #CONFIG_LOCATION_PARAM*/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) {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, etcif (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);}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;}}}
- ③ 创建 IOC 容器:
public class ContextLoader {.../*** Instantiate the root WebApplicationContext for this loader, either the* default context class or a custom context class if specified.* <p>This implementation expects custom contexts to implement the* {@link ConfigurableWebApplicationContext} interface.* Can be overridden in subclasses.* <p>In addition, {@link #customizeContext} gets called prior to refreshing the* context, allowing subclasses to perform custom modifications to the context.* @param sc current servlet context* @return the root WebApplicationContext* @see ConfigurableWebApplicationContext*/protected WebApplicationContext createWebApplicationContext(ServletContext sc) {Class<?> contextClass = determineContextClass(sc);if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {throw new ApplicationContextException("Custom context class [" + contextClass.getName() +"] is not of type [" + ConfigurableWebApplicationContext.class.getName() + "]");}return (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);}}
第三章:探讨两个 IOC 容器之间的关系
- 打印两个 IOC 容器对象的 toString() 方法:
package com.github.fairy.era.handler;import com.github.fairy.era.service.HelloWorldService;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Controller;import org.springframework.ui.Model;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.context.WebApplicationContext;import org.springframework.web.servlet.FrameworkServlet;import javax.servlet.ServletContext;/*** @author 许大仙* @version 1.0* @since 2021-11-20 07:18*/@Controllerpublic class HelloWorldHandler {private Logger logger = LoggerFactory.getLogger(this.getClass());@Autowiredprivate HelloWorldService helloWorldService;@Autowiredprivate ServletContext servletContext;@GetMapping("/hello/world")public String helloWorld(Model model) {String message = helloWorldService.getMessage();model.addAttribute("message", message);Object springMVCIOC = servletContext.getAttribute(FrameworkServlet.SERVLET_CONTEXT_PREFIX + "dispatcherServlet");logger.info("springMVCIOC = {}", springMVCIOC);Object springIOC = servletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE);logger.info("springIOC = {}", springIOC);return "target";}}
- 日志如下:
07:51:13.727] [INFO ] [http-nio-8080-exec-4] [com.github.fairy.era.handler.HelloWorldHandler] [springMVCIOC = WebApplicationContext for namespace 'dispatcherServlet-servlet', started on Sat Nov 20 07:51:08 CST 2021, parent: Root WebApplicationContext][07:51:13.727] [INFO ] [http-nio-8080-exec-4] [com.github.fairy.era.handler.HelloWorldHandler] [springIOC = Root WebApplicationContext, started on Sat Nov 20 07:51:08 CST 2021]
- 结论:两个组件分别创建的 IOC 容器是
父子关系。- 父容器:ContextLoaderListener 创建的 IOC 容器。
- 子容器:DispatchServlet 创建的 IOC 容器。
- 父子关系是如何确定的?
- Tomcat 在读取 web.xml 之后,加载组件的顺序就是监听器、过滤器、Servlet。
- ContextLoaderListener 初始化时如果检查到有已经存在的根级别 IOC 容器,那么会抛出异常。
- DispatcherServlet 创建的 IOC 容器会在初始化时先检查当前环境下是否存在已经创建好的 IOC 容器。
- 如果有:则将已存在的这个 IOC 容器设置为自己的父容器。
- 如果没有:则将自己设置为 root 级别的 IOC 容器。
- DispatcherServlet 创建的 IOC 容器设置父容器的源码:
- 所在类:org.springframework.web.servlet.FrameworkServlet
- 所在方法:createWebApplicationContext()
public abstract class FrameworkServlet extends HttpServletBean implements ApplicationContextAware {...protected WebApplicationContext createWebApplicationContext(@Nullable ApplicationContext parent) {Class<?> contextClass = this.getContextClass();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);String configLocation = this.getContextConfigLocation();if (configLocation != null) {wac.setConfigLocation(configLocation);}this.configureAndRefreshWebApplicationContext(wac);return wac;}}}
第四章:两个 IOC 容器之间 bean 的互相访问
- EmpDao.java
package com.github.fairy.era.dao;import org.springframework.stereotype.Repository;/*** @author 许大仙* @version 1.0* @since 2021-11-20 18:07*/@Repositorypublic class EmpDao {public String getMessage() {return "你好啊";}}
- EmpService.java
package com.github.fairy.era.service;import com.github.fairy.era.dao.EmpDao;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;/*** @author 许大仙* @version 1.0* @since 2021-11-20 18:08*/@Servicepublic class EmpService {@Autowiredprivate EmpDao empDao;public String getMessage() {return empDao.getMessage();}}
- EmpHandler.java
package com.github.fairy.era.handler;import com.github.fairy.era.service.EmpService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Controller;import org.springframework.ui.Model;import org.springframework.web.bind.annotation.GetMapping;/*** @author 许大仙* @version 1.0* @since 2021-11-20 18:08*/@Controllerpublic class EmpHandler {@Autowiredprivate EmpService empService;@GetMapping("/message")public String message(Model model) {model.addAttribute("message", empService.getMessage());return "target";}}
- springmvc.xml
<?xml version="1.0" encoding="UTF-8"?><beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:context="http://www.springframework.org/schema/context"xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns="http://www.springframework.org/schema/beans"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd"><!-- 自动扫描包 --><context:component-scan base-package="com.github.fairy.era.handler"></context:component-scan><!-- 配置视图解析器 --><bean id="viewResolver" class="org.thymeleaf.spring5.view.ThymeleafViewResolver"><property name="order" value="1"/><property name="characterEncoding" value="UTF-8"/><property name="templateEngine"><bean class="org.thymeleaf.spring5.SpringTemplateEngine"><property name="templateResolver"><bean class="org.thymeleaf.spring5.templateresolver.SpringResourceTemplateResolver"><!-- 物理视图:视图前缀+逻辑视图+视图后缀 --><!-- 视图前缀 --><property name="prefix" value="/WEB-INF/templates/"/><!-- 视图后缀 --><property name="suffix" value=".html"/><property name="templateMode" value="HTML5"/><property name="characterEncoding" value="UTF-8"/></bean></property></bean></property></bean><mvc:annotation-driven/><mvc:default-servlet-handler/><mvc:view-controller path="/" view-name="portal"/></beans>
- spring-presist.xml
<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:context="http://www.springframework.org/schema/context"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd"><context:component-scan base-package="com.github.fairy.era.service,com.github.fairy.era.dao"/></beans>
- bean 所属 IOC 容器的关系:
- 父容器:
- EmpService。
- EmpDao。
- 子容器:
- EmpController。
- 父容器:
- 结论:子容器中的 EmpHandler 装配父容器中的 EmpService 能够正常工作,说明子容器可以访问父容器中的 bean 。
- 分析:
子可父用,父不能子用的根本原因是子容器中有一个属性getParent()方法可以获取到父容器的这个对应的引用。 - 源码分析:
- ① 在 AbstractApplicationContext 类中,有 parent 属性。
- ② 在 AbstractApplicationContext 类中,有获取 parent 属性的 getParent() 方法。
- ③ 子容器可以通过 getParent() 方法获取到父容器对象的引用,进而调用父容器中类似 “getBean()” 这样的方法获取到需要的 bean 完成装配。
- ④ 父容器中并没有类似 getChildren() 这样的方法,所以没法拿到子容器对象的引用。

第五章:提出和解决重复创建对象的问题
5.1 提出有可能重复创建对象的问题
- 修改 logback.xml ,将日志级别改为 DEBUG
<?xml version="1.0" encoding="UTF-8"?><configuration debug="true"><!-- 指定日志输出的位置 --><appender name="STDOUT"class="ch.qos.logback.core.ConsoleAppender"><encoder><!-- 日志输出的格式 --><!-- 按照顺序分别是:时间、日志级别、线程名称、打印日志的类、日志主体内容、换行 --><pattern>[%d{HH:mm:ss.SSS}] [%-5level] [%thread] [%logger] [%msg]%n</pattern><charset>UTF-8</charset></encoder></appender><!-- 设置全局日志级别。日志级别按顺序分别是:DEBUG、INFO、WARN、ERROR --><!-- 指定任何一个日志级别都只打印当前级别和后面级别的日志。 --><root level="DEBUG"><!-- 指定打印日志的appender,这里通过“STDOUT”引用了前面配置的appender --><appender-ref ref="STDOUT" /></root><!-- 根据特殊需求指定局部日志级别 --><logger name="org.springframework.web.servlet.DispatcherServlet" level="DEBUG" /></configuration>
- spring-persist.xml
<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:context="http://www.springframework.org/schema/context"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd"><context:component-scan base-package="com.github.fairy.era"/></beans>
- springmvc.xml
<?xml version="1.0" encoding="UTF-8"?><beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:context="http://www.springframework.org/schema/context"xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns="http://www.springframework.org/schema/beans"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd"><!-- 自动扫描包 --><context:component-scan base-package="com.github.fairy.era"></context:component-scan><!-- 配置视图解析器 --><bean id="viewResolver" class="org.thymeleaf.spring5.view.ThymeleafViewResolver"><property name="order" value="1"/><property name="characterEncoding" value="UTF-8"/><property name="templateEngine"><bean class="org.thymeleaf.spring5.SpringTemplateEngine"><property name="templateResolver"><bean class="org.thymeleaf.spring5.templateresolver.SpringResourceTemplateResolver"><!-- 物理视图:视图前缀+逻辑视图+视图后缀 --><!-- 视图前缀 --><property name="prefix" value="/WEB-INF/templates/"/><!-- 视图后缀 --><property name="suffix" value=".html"/><property name="templateMode" value="HTML5"/><property name="characterEncoding" value="UTF-8"/></bean></property></bean></property></bean><mvc:annotation-driven/><mvc:default-servlet-handler/><mvc:view-controller path="/" view-name="portal"/></beans>
- 日志:
...[20:37:48.766] [DEBUG] [RMI TCP Connection(3)-127.0.0.1] [org.springframework.beans.factory.support.DefaultListableBeanFactory] [Creating shared instance of singleton bean 'empDao'][20:37:48.772] [DEBUG] [RMI TCP Connection(3)-127.0.0.1] [org.springframework.beans.factory.support.DefaultListableBeanFactory] [Creating shared instance of singleton bean 'empHandler'][20:37:48.791] [DEBUG] [RMI TCP Connection(3)-127.0.0.1] [org.springframework.beans.factory.support.DefaultListableBeanFactory] [Creating shared instance of singleton bean 'empService']...[20:37:49.185] [DEBUG] [RMI TCP Connection(3)-127.0.0.1] [org.springframework.beans.factory.support.DefaultListableBeanFactory] [Creating shared instance of singleton bean 'empDao'][20:37:49.186] [DEBUG] [RMI TCP Connection(3)-127.0.0.1] [org.springframework.beans.factory.support.DefaultListableBeanFactory] [Creating shared instance of singleton bean 'empHandler'][20:37:49.187] [DEBUG] [RMI TCP Connection(3)-127.0.0.1] [org.springframework.beans.factory.support.DefaultListableBeanFactory] [Creating shared instance of singleton bean 'empService']
5.2 重复创建对象的问题
- ① 浪费内存空间。
- ② 两个 IOC 容器能力是不同的:
- springmvc.xml:仅配置和处理请求相关的功能,所以不能给 service 类附加声明式事务功能。
- 结论:基于 springmvc.xml 配置文件创建的 EmpService 的 bean 不带有声明式事务的功能。
- 影响:DispatcherServlet 处理浏览器请求时会调用自己创建的 EmpController,然后再调用自己创建的EmpService,而这个 EmpService 是没有事务的,所以处理请求时
没有事务功能的支持。
- spring-persist.xml:配置声明式事务,所以可以给 service 类附加声明式事务功能。
- 结论:基于 spring-persist.xml 配置文件创建的 EmpService 有声明式事务的功能。
- 影响:由于 DispatcherServlet 的 IOC 容器会优先使用自己创建的 EmpController,进而装配自己创建的EmpService,所以基于 spring-persist.xml 配置文件创建的有声明式事务的 EmpService 用不上。
- springmvc.xml:仅配置和处理请求相关的功能,所以不能给 service 类附加声明式事务功能。
5.3 解决重复创建对象的问题
5.3.1 解决方案一(建议使用)
- 让两个配置文件配置自动扫描的包时,各自扫描各自的组件:
- ① SpringMVC 就扫描 XxxHandler、XXXController 。
- ② Spring 扫描 XxxService 和 XxxDao 。
5.3.2 解决方案二
- 如果由于某种原因,必须扫描同一个包,确实存在重复创建对象的问题,可以采取下面的办法处理。
- springmvc.xml 配置文件在整体扫描的基础上进一步配置:仅包含被 @Controller 注解标记的类。
- spring-persist.xml 配置在整体扫描的基础上进一步配置:排除被 @Controller 注解标记的类。
- 具体 springmvc.xml 配置文件中的配置方式如下:
<!-- 两个Spring的配置文件扫描相同的包 --><!-- 为了解决重复创建对象的问题,需要进一步制定扫描组件时的规则 --><!-- 目标:『仅』包含@Controller注解标记的类 --><!-- use-default-filters="false"表示关闭默认规则,表示什么都不扫描,此时不会把任何组件加入IOC容器;再配合context:include-filter实现“『仅』包含”效果 --><context:component-scan base-package="com.github.spring.component" use-default-filters="false"><!-- context:include-filter标签配置一个“扫描组件时要包含的类”的规则,追加到默认规则中 --><!-- type属性:指定规则的类型,根据什么找到要包含的类,现在使用annotation表示基于注解来查找 --><!-- expression属性:规则的表达式。如果type属性选择了annotation,那么expression属性配置注解的全类名 --><context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/></context:component-scan>
- 具体 spring-persist.xml 配置文件中的配置方式如下:
<!-- 两个Spring的配置文件扫描相同的包 --><!-- 在默认规则的基础上排除标记了@Controller注解的类 --><context:component-scan base-package="com.github.spring.component"><!-- 配置具体排除规则:把标记了@Controller注解的类排除在扫描范围之外 --><context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/></context:component-scan>
