思考,输出,沉淀。用通俗的语言陈述技术,让自己和他人都有所收获。
作者:毅航😜
在使用
SpringMvc中,我们不可避免的总是会涉及到父子容器的相关概念,而一概念其实并非Spring所独创。这其实是软件架构中层级化资源管理的通用设计思想,Spring框架只是将这一思想落地到IOC容器体系中并形成了标准化实现。
前言
在学习了解SpringMVC的过程中,框架内容部通常会构建两个Bean容器,两个容器相互绑定,从而形成父子容器的概念。容器间借助单向可见的层级关系实现 Bean 职责的隔离与有序管理。
其中,父容器作为全局根容器,负责管理 Service、Repository、数据源等非 Web 层核心 Bean;而子容器则归属于 Spring MVC 核心组件 DispatcherServlet,专门管理 Controller、视图解析器等Web 层相关的Bean信息。
接下来,我们便从 SpringMVC源码的角度来深入剖析SpringMVC内部双容器的构建全流程。
SpringMVC容器构建流程
父容器构建
在使用SpringMVC时,通常我们会在web.xml文件中配置一个叫做ContextLoaderListener的监听器,具体配置如下。
<listener>
<listener-class>org.springframework.web.
context.ContextLoaderListener</listener-class>
</listener>
而ContextLoaderListener默认会实现ServletContextListener接口。而当 Tomcat启动并完成ServletContext上下文的创建后,会自动触发ServletContextEvent事件。
此时ContextLoaderListener作为监听器,会接收到相关事件并执行contextInitialized()回调方法;进而调用内部initWebApplicationContext()方法,从而完成启动并初始化Spring IoC。
(PS:有关ContextLoaderListener的介绍可参考:SpringMVC流程分析(一):从一行配置入手,搞懂web环境下Ioc容器的构建)
子容器构建
在梳理完 SpringMVC 父容器的构建脉络后,我们继续深入子容器的实现流程,延续前文的分析思路,我们还是从配置文件这一基础环节切入。在最开始接触SpringMVC时,对于DispatcherServlet的加载,我们通常通过在web.xml中配置如下内容:
<servlet>
<!-- Servlet 名称,自定义(需与下方 servlet-mapping 对应) -->
<servlet-name>dispatcherServlet</servlet-name>
<!-- DispatcherServlet 全类名(必选) -->
<servlet-class>org.springframework.web.servlet.DispatcherServlet
</servlet-class>
</servlet>
上述配置信息的解析借助Tomcat来完成。具体来看,当Tomcat启动并初始化 Web 应用时,会优先扫描解析应用的部署描述文件 web.xml,从中识别 <servlet> 标签定义,提取出Servlet的名称、全类名及初始化参数等核心配置信息。随后 Tomcat 内置的 ServletContainer 会根据解析到的类名,通过类加载器加载 DispatcherServlet 并完成实例化。
当相关Servlet完成加载后,Tomcat 会在启动阶段立即调用其 init() 方法完成初始化,init() 方法完成相关Servlet的初始化操作。具体到DispatcherServlet中来看,其容器初始化链路整体如下:
FrameworkServlet # initWebApplicationContext()
/**
* 初始化Web应用程序上下文
*
* @return 初始化后的Web应用程序上下文
*/
protected WebApplicationContext initWebApplicationContext() {
WebApplicationContext rootContext =
WebApplicationContextUtils.getWebApplicationContext(getServletContext());
WebApplicationContext wac = null;
//初始时,属性webApplicationContext为空,不会执行此处父子容器设定逻辑
if (this.webApplicationContext != null) {
// ...省略其他无关代码
}
}
// 检查是否已通过构造函数注入了上下文实例
if (wac == null) {
wac = findWebApplicationContext();
}
// 如果仍未找到上下文实例,则创建一个新的本地上下文
if (wac == null) {
wac = createWebApplicationContext(rootContext);
}
// ...省略其他无关代码
return wac;
}
其中之所以会执行FrameworkServlet的initServletBean方法。这正是由于我们在web.xml 里面配置了DispatcherServlet缘故。
当加载DispatcherServlet时,会去调用DispatcherServlet的init方法,由于DispatcherServlet与FrameworkServlet的继承关系,上述Servlet初始化的调用逻辑,最终会执行到FrameworkServlet内部initWebApplicationContext方法,这就是SpringMVC内部子容器的一个启动执行顺序。
但这里有一个很容易被忽视的点,很多人总是认为SpringMVC内部父子容器的设定在this.webApplicationContext != null这处分支逻辑中执行,但其实并不然。
造成这一错误认知的核心在于对于搞混了Spring中Bean信息的管理。很多人在翻阅SpringMVC源码时,看到FrameworkServlet会实现ApplicationContextAware接口,所以总会想当然的将认为当FrameworkServlet子类在初始化时,会默认将容器中的容器赋值到webApplicationContext。
回到我们开始有关DispatcherServlet的配置信息,你会发现,有关DispatcherServlet 的注入其实我们并没有借助Spring容器,而是借助Tomcat对于Servlet的解析来完成。
换言之,我们通过web.xml中配置的方式 ,让Tomcat识别到了DispatcherServlet信息,从而令其摆脱了Spring的管理。这也就使得其生命周期不被Spring所管理,进而相关Spring中的ApplicationContextAware埋点操作也就不会执行。
因此,在SpringMVC内部,相关父子容器的绑定其实是在wac == null这一分支条件中执行,即借助createWebApplicationContext完成相关SpringMVC子容器的构建。
FrameworkServlet # createWebApplicationContext()
protected WebApplicationContext createWebApplicationContext(@Nullable ApplicationContext parent) {
Class<?> contextClass = getContextClass();
if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {
throw new ApplicationContextException(
"Fatal initialization error in servlet with name '" + getServletName() +
"': custom WebApplicationContext class [" + contextClass.getName() +
"] is not of type ConfigurableWebApplicationContext");
}
ConfigurableWebApplicationContext wac =
(ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);
wac.setEnvironment(getEnvironment());
wac.setParent(parent);
String configLocation = getContextConfigLocation();
if (configLocation != null) {
wac.setConfigLocation(configLocation);
}
configureAndRefreshWebApplicationContext(wac);
return wac;
}
总结来看,在SpringMVC内部,整个父子容器构建逻辑如下图所示。首先,ContextLoaderListner监听到Tomcat容器启动事件后,完成父容器的构建;待进行到相关Servlet初始化阶段后,DispatcherServlet完成相关子容器构建,并实现父子容器的相互绑定。
最终,当SpringMVC中完成父子容器构建后,两者容器间的关系如下所示。其中,父容器作为全局根容器,负责管理 Service、Repository、数据源等非Web 层核心 Bean;子容器则归属于Spring MVC ,并专门管理 Controller、视图解析器等 Web 层 Bean。
深究父子容器机制
至此,我们便讲清楚了SpringMVC中父子容器的构建全过程,但你可能会疑惑,为什么Spring在构建时,会通过双容器的机制来完成容器加载呢?
其实,之所以的采用父子容器的实现的本质,无非就是进行分层。从而解决单一容器管理所有 Bean 时易出现的职责混编、加载冲突、功能失效等问题。
-
避免功能失效。 若不拆分父子容器,
DispatcherServlet初始化时会与ContextLoaderListener重复扫描并创建Service等业务层Bean。子容器重复创建的Bean则无法关联到事务管理器,直接导致@Transactional注解完全失效,业务层事务控制形同虚设。 -
降低加载冲突。在单一容器中若出现同名
Bean(如容器中如果出现同名Bean会直接致使项目无法正常加载。而父子容器的 “单向可见性”(子容器可访问父容器Bean,父容器无法感知子容器Bean)避免了子容器Bean污染父容器,进而且明确的职责拆分让Bean管理的边界清晰可追溯。同时更能有效解决多DispatcherServlet实例的配置冲突问题。多模块项目中若配置多个DispatcherServlet,单一容器会导致不同DispatcherServlet的配置互相干扰。父子容器架构下,每个DispatcherServlet拥有独立的子容器,可配置专属的Bean信息,同时共享父容器的核心业务层Bean,既保证了多Servlet实例的隔离性,又实现了核心资源的复用。
进一步来看,SpringMVC内部父子容器思想的本质便是软件架构中 “层级化资源管理” 的通用设计思想,而Spring 只是将这一思想落地到 IoC 容器体系中并形成了标准化实现。从技术本质来看,只要涉及 “分层隔离、单向依赖、资源复用” 的场景,都会衍生出类似父子容器的设计。例如,操作系统中父子进程的资源隔离,以及 前端Vue框架中父子组件的关系,其本质都是该思想的拓展。
总结
好的设计往往具备共通的核心逻辑,回到如今的大模型开发场景,资源隔离是其面临的典型难题。具体来看,大模型的交互过程中,不同用户、不同任务的上下文必须严格区分。
例如用户 A的对话历史、会话配置(如温度参数、角色设定)与用户 B的相关内容需完全隔离,不能出现混淆。若未做好隔离设计,不仅可能导致用户 A的对话内容被用户 B意外获取,还会造成前序任务的上下文污染后续任务的推理结果,严重影响使用安全性与推理准确性。
针对这一痛点,我们可借鉴 Spring MVC 父子容器的分层设计思路来优化。具体而言,可构建 “父 - 子” 两级上下文容器,实现资源隔离与高效复用:
- 父上下文(全局容器) :存储所有会话共享的通用资源,包括模型基础参数、全局通用知识库、统一权限规则等,一次初始化后供所有会话调用;
- 子上下文(会话容器) :仅存储单一会话的临时状态,比如用户专属对话历史、个性化
Prompt模板、会话级配置等,仅作用于当前会话,且可单向访问父上下文的通用资源,父上下文无需感知子容器存在。
通过这种 “全局复用 + 会话隔离” 的架构设计,既能从根源避免不同会话的临时状态互相干扰,杜绝上下文串流与数据泄露风险,又能减少通用资源的重复加载,显著降低系统资源占用,提升大模型服务的稳定性与运行效率。