1. 父子容器的定义与设计初衷
一句话总结:父子容器的核心价值在于解耦 Web 层与业务层,实现职责分离与上下文隔离。
1.1 父子容器的层次关系
在 Spring MVC 中,容器分为两类:
父容器(Root ApplicationContext):由
ContextLoaderListener
创建,主要存放 Service、DAO、事务管理器、数据源 等业务相关 Bean。子容器(WebApplicationContext):由
DispatcherServlet
创建,主要存放 Controller、HandlerMapping、ViewResolver 等 Web 层相关 Bean。
层级关系描述(文字版流程图):
Tomcat 启动,
ContextLoaderListener
初始化父容器。父容器加载全局 Bean(数据源、事务管理器、业务 Service)。
DispatcherServlet
初始化子容器,并将父容器引用传递给它。子容器加载 Web 层 Bean(Controller、ViewResolver)。
Bean 查找规则:先找子容器 → 找不到再去父容器。
1.2 生命周期差异
特性 | 父容器(Root) | 子容器(Web) |
---|---|---|
创建时机 | Web 容器启动时 | 每个 DispatcherServlet 启动时 |
销毁时机 | Web 容器关闭时 | 对应 Servlet 销毁时 |
Bean 作用域 | 全局共享 | 仅限当前 Servlet |
常见存放对象 | Service、DAO、事务管理器 | Controller、视图解析器、拦截器 |
1.3 单容器 vs 父子容器
对比维度 | 单容器架构 | 父子容器架构 |
---|---|---|
隔离性 | 无隔离,所有 Bean 混在一个容器里 | Web 层与业务层隔离,减少耦合 |
可维护性 | 项目大时配置混乱 | 分层清晰,职责明确 |
启动效率 | 启动慢(所有 Bean 一起加载) | 可按 Servlet 粒度启动部分 Web 层 |
适用场景 | 小型单体应用 | 中大型单体应用,多个 Web 模块共享业务层 |
1.4 为什么要隔离 Web 层与业务层(业务场景)
场景:一个订单管理系统,有两个模块:
PC 端订单管理(
/pc/order
)移动端订单管理(
/mobile/order
)
如果使用 父子容器:
订单 Service、DAO、事务管理器放在 父容器,PC 和移动端的 Controller 可以共享它们。
两个模块的 Controller、拦截器、视图配置放在 不同的子容器,互不干扰。
1.5 示例代码:XML 版父子容器
web.xml
<!-- 父容器配置 -->
<context-param><param-name>contextConfigLocation</param-name><param-value>classpath:spring/root-context.xml</param-value>
</context-param>
<listener><listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener><!-- 子容器配置 -->
<servlet><servlet-name>spring-mvc</servlet-name><servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class><init-param><param-name>contextConfigLocation</param-name><param-value>classpath:spring/servlet-context.xml</param-value></init-param><load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping><servlet-name>spring-mvc</servlet-name><url-pattern>/</url-pattern>
</servlet-mapping>
root-context.xml(父容器)
<context:component-scan base-package="com.example.service, com.example.dao" />
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" />
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"><property name="dataSource" ref="dataSource"/>
</bean>
servlet-context.xml(子容器)
<context:component-scan base-package="com.example.web" />
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"><property name="prefix" value="/WEB-INF/views/" /><property name="suffix" value=".jsp" />
</bean>
2. 父子容器的实现原理
一句话总结:父子容器通过 WebApplicationContext
的层级结构和 Bean 查找链实现单向依赖。
2.1 ContextLoaderListener(父容器)加载流程
源码入口:ContextLoaderListener
→ contextInitialized()
→ initWebApplicationContext()
流程:
创建
WebApplicationContext
实例(默认XmlWebApplicationContext
)。从
contextConfigLocation
读取父容器配置文件。调用
refresh()
完成 Bean 加载和初始化。将父容器放入
ServletContext
,供子容器引用。
简化源码(伪代码):
public WebApplicationContext initWebApplicationContext(ServletContext sc) {WebApplicationContext wac = createWebApplicationContext(sc);configureAndRefresh(wac);sc.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, wac);return wac;
}
2.2 DispatcherServlet(子容器)加载流程
源码入口:DispatcherServlet
→ initServletBean()
→ initWebApplicationContext()
流程:
创建子容器
WebApplicationContext
。从
contextConfigLocation
读取子容器配置文件。调用
setParent()
将父容器引用传入。调用
refresh()
初始化 Web 层 Bean。
简化源码(伪代码):
protected WebApplicationContext initWebApplicationContext() {WebApplicationContext parent = WebApplicationContextUtils.getWebApplicationContext(getServletContext());WebApplicationContext wac = createWebApplicationContext(parent);configureAndRefresh(wac);return wac;
}
2.3 Bean 查找机制
WebApplicationContext
继承自 ApplicationContext
,其 getBean()
查找规则:
先在当前容器查找 Bean。
如果找不到且有父容器,则向父容器递归查找。
找不到则抛出
NoSuchBeanDefinitionException
。Bean getBean(String name) {if (this.containsBean(name)) {return this.getLocalBean(name);} else if (this.parent != null) {return this.parent.getBean(name);} else {throw new NoSuchBeanDefinitionException(name);} }
2.4 子容器访问父容器 Bean(示例)
Service(父容器)
@Service
public class OrderService {public void createOrder() {System.out.println("订单创建成功");}
}
Controller(子容器)
@Controller
public class OrderController {@Autowiredprivate OrderService orderService;@RequestMapping("/create")public String createOrder() {orderService.createOrder();return "success";}
}
2.5 深入源码:refresh()
方法调用链
refresh()
是 Spring 容器启动的核心方法,父子容器初始化时都会调用它。无论是 ContextLoaderListener
还是 DispatcherServlet
,最终都会走到这里。
核心流程(文字版调用链)
prepareRefresh() — 准备环境变量、校验配置文件、记录启动时间。
obtainFreshBeanFactory() — 创建或刷新
BeanFactory
实例(DefaultListableBeanFactory
)。prepareBeanFactory(beanFactory) — 注册默认的
BeanPostProcessor
、环境变量、依赖解析器等。postProcessBeanFactory(beanFactory) — 模板方法,允许子类扩展(如
AbstractRefreshableWebApplicationContext
会在这里注册 Web 相关 Bean)。invokeBeanFactoryPostProcessors(beanFactory) — 执行
BeanFactoryPostProcessor
(如ConfigurationClassPostProcessor
解析@Configuration
和@ComponentScan
)。registerBeanPostProcessors(beanFactory) — 注册所有
BeanPostProcessor
(AOP、@Autowired 等依赖注入的关键)。initMessageSource() — 初始化国际化资源。
initApplicationEventMulticaster() — 初始化事件广播器。
onRefresh() — 模板方法,Spring MVC 子容器会在这里初始化
HandlerMapping
、HandlerAdapter
等。registerListeners() — 注册所有事件监听器。
finishBeanFactoryInitialization(beanFactory) — 实例化所有非懒加载单例 Bean。
finishRefresh() — 发布
ContextRefreshedEvent
事件,标记容器启动完成。
关键源码(简化版)
public void refresh() {synchronized (this.startupShutdownMonitor) {prepareRefresh();ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();prepareBeanFactory(beanFactory);postProcessBeanFactory(beanFactory);invokeBeanFactoryPostProcessors(beanFactory);registerBeanPostProcessors(beanFactory);initMessageSource();initApplicationEventMulticaster();onRefresh(); // Web 容器在这里启动 MVC 组件registerListeners();finishBeanFactoryInitialization(beanFactory);finishRefresh();}
}
2.6 父子容器的 BeanFactory 关系
父容器:
DefaultListableBeanFactory
子容器:
DefaultListableBeanFactory
,但 parentBeanFactory 指向父容器的 BeanFactory这种设计保证了 子容器可以访问父容器 Bean,但父容器无法访问子容器 Bean。
关系示意(文字版):
Parent BeanFactory (Service, DAO, TransactionManager)↑
Child BeanFactory (Controller, HandlerMapping, ViewResolver)
2.7 业务场景中的 refresh()
应用
假设我们有以下结构:
父容器:
DataSourceConfig
(数据源)、TransactionConfig
(事务管理器)子容器:
WebMvcConfig
(Controller、ViewResolver)
启动时:
ContextLoaderListener
调用refresh()
完成父容器初始化,数据源和事务管理器就绪。DispatcherServlet
调用refresh()
初始化子容器,Controller 中通过@Autowired
获取 Service。由于子容器的 BeanFactory parentBeanFactory = 父容器的 BeanFactory,Controller 可以直接注入 Service。
2.8 示例:验证父子容器 Bean 访问规则
// 父容器中的 Bean
@Service
public class ProductService {public String getProductName() {return "MacBook Pro";}
}// 子容器中的 Bean
@Controller
public class ProductController {@Autowiredprivate ProductService productService; // 直接注入父容器的 Bean@RequestMapping("/product")@ResponseBodypublic String product() {return productService.getProductName();}
}
如果你尝试在 父容器的 Bean 中注入子容器的 Controller,会报错:
@Service
public class InvalidService {@Autowiredprivate ProductController controller; // ❌ NoSuchBeanDefinitionException
}
3. 父子容器的配置实践
一句话总结:父子容器配置的核心是职责分层与包路径隔离,确保 Web 层和业务层的 Bean 不会相互污染。
3.1 基于 XML 的配置
3.1.1 web.xml 配置
<!-- 父容器配置(业务层) -->
<context-param><param-name>contextConfigLocation</param-name><param-value>classpath:spring/root-context.xml</param-value>
</context-param>
<listener><listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener><!-- 子容器配置(Web 层) -->
<servlet><servlet-name>spring-mvc</servlet-name><servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class><init-param><param-name>contextConfigLocation</param-name><param-value>classpath:spring/servlet-context.xml</param-value></init-param><load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping><servlet-name>spring-mvc</servlet-name><url-pattern>/</url-pattern>
</servlet-mapping>
解析:
contextConfigLocation
(父容器)会被ContextLoaderListener
在initWebApplicationContext()
中读取,然后传给refresh()
去加载配置。DispatcherServlet
自己的contextConfigLocation
也是在initWebApplicationContext()
里读取,并调用refresh()
初始化子容器。
3.1.2 父容器配置(root-context.xml)
<context:component-scan base-package="com.example.service, com.example.dao" /><bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"><property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/><property name="url" value="jdbc:mysql://localhost:3306/demo"/><property name="username" value="root"/><property name="password" value="123456"/>
</bean><bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"><property name="dataSource" ref="dataSource"/>
</bean>
3.1.3 子容器配置(servlet-context.xml)
<context:component-scan base-package="com.example.web" /><bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"><property name="prefix" value="/WEB-INF/views/"/><property name="suffix" value=".jsp"/>
</bean>
3.2 基于 Java Config 的配置
3.2.1 父容器配置
@Configuration
@ComponentScan(basePackages = {"com.example.service", "com.example.dao"}
)
public class RootConfig {@Beanpublic DataSource dataSource() {BasicDataSource ds = new BasicDataSource();ds.setDriverClassName("com.mysql.cj.jdbc.Driver");ds.setUrl("jdbc:mysql://localhost:3306/demo");ds.setUsername("root");ds.setPassword("123456");return ds;}@Beanpublic PlatformTransactionManager transactionManager(DataSource dataSource) {return new DataSourceTransactionManager(dataSource);}
}
3.2.2 子容器配置
@Configuration
@EnableWebMvc
@ComponentScan(basePackages = {"com.example.web"},excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION,classes = Service.class // 排除业务层)
)
public class WebConfig implements WebMvcConfigurer {@Beanpublic ViewResolver viewResolver() {InternalResourceViewResolver vr = new InternalResourceViewResolver();vr.setPrefix("/WEB-INF/views/");vr.setSuffix(".jsp");return vr;}
}
3.3 包隔离策略
为了确保父子容器的 Bean 空间不冲突,建议:
业务层包路径:
com.example.service
,com.example.dao
Web 层包路径:
com.example.web
父容器的
@ComponentScan
不要扫描com.example.web
子容器的
@ComponentScan
使用excludeFilters
排除业务层包
3.4 业务场景示例
场景:PC 端和移动端共用业务逻辑,但 UI 层不同。
父容器放:
OrderService
,ProductService
PC 子容器放:
PcOrderController
Mobile 子容器放:
MobileOrderController
好处:
两个子容器都能调用相同的
OrderService
修改 PC 端 Controller 不会影响移动端 Controller
启动时可以单独加载一个子容器进行测试
4. 父子容器的应用场景与局限性
一句话总结:父子容器非常适合中大型单体应用的分层管理,但在微服务场景中可能会被替代。
4.1 典型应用场景
场景 1:多个 Web 模块共享业务逻辑
假设一个企业系统有:
后台管理模块(/admin)
前台门户模块(/portal)
父容器:
UserService
ProductService
DataSource
TransactionManager
子容器:
admin 子容器:
AdminController
、AdminInterceptor
portal 子容器:
PortalController
、PortalInterceptor
好处:
Service 和 DAO 只加载一次,节省内存
控制器互不干扰,职责分离
场景 2:多 DispatcherServlet 的多语言站点
/en/*
→ English 子容器/cn/*
→ Chinese 子容器公共业务逻辑放在父容器
4.2 事务管理器必须放在父容器的原因
原因:事务管理器通常会在 Service 层通过 @Transactional
生效,而 @Transactional
的底层依赖于 AOP 代理,代理对象的生成需要在 业务 Bean 初始化阶段完成。
如果事务管理器放在子容器:
父容器初始化 Service 时找不到事务管理器 Bean(因为父容器无法向下访问子容器)。
事务增强器
BeanFactoryTransactionAttributeSourceAdvisor
无法正常创建代理对象,导致事务失效。
简化源码片段(事务增强器注册过程):
public class ProxyTransactionManagementConfiguration {@Beanpublic BeanFactoryTransactionAttributeSourceAdvisor transactionAdvisor(...) {// 需要获取事务管理器 Bean}
}
因为这个 Bean 注册发生在父容器 refresh()
阶段,所以事务管理器必须提前在父容器中准备好。
4.3 常见问题
问题 1:Bean 覆盖
如果父子容器中有相同名称的 Bean,子容器会优先返回自己的 Bean。
// 父容器
@Bean("productService")
public ProductService productServiceV1() { ... }// 子容器
@Bean("productService")
public ProductService productServiceV2() { ... }
结果:Controller 注入的是子容器版本。
解决方法:
使用
@Primary
明确优先级或者使用
@Qualifier
指定 Bean 名称
问题 2:依赖冲突
子容器中扫描到的 Bean 如果引用了父容器不存在的依赖,会导致启动失败。
避免在 Service 层直接引用 Controller
4.4 在微服务架构中的适用性
在微服务(如 Spring Boot + Spring Cloud)中,每个服务本质上都是一个独立的
ApplicationContext
,父子容器的概念意义不大。替代方案:
通过 API Gateway 和 Feign Client 进行模块解耦
使用共享依赖库(JAR)来复用业务逻辑
4.5 示例:父容器中的事务管理器配置
RootConfig.java
@Configuration
@ComponentScan(basePackages = {"com.example.service", "com.example.dao"})
@EnableTransactionManagement
public class RootConfig {@Beanpublic DataSource dataSource() {BasicDataSource ds = new BasicDataSource();ds.setDriverClassName("com.mysql.cj.jdbc.Driver");ds.setUrl("jdbc:mysql://localhost:3306/demo");ds.setUsername("root");ds.setPassword("123456");return ds;}@Beanpublic PlatformTransactionManager transactionManager(DataSource dataSource) {return new DataSourceTransactionManager(dataSource);}
}
OrderService.java
@Service
public class OrderService {@Transactionalpublic void createOrder() {System.out.println("事务开始:创建订单");// 数据库操作...}
}
OrderController.java
@Controller
public class OrderController {@Autowiredprivate OrderService orderService;@RequestMapping("/order")@ResponseBodypublic String order() {orderService.createOrder(); // 事务生效return "success";}
}
5. 父子容器的进阶优化
5.1 使用 WebApplicationInitializer
手动创建父子容器
Spring MVC 默认是通过 ContextLoaderListener
创建父容器,再由 DispatcherServlet
创建子容器。
我们可以用 Java Config 全程替代 XML,并且手动精确控制父子容器的关系。
示例代码
public class MyWebAppInitializer implements WebApplicationInitializer {@Overridepublic void onStartup(ServletContext servletContext) throws ServletException {// 1. 创建父容器AnnotationConfigWebApplicationContext rootContext = new AnnotationConfigWebApplicationContext();rootContext.register(RootConfig.class);servletContext.addListener(new ContextLoaderListener(rootContext));// 2. 创建子容器(DispatcherServlet 专用)AnnotationConfigWebApplicationContext mvcContext = new AnnotationConfigWebApplicationContext();mvcContext.register(WebMvcConfig.class);ServletRegistration.Dynamic dispatcher =servletContext.addServlet("dispatcher", new DispatcherServlet(mvcContext));dispatcher.setLoadOnStartup(1);dispatcher.addMapping("/");}
}
核心好处:
父子容器的边界由你自己定义
可以注册多个 DispatcherServlet,每个都有独立的子容器
可以在父容器创建前做预处理(如动态加载配置文件)
5.2 父子容器 Bean 冲突优化
在开发中,父子容器如果不加规则,很容易出现 Bean 名称冲突问题。
解决策略:
命名空间法
父容器所有 Bean 以
core*
开头子容器 Bean 以
web*
开头
这样在注入时几乎不会冲突
@Primary
标记优先被注入的 Bean@Bean @Primary public ProductService newProductService() { ... }
@Qualifier
明确注入指定 Bean 名称
5.3 事务与 AOP 跨容器优化
当事务涉及多个子容器的 Controller 时,有两个注意点:
事务必须放在父容器
否则子容器 Controller 调用父容器 Service 时可能找不到事务增强器
AOP 切面建议也放在父容器
避免每个子容器重复创建切面 Bean
示例:切面放在父容器
@Aspect
@Component
public class LogAspect {@Before("execution(* com.example.service.*.*(..))")public void logBefore() {System.out.println("调用 Service 前记录日志");}
}
5.4 Spring Boot 中的父子容器简化策略
Spring Boot 虽然默认是单容器,但仍然可以模拟父子容器:
父容器:
SpringApplicationBuilder
的第一个sources()
子容器:
child()
方法
示例:Boot 模拟父子容器
new SpringApplicationBuilder(ParentConfig.class).child(WebConfig.class).run(args);
这样可以在 Boot 项目中仍然使用父子容器分层结构,但更轻量。
5.5 性能与维护建议
性能优化:
父容器只加载一次,不要放和 Web 强绑定的 Bean
子容器尽量只扫描 Controller、拦截器等 Web 组件
维护优化:
清晰标注哪些类属于父容器、哪些属于子容器
在多模块项目中,将父容器 Bean 放到独立的
core
模块,子容器 Bean 放到web
模块
6. 父子容器的源码解析
6.1 创建父容器:ContextLoaderListener
当 Web 容器启动时,ContextLoaderListener
会先调用:
public void contextInitialized(ServletContextEvent event) {initWebApplicationContext(event.getServletContext());
}
这里核心步骤:
创建
WebApplicationContext
(通常是XmlWebApplicationContext
或AnnotationConfigWebApplicationContext
)调用
configureAndRefreshWebApplicationContext()
设置配置文件位置
调用
refresh()
初始化所有 Bean
6.2 创建子容器:DispatcherServlet
DispatcherServlet
在 init()
方法中:
this.webApplicationContext = initWebApplicationContext();
initWebApplicationContext()
核心:
如果没有传入外部的
WebApplicationContext
,就自己创建一个调用
setParent(parentContext)
将父容器传进来调用
refresh()
初始化子容器 Bean
6.3 Bean 查找的向上链路
当你在子容器中调用:
ctx.getBean("xxx");
执行流程:
在子容器的
beanFactory
查找 BeanDefinition如果没找到,就调用:
if (this.parent != null) {return this.parent.getBean(name, requiredType); }
这样会递归向父容器查找,直到顶层容器或抛出异常
6.4 时序图(简化版)
[Servlet 容器启动]↓
ContextLoaderListener -------------------------------| 创建父容器| refresh() 父容器↓
DispatcherServlet -----------------------------------| 创建子容器| setParent(父容器)| refresh() 子容器↓
运行中:getBean() → 子容器→ 父容器→ 祖先容器...
6.5 为什么理解调用链很重要
调试问题
当出现 "NoSuchBeanDefinitionException" 时,你能立刻判断是子容器没扫描到,还是父容器没加载性能优化
你知道哪些 Bean 会被多个子容器共享,就应该放到父容器避免重复初始化扩展能力
可以自己写WebApplicationInitializer
精准控制父子容器的生命周期