深入拆解SpringMvc父子容器构建全流程

102 阅读8分钟

思考,输出,沉淀。用通俗的语言陈述技术,让自己和他人都有所收获。
作者:毅航😜


    在使用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中来看,其容器初始化链路整体如下:

image.png

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;
}

其中之所以会执行FrameworkServletinitServletBean方法。这正是由于我们在web.xml 里面配置了DispatcherServlet缘故。

当加载DispatcherServlet时,会去调用DispatcherServletinit方法,由于DispatcherServletFrameworkServlet的继承关系,上述Servlet初始化的调用逻辑,最终会执行到FrameworkServlet内部initWebApplicationContext方法,这就是SpringMVC内部子容器的一个启动执行顺序。

但这里有一个很容易被忽视的点,很多人总是认为SpringMVC内部父子容器的设定在this.webApplicationContext != null这处分支逻辑中执行,但其实并不然。

造成这一错误认知的核心在于对于搞混了SpringBean信息的管理。很多人在翻阅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完成相关子容器构建,并实现父子容器的相互绑定。

image.png

最终,当SpringMVC中完成父子容器构建后,两者容器间的关系如下所示。其中,父容器作为全局根容器,负责管理 Service、Repository、数据源等非Web 层核心 Bean;子容器则归属于Spring MVC ,并专门管理 Controller、视图解析器等 WebBean

image.png

深究父子容器机制

至此,我们便讲清楚了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 模板、会话级配置等,仅作用于当前会话,且可单向访问父上下文的通用资源,父上下文无需感知子容器存在。

通过这种 “全局复用 + 会话隔离” 的架构设计,既能从根源避免不同会话的临时状态互相干扰,杜绝上下文串流与数据泄露风险,又能减少通用资源的重复加载,显著降低系统资源占用,提升大模型服务的稳定性与运行效率。