当前位置: 首页 > news >正文

Spring MVC 父子容器深度解析:原理、实战与优化

1. 父子容器的定义与设计初衷

一句话总结:父子容器的核心价值在于解耦 Web 层与业务层,实现职责分离与上下文隔离。

1.1 父子容器的层次关系

Spring MVC 中,容器分为两类:

  • 父容器(Root ApplicationContext):由 ContextLoaderListener 创建,主要存放 Service、DAO、事务管理器、数据源 等业务相关 Bean。

  • 子容器(WebApplicationContext):由 DispatcherServlet 创建,主要存放 Controller、HandlerMapping、ViewResolver 等 Web 层相关 Bean。

层级关系描述(文字版流程图)

  1. Tomcat 启动,ContextLoaderListener 初始化父容器。

  2. 父容器加载全局 Bean(数据源、事务管理器、业务 Service)。

  3. DispatcherServlet 初始化子容器,并将父容器引用传递给它。

  4. 子容器加载 Web 层 Bean(Controller、ViewResolver)。

  5. 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(父容器)加载流程

源码入口:ContextLoaderListenercontextInitialized()initWebApplicationContext()

流程

  1. 创建 WebApplicationContext 实例(默认 XmlWebApplicationContext)。

  2. contextConfigLocation 读取父容器配置文件。

  3. 调用 refresh() 完成 Bean 加载和初始化。

  4. 将父容器放入 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(子容器)加载流程

源码入口:DispatcherServletinitServletBean()initWebApplicationContext()

流程

  1. 创建子容器 WebApplicationContext

  2. contextConfigLocation 读取子容器配置文件。

  3. 调用 setParent() 将父容器引用传入。

  4. 调用 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() 查找规则:

  1. 先在当前容器查找 Bean。

  2. 如果找不到且有父容器,则向父容器递归查找。

  3. 找不到则抛出 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,最终都会走到这里。

核心流程(文字版调用链)

  1. prepareRefresh() — 准备环境变量、校验配置文件、记录启动时间。

  2. obtainFreshBeanFactory() — 创建或刷新 BeanFactory 实例(DefaultListableBeanFactory)。

  3. prepareBeanFactory(beanFactory) — 注册默认的 BeanPostProcessor、环境变量、依赖解析器等。

  4. postProcessBeanFactory(beanFactory) — 模板方法,允许子类扩展(如 AbstractRefreshableWebApplicationContext 会在这里注册 Web 相关 Bean)。

  5. invokeBeanFactoryPostProcessors(beanFactory) — 执行 BeanFactoryPostProcessor(如 ConfigurationClassPostProcessor 解析 @Configuration@ComponentScan)。

  6. registerBeanPostProcessors(beanFactory) — 注册所有 BeanPostProcessor(AOP、@Autowired 等依赖注入的关键)。

  7. initMessageSource() — 初始化国际化资源。

  8. initApplicationEventMulticaster() — 初始化事件广播器。

  9. onRefresh() — 模板方法,Spring MVC 子容器会在这里初始化 HandlerMappingHandlerAdapter 等。

  10. registerListeners() — 注册所有事件监听器。

  11. finishBeanFactoryInitialization(beanFactory) — 实例化所有非懒加载单例 Bean。

  12. 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)

启动时:

  1. ContextLoaderListener 调用 refresh() 完成父容器初始化,数据源和事务管理器就绪。

  2. DispatcherServlet 调用 refresh() 初始化子容器,Controller 中通过 @Autowired 获取 Service。

  3. 由于子容器的 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(父容器)会被 ContextLoaderListenerinitWebApplicationContext() 中读取,然后传给 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 空间不冲突,建议:

  1. 业务层包路径com.example.service, com.example.dao

  2. Web 层包路径com.example.web

  3. 父容器的 @ComponentScan 不要扫描 com.example.web

  4. 子容器的 @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 子容器:AdminControllerAdminInterceptor

  • portal 子容器:PortalControllerPortalInterceptor

好处:

  • Service 和 DAO 只加载一次,节省内存

  • 控制器互不干扰,职责分离


场景 2:多 DispatcherServlet 的多语言站点

  • /en/* → English 子容器

  • /cn/* → Chinese 子容器

  • 公共业务逻辑放在父容器


4.2 事务管理器必须放在父容器的原因

原因:事务管理器通常会在 Service 层通过 @Transactional 生效,而 @Transactional 的底层依赖于 AOP 代理,代理对象的生成需要在 业务 Bean 初始化阶段完成。
如果事务管理器放在子容器:

  1. 父容器初始化 Service 时找不到事务管理器 Bean(因为父容器无法向下访问子容器)。

  2. 事务增强器 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 名称冲突问题。

解决策略

  1. 命名空间法

    • 父容器所有 Bean 以 core* 开头

    • 子容器 Bean 以 web* 开头
      这样在注入时几乎不会冲突

  2. @Primary
    标记优先被注入的 Bean

    @Bean
    @Primary
    public ProductService newProductService() { ... }
    
  3. @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());
}

这里核心步骤:

  1. 创建 WebApplicationContext(通常是 XmlWebApplicationContextAnnotationConfigWebApplicationContext

  2. 调用 configureAndRefreshWebApplicationContext()

    • 设置配置文件位置

    • 调用 refresh() 初始化所有 Bean


6.2 创建子容器:DispatcherServlet

DispatcherServletinit() 方法中:

this.webApplicationContext = initWebApplicationContext();

initWebApplicationContext() 核心:

  1. 如果没有传入外部的 WebApplicationContext,就自己创建一个

  2. 调用 setParent(parentContext) 将父容器传进来

  3. 调用 refresh() 初始化子容器 Bean


6.3 Bean 查找的向上链路

当你在子容器中调用:

ctx.getBean("xxx");

执行流程:

  1. 在子容器的 beanFactory 查找 BeanDefinition

  2. 如果没找到,就调用:

    if (this.parent != null) {return this.parent.getBean(name, requiredType);
    }
    
  3. 这样会递归向父容器查找,直到顶层容器或抛出异常


6.4 时序图(简化版)

[Servlet 容器启动]↓
ContextLoaderListener -------------------------------| 创建父容器| refresh() 父容器↓
DispatcherServlet -----------------------------------| 创建子容器| setParent(父容器)| refresh() 子容器↓
运行中:getBean() → 子容器→ 父容器→ 祖先容器...

6.5 为什么理解调用链很重要

  • 调试问题
    当出现 "NoSuchBeanDefinitionException" 时,你能立刻判断是子容器没扫描到,还是父容器没加载

  • 性能优化
    你知道哪些 Bean 会被多个子容器共享,就应该放到父容器避免重复初始化

  • 扩展能力
    可以自己写 WebApplicationInitializer 精准控制父子容器的生命周期

http://www.lryc.cn/news/613983.html

相关文章:

  • Pytest项目_day09(skip、skipif跳过)
  • iOS 签名证书全流程详解,申请、管理与上架实战
  • 三方相机问题分析七:【datespace导致GPU异常】facebook 黑块和Instagram花图问题
  • 【性能测试】-2- JMeter工具的使用
  • 网吧在线选座系统|基于java和小程序的网吧在线选座小程序系统设计与实现(源码+数据库+文档)
  • 【Jmeter】设置线程组运行顺序的方法
  • Baumer相机如何通过YoloV8深度学习模型实现危险区域人员的实时检测识别(C#代码UI界面版)
  • 利用千眼狼sCMOS相机开展冷离子云成像与测量实验
  • 平板探测器的主要技术指标
  • Spring Boot 优雅配置InfluxDB3客户端指南:@Configuration + @Bean + yml实战
  • C# 异步编程(GUI程序中的异步操作)
  • 从浅拷贝到深拷贝:C++赋值运算符重载的核心技术
  • 【设计模式】抽象工厂模式 (工具(Kit)模式)
  • 【接口自动化】-2- request模块及通过变量实现接口关联
  • 瑞利杂波背景下不同环境的虚警概率与目标检测概率仿真
  • 项目历程—右键菜单(问题,解决,拓展(非教学向,因为乱))
  • django uwsgi启动报错failed to get the Python codec of the filesystem encoding
  • 17.14 CogVLM-17B多模态模型爆肝部署:4-bit量化+1120px高清输入,A100实战避坑指南
  • 流形折叠与条件机制
  • 【ee类保研面试】其他类---计算机网络
  • STM32HAL 快速入门(二):用 CubeMX 配置点灯程序 —— 从工程生成到 LED 闪烁
  • 如何在Vue中使用拓扑图功能
  • 相机坐标系与世界坐标系的点相互转换:原理、可视化与实践
  • HTML 与 CSS:从 “认识标签” 到 “美化页面” 的入门指南
  • Numpy科学计算与数据分析:Numpy数据分析与图像处理入门
  • 使用Python提取PDF大纲(书签)完整指南
  • Date、Calendar、LocalDateTime:Java 处理时间的类该怎么选?
  • 【网络自动化】利用Python脚本与计划任务,实现H3C/HPE设备配置无人值守备份
  • 安装向量数据库chromadb
  • Java+uniapp+websocket实现实时聊天,并保存聊天记录