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
精準控制父子容器的生命周期