概述
衔接前文: 前文已深入剖析了 Spring IoC 容器的启动流程与 Bean 生命周期,以及 Spring Boot 的自动配置与嵌入式容器机制。在 Web 环境下,Spring 通过 DispatcherServlet 将 HTTP 请求接入 IoC 容器,并利用父子容器实现 Web 层与业务层的组件隔离。本文将揭示 DispatcherServlet 如何被初始化、父子容器如何被创建并协同工作,以及 Spring Boot 如何让这一切变得更简单。
总结性引言: Spring MVC 是 Spring 在 Web 领域的核心框架,而 DispatcherServlet 则是它的中枢神经。从传统 web.xml 中的 <servlet> 配置,到 Servlet 3.0 的无 web.xml 启动,再到 Spring Boot 的嵌入式自动化,DispatcherServlet 的启动方式不断演变,但其内部逻辑始终保持一致:它创建一个专属于 Web 层的子容器,并与业务层、数据层的根容器形成父子关系,实现组件的分层隔离。本文将正面拆解 DispatcherServlet 的初始化流程,从 ContextLoaderListener 到 FrameworkServlet,再到 DispatcherServlet.onRefresh(),完整展现 Spring MVC 的启动全景,并结合父子容器的设计意图,分析其在实际开发中的潜在陷阱与最佳实践。
核心要点:
- 父子容器体系 :
ContextLoaderListener创建根容器(Root WebApplicationContext),DispatcherServlet创建子容器(Servlet WebApplicationContext),形成层次化上下文。 - 初始化流程 :
DispatcherServlet.init()→FrameworkServlet.initWebApplicationContext()创建或查找子容器 →onRefresh()初始化 Spring MVC 九大策略组件。 - 策略组件的初始化:采用"默认+可覆盖"的策略,即"发现即使用",体现了 IoC 容器的扩展性与模板方法模式。
- Spring Boot 的简化 :默认使用单一
AnnotationConfigServletWebServerApplicationContext容器,消除父子容器层次,并通过自动配置注册DispatcherServlet。
文章组织架构图
架构图说明:
- 总览说明:全文 8 个模块遵循从传统 Servlet 与 Spring 的桥接开始,逐步深入到根容器创建、子容器初始化、策略组件加载,再对比 Spring Boot 的现代简化模型,最后通过生产事故排查和面试专题完成从理论到实践的闭环。
- 逐模块说明 :
- 模块 1 建立整体认知,厘清 Servlet 容器与 Spring IoC 容器如何通过
ServletContext桥接。 - 模块 2-3 详细解剖两大容器的创建过程与
DispatcherServlet的初始化全流程。 - 模块 4 深入
onRefresh()方法内部,揭示九大策略组件如何利用 IoC 容器实现"策略模式"。 - 模块 5 分析父子容器间的 Bean 访问与注入规则,为排查问题打下理论基础。
- 模块 6 对比 Spring Boot 如何通过自动配置与单一容器优雅地解决传统部署模型的复杂性。
- 模块 7-8 将理论落地到生产实践,解决常见事故并应对高频面试。
- 模块 1 建立整体认知,厘清 Servlet 容器与 Spring IoC 容器如何通过
- 关键结论 :父子容器是 Spring MVC 实现分层隔离的核心设计,理解其创建时机和访问规则,是排查"Controller 无法注入 Service"等问题的关键。
1. Spring MVC 启动总览:Servlet 容器与 Spring 容器的桥接
Spring MVC 并非一个独立运行的应用,它必须依赖 Servlet 容器(如 Tomcat、Jetty)来接收和响应 HTTP 请求。理解 Spring MVC 的启动,首先要理解它是如何与 Servlet 容器这一底层基础设施深度集成的。
1.1 Servlet 规范的核心角色
Servlet 3.1+ 规范定义了三个核心角色,它们共同构成了 Java Web 应用的运行时环境:
ServletContext:一个 Web 应用对应一个唯一的ServletContext。它是整个 Web 应用的全局上下文,可以被所有 Servlet 和 Filter 共享。它提供了获取应用初始化参数、获取资源路径、在组件间共享数据的能力。对于 Spring 而言,ServletContext是挂载根 Spring 容器的理想位置。ServletContextListener:这是ServletContext的生命周期监听器。当 Servlet 容器启动,ServletContext被创建时,会调用所有注册的ServletContextListener的contextInitialized(ServletContextEvent sce)方法;当应用关闭时,调用contextDestroyed(ServletContextEvent sce)。它为 Spring 容器的启动和销毁提供了生命周期钩子。Servlet:用于处理请求和生成响应。一个 Servlet 通常对应一套请求处理逻辑。DispatcherServlet就是一个特殊的 Servlet,它作为前端控制器(Front Controller),负责接收所有进入应用的请求,并分发给合适的处理器。
这三者的关系清晰地定义了 Spring MVC 的生命周期边界:Spring IoC 容器的生命周期必须依附于 Servlet 容器的生命周期。
1.2 Spring 的桥接者:ContextLoaderListener 与 DispatcherServlet
为了让 Spring 的 IoC 容器与 Servlet 容器共生,Spring 设计了两个关键组件作为桥梁:
-
ContextLoaderListener:它实现了ServletContextListener。在 Web 应用启动时,它负责创建 Spring 的根(Root)WebApplicationContext 。这个根容器通常管理着业务逻辑、数据访问、事务等中间层和后端层的 Bean。创建完成后,它会将根容器作为属性存入ServletContext中,使整个 Web 应用都可以访问到它。 -
DispatcherServlet:它继承自HttpServlet,是 Spring MVC 的核心。每个DispatcherServlet实例在初始化时,都会创建自己专属的子(Servlet)WebApplicationContext 。它会自动找到存于ServletContext中的根容器,并将其设置为自己的父容器。这个子容器专注于 Web 层的 Bean,例如 Controller、ViewResolver、HandlerMapping 等。
1.3 整体启动时序
一个典型的 Spring MVC 应用启动时序如下:
- Servlet 容器(如 Tomcat)启动,解析
web.xml或扫描ServletContainerInitializer。 - 容器触发
ContextLoaderListener.contextInitialized()回调。 ContextLoaderListener创建并刷新Root WebApplicationContext。- 容器初始化
DispatcherServlet。 DispatcherServlet.init()被调用,在其内部:- 找到已存在的 Root WebApplicationContext 作为父容器。
- 创建并刷新Servlet WebApplicationContext(子容器)。
- 在
onRefresh()中初始化九大策略组件。
- 应用启动完成,等待处理请求。
1.4 父子容器层次结构图
这张图清晰地展示了 Bean 在两层容器中的分布和关系。
图表主旨概括 :此图展示了 Spring MVC 父子容器层次结构的核心模型,直观地显示了 ServletContext、Root WebApplicationContext 和 Servlet WebApplicationContext 三者的包含与关联关系,以及各自管辖的典型 Bean 类型。
逐层/逐元素分解:
ServletContext层 :作为顶层全局上下文,它不直接管理 Bean,但持有对根容器和(通常通过DispatcherServlet间接)对子容器的引用,是整个体系的基础设施。- Root WebApplicationContext 层 :包含
DataSource、Service、TransactionManager等应用基础服务和业务逻辑 Bean。它们与 Web 层解耦,可以被任何 Web 上下文或定时任务等非 Web 场景共享。 - Servlet WebApplicationContext 层 :包含
Controller、HandlerMapping、ViewResolver等 Web 层专属 Bean。这些 Bean 高度依赖 Servlet API,且通常与某个具体的DispatcherServlet绑定。
设计原理映射:
- 分层隔离原则 :通过容器层次实现了关注点分离(Separation of Concerns),将 Web 层(展现层)与业务层、数据层强制解耦。
- 组合模式 :
ApplicationContext的层次结构是组合模式的一种体现,一个应用上下文可以包含另一个作为其父上下文,形成一个树形结构,统一了对 Bean 的访问方式。
工程联系与关键结论 :子容器可以透明地注入父容器中的任何 Bean,因为它们共享同一个祖先。反之,父容器无法感知到子容器中的 Bean。 这种设计保证了通用业务服务可以被多个 Web 上下文共享,同时避免了 Web 层的特定实现污染核心业务逻辑。
2. Root 容器的创建:ContextLoaderListener 与 ContextLoader
根容器的创建是整个 Spring MVC 启动的起点。ContextLoaderListener 和其委托类 ContextLoader 扮演了关键角色。
2.1 ContextLoaderListener 的实现与触发
ContextLoaderListener 是一个标准的 ServletContextListener 实现。当 Servlet 容器启动并触发 contextInitialized 事件时,它便开始工作。
java
// org.springframework.web.context.ContextLoaderListener
public class ContextLoaderListener extends ContextLoader implements ServletContextListener {
public ContextLoaderListener() {
}
public ContextLoaderListener(WebApplicationContext context) {
super(context);
}
// ServletContextListener 的入口方法
@Override
public void contextInitialized(ServletContextEvent event) {
// 委托给父类 ContextLoader 的 initWebApplicationContext 方法
initWebApplicationContext(event.getServletContext());
}
@Override
public void contextDestroyed(ServletContextEvent event) {
closeWebApplicationContext(event.getServletContext());
ContextCleanupListener.cleanupAttributes(event.getServletContext());
}
}
源码解读:
ContextLoaderListener继承自ContextLoader,这种设计将监听器定义与具体的容器创建逻辑分离开,体现了单一职责原则。contextInitialized方法简单地委托给initWebApplicationContext,传入ServletContext实例。这标志着 Spring 开始接管 Web 应用的空间。
2.2 ContextLoader.initWebApplicationContext() 的容器创建逻辑
ContextLoader 是干重活的。它的 initWebApplicationContext 方法负责决定创建何种类型的 ApplicationContext,加载哪些配置,并最终刷新容器。
java
// org.springframework.web.context.ContextLoader
public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
// 1. 检查 ServletContext 中是否已有根容器,确保唯一性
if (servletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE) != null) {
throw new IllegalStateException(
"Cannot initialize context because there is already a root application context present - " +
"check whether you have multiple ContextLoader* definitions in your web.xml!");
}
// ... 日志记录 ...
long startTime = System.currentTimeMillis();
try {
// 2. 尝试先从 ServletContext 中获取构造函数传入的或已存在的 ApplicationContext
if (this.context == null) {
this.context = createWebApplicationContext(servletContext);
}
// 3. 确保它是一个 ConfigurableWebApplicationContext 实例
if (this.context instanceof ConfigurableWebApplicationContext) {
ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) this.context;
if (!cwac.isActive()) {
// 4. 如果容器尚未刷新(刚创建),则设置并加载父容器
if (cwac.getParent() == null) {
// 设置一个可能存在的父上下文,但通常为 null
ApplicationContext parent = loadParentContext(servletContext);
cwac.setParent(parent);
}
// 5. 配置并刷新容器
configureAndRefreshWebApplicationContext(cwac, servletContext);
}
}
// 6. 将创建并刷新好的根容器放入 ServletContext 的全局属性中
servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);
// ... 日志和返回值 ...
return this.context;
}
catch (RuntimeException | Error ex) {
// ... 异常处理 ...
throw ex;
}
}
源码解读:
- 唯一性检查 :方法开头就检查
ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE,确保一个 Web 应用中只有一个根容器,这是结构稳定性的保障。 - 创建 WebApplicationContext :
createWebApplicationContext(servletContext)方法根据contextClass参数来决定具体的 ApplicationContext 类型。默认情况下,如果使用传统 XML 配置,是XmlWebApplicationContext;如果使用注解驱动,可以通过<context-param>指定为AnnotationConfigWebApplicationContext。 configureAndRefreshWebApplicationContext:这是核心配置方法,它会设置ServletContext、读取contextConfigLocation(如WEB-INF/applicationContext.xml或配置类全限定名),然后调用refresh()方法。此refresh()方法,正是我们前文 IoC 容器篇所详细剖析的入口,它会触发 BeanDefinition 加载、Bean 实例化、后置处理器执行等一系列流程。此举标志着根容器中所有配置的业务 Bean 正式完成初始化。- 全局存储 :最后,
servletContext.setAttribute(...)将根容器放入一个全局可见的"篮子"里,供后续DispatcherServlet初始化时获取。
2.3 根容器的典型配置
在传统 web.xml 中,根容器的配置如下:
xml
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/applicationContext.xml</param-value>
</context-param>
在 无 web.xml 时代 (参见 2.4 节),也可以通过编程方式指定,此处先不展开。无论是哪种方式,其目的都是告诉 ContextLoader:请创建一个容器,并从这些配置文件中加载 Bean 。通常,我们会将 @ComponentScan 的扫描范围限定在 Service、Repository 等非 Web 组件上。
3. DispatcherServlet 的初始化与子容器创建
当根容器准备就绪后,DispatcherServlet 的初始化过程便开始了。这是启动全景中最精妙的部分。
3.1 init() 方法的调用链:从 HttpServletBean 到 FrameworkServlet
Servlet 的初始化从 init(ServletConfig config) 方法开始。在 Spring 中,这个调用链被精心设计,每个父类都负责一部分职责。
-
HttpServletBean.init():继承自HttpServlet的HttpServletBean覆写了init()方法。它的主要职责是将ServletConfig中的初始化参数(init-param)解析并设置到当前 Servlet 实例的属性上(例如,<init-param>中配置的contextConfigLocation)。之后,它调用initServletBean()模板方法,将流程移交给子类。 -
FrameworkServlet.initServletBean():FrameworkServlet是DispatcherServlet的父类,它实现了initServletBean()。这个方法的核心任务就是创建或初始化其专属的 WebApplicationContext ,并调用initWebApplicationContext()。 -
FrameworkServlet.initWebApplicationContext():这是最关键的方法,它完成了父子容器的关联。
3.2 子容器的核心创建与关联逻辑
FrameworkServlet.initWebApplicationContext() 方法实现了查找或创建子容器,并与根容器建立父子关系的完整逻辑。
java
// org.springframework.web.servlet.FrameworkServlet
protected WebApplicationContext initWebApplicationContext() {
// 1. 从 ServletContext 中获取根容器(此根容器由 ContextLoaderListener 创建)
WebApplicationContext rootContext =
WebApplicationContextUtils.getWebApplicationContext(getServletContext());
WebApplicationContext wac = null;
// 2. 如果构造函数中已经注入了 WebApplicationContext(例如在测试或编程式注册时),则直接使用
if (this.webApplicationContext != null) {
wac = this.webApplicationContext;
if (wac instanceof ConfigurableWebApplicationContext) {
ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac;
if (!cwac.isActive()) {
if (cwac.getParent() == null) {
// 设置根容器为父容器
cwac.setParent(rootContext);
}
configureAndRefreshWebApplicationContext(cwac);
}
}
}
// 3. 否则,尝试从 ServletContext 中查找已存在的子容器
if (wac == null) {
wac = findWebApplicationContext();
}
// 4. 如果仍未找到,则创建一个全新的子容器
if (wac == null) {
wac = createWebApplicationContext(rootContext);
}
// 5. 触发 onRefresh 钩子方法
if (!this.refreshEventReceived) {
synchronized (this.onRefreshMonitor) {
onRefresh(wac);
}
}
// 6. 将此子容器也放入 ServletContext 的属性中,以便其他组件通过 Servlet 名称找到它
if (this.publishContext) {
String attrName = getServletContextAttributeName();
getServletContext().setAttribute(attrName, wac);
}
return wac;
}
源码解读:
- 获取根容器 :
WebApplicationContextUtils.getWebApplicationContext(getServletContext())就是从ServletContext中取出之前ContextLoaderListener存进去的根容器。这是父子关系建立的基础。 - 子容器创建 :
createWebApplicationContext(rootContext)方法内部,首先实例化一个XmlWebApplicationContext或AnnotationConfigWebApplicationContext,然后调用cwac.setParent(rootContext)建立父子关系。这一步至关重要。 - 配置并刷新 :
configureAndRefreshWebApplicationContext(cwac)同样会处理contextConfigLocation(这个配置来自于DispatcherServlet自己的<init-param>或编程式配置),并最终调用refresh()方法。这一次refresh(),会初始化子容器中所有的 Web 组件 Bean。 onRefresh(wac)钩子 :这是FrameworkServlet留给DispatcherServlet的入口。此时,子容器中的所有自定义 Bean 都已初始化完毕,DispatcherServlet可以安全地基于这些 Bean 来初始化自己的九大策略组件。这是典型的模板方法模式应用。- 双重存储 :子容器创建后,也会以其 Servlet 名称为 Key 存入
ServletContext。这使得可能存在多个DispatcherServlet实例时,每个都能被区分。
3.3 DispatcherServlet 初始化序列图
此序列图详细展示了从 Servlet 容器触发到策略组件初始化的完整交互过程。
图表主旨概括 :该序列图清晰地描绘了 DispatcherServlet 初始化过程中,从 init() 入口到九大策略组件初始化的完整时序,重点突出了父子容器关联的关键步骤(步骤 6-10)和 onRefresh 钩子的触发时机。
逐层/逐元素分解:
- 步骤 1-3(启动与前置处理) :
Servlet容器启动初始化流程,HttpServletBean作为"管家"处理配置参数,并通过initServletBean()将控制权交给FrameworkServlet。 - 步骤 5-11(父子容器创建与刷新) :这是交互的核心。
FrameworkServlet主动查找根容器,创建子容器并建立父子关系,然后刷新子容器。这个过程保证了在步骤 12 执行时,子容器中的 Bean 已经就绪。 - 步骤 12-13(策略组件初始化) :
FrameworkServlet调用DispatcherServlet覆写的onRefresh()方法,启动 MVC 专属策略组件的发现和初始化流程。
设计原理映射:
- 模板方法模式 :整个初始化流程是模板方法模式的典范。
HttpServletBean.init()定义了骨架(initServletBean()就是一个模板方法),FrameworkServlet和DispatcherServlet分别实现特定的步骤。 - 依赖倒置原则 :
FrameworkServlet不关心DispatcherServlet具体要初始化哪些组件,它只定义了一个onRefresh抽象钩子,将具体实现推迟给子类。这使得框架具有极高的扩展性。
工程联系与关键结论 :onRefresh() 方法是连接子容器 Bean 就绪与 MVC 策略组件初始化的关键桥梁。任何需要在 DispatcherServlet 启动时执行的初始化逻辑,都可以通过扩展此方法或 initStrategies 方法来实现。 同时,如果在 onRefresh 执行前,子容器中的 Controller 或配置 Bean 出现问题,将直接导致 refresh() 失败,DispatcherServlet 无法启动。
4. onRefresh:九大策略组件的初始化
DispatcherServlet.onRefresh() 是所有 Spring MVC 组件的总调度入口。它将"初始化"这一行为,转化为从 IoC 容器中"发现"对应类型的 Bean。
4.1 onRefresh() 与 initStrategies() 源码解读
onRefresh 方法直接调用了 initStrategies,将初始化具体策略的细节封装在里面。
java
// org.springframework.web.servlet.DispatcherServlet
@Override
protected void onRefresh(ApplicationContext context) {
initStrategies(context);
}
protected void initStrategies(ApplicationContext context) {
initMultipartResolver(context);
initLocaleResolver(context);
initThemeResolver(context);
initHandlerMappings(context);
initHandlerAdapters(context);
initHandlerExceptionResolvers(context);
initRequestToViewNameTranslator(context);
initViewResolvers(context);
initFlashMapManager(context);
}
设计意图 :这种设计非常巧妙。它将每个策略组件的初始化过程解耦为一个独立的 init* 方法,使得整个流程清晰、易于阅读和调试。同时,它也为后续的组件扩展提供了明确的入口点。
4.2 策略组件的核心初始化模式:"发现即使用"
我们以 initHandlerMappings 为例,来剖析这个经典的初始化模式。其他所有 init* 方法都遵循几乎完全相同的逻辑。
java
// org.springframework.web.servlet.DispatcherServlet
private void initHandlerMappings(ApplicationContext context) {
this.handlerMappings = null;
// 1. 是否启用自动发现(默认为 true)
if (this.detectAllHandlerMappings) {
// 2. 从容器(包括父容器)中查找所有 HandlerMapping 类型的 Bean
Map<String, HandlerMapping> matchingBeans =
BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false);
if (!matchingBeans.isEmpty()) {
this.handlerMappings = new ArrayList<>(matchingBeans.values());
// 3. 对找到的 HandlerMapping 进行排序
AnnotationAwareOrderComparator.sort(this.handlerMappings);
}
}
// 4. 如果未找到任何用户自定义的 Bean,或者未启用自动发现,则加载默认策略
if (this.handlerMappings == null) {
this.handlerMappings = getDefaultStrategies(context, HandlerMapping.class);
if (logger.isTraceEnabled()) {
logger.trace("No HandlerMappings declared for servlet '" + getServletName() +
"': using default strategies from DispatcherServlet.properties");
}
}
}
源码解读:
detectAllHandlerMappings:这是一个标志位,默认为true。它赋予了开发者选择权:是自动发现所有,还是只查找特定的一个。BeanFactoryUtils.beansOfTypeIncludingAncestors:这是层次性容器带来的便利。它不仅在子容器中查找,还会在父容器中查找 。例如,如果开发者在根容器中定义了一个全局的HandlerMapping,它也会被发现。当然,排序规则会保证子容器中的 Bean 通常先于父容器中的 Bean。getDefaultStrategies:如果没有发现任何 Bean,DispatcherServlet也不会报错。它会从类路径下的DispatcherServlet.properties文件中读取默认策略实现。例如,HandlerMapping的默认实现是BeanNameUrlHandlerMapping和RequestMappingHandlerMapping。
4.3 九大策略组件一览
HandlerMapping:将请求映射到处理器(Handler)和拦截器(Interceptor)。默认实现:RequestMappingHandlerMapping。HandlerAdapter:帮助DispatcherServlet调用任何类型的处理器。默认实现:RequestMappingHandlerAdapter。HandlerExceptionResolver:解析处理器执行过程中抛出的异常,映射到错误视图或错误响应。默认实现:ExceptionHandlerExceptionResolver等。ViewResolver:将逻辑视图名解析为具体的视图实例(如 JSP 或 Thymeleaf 模板)。默认实现:InternalResourceViewResolver。LocaleResolver:解析客户端的区域信息,实现国际化。ThemeResolver:解析 Web 应用的主题。MultipartResolver:处理文件上传请求。FlashMapManager:管理 Flash 属性,用于重定向时的参数传递。RequestToViewNameTranslator:在没有明确返回视图名时,根据请求自动推断默认视图名。
设计思想 :这种"发现在先,默认兜底"的机制,是策略模式 在 Spring IoC 容器中的完美运用。每个策略组件都是一个抽象接口,其具体实现由容器管理。开发者要扩展一个组件,只需创建一个实现了相应接口的 Bean 并注册到子容器中。DispatcherServlet 在启动时"发现"它并自动集成,无需修改任何 Spring 框架核心代码。这正是 IoC 和 DI 带来的强大扩展性。
5. 父子容器的 Bean 访问规则
理解父子容器间的 Bean 访问规则是避免生产问题的关键。
5.1 层次性 BeanFactory 的查找逻辑
AbstractBeanFactory 中的 doGetBean 方法是所有依赖查找的起点。
java
// org.springframework.beans.factory.support.AbstractBeanFactory
protected <T> T doGetBean(final String name, @Nullable final Class<T> requiredType,
@Nullable final Object[] args, boolean typeCheckOnly) throws BeansException {
final String beanName = transformedBeanName(name);
Object bean;
// 1. 首先从当前容器的单例缓存中获取
Object sharedInstance = getSingleton(beanName);
if (sharedInstance != null && args == null) {
// ... 返回 Bean ...
}
else {
// ... 处理原型作用域、depends-on 等 ...
// 2. 获取父 BeanFactory
BeanFactory parentBeanFactory = getParentBeanFactory();
if (parentBeanFactory != null && !containsBeanDefinition(beanName)) {
// 3. 如果当前容器没有此 Bean 的定义,则委托给父工厂查找
String nameToLookup = originalBeanName(name);
if (parentBeanFactory instanceof AbstractBeanFactory) {
return ((AbstractBeanFactory) parentBeanFactory).doGetBean(
nameToLookup, requiredType, args, typeCheckOnly);
}
else {
return parentBeanFactory.getBean(nameToLookup);
}
}
// ... 在当前容器中创建 Bean ...
}
}
源码解读:
- 当通过
getBean或@Autowired查找 Bean 时,Spring 会先查找当前容器。 - 如果找不到,并且当前容器有父容器,则会递归地在父容器中查找。
- 这意味着子容器可以透明地访问父容器中的所有 Bean。
- 反之,父容器不会向下查找子容器的 Bean ,因为它的
parentBeanFactory为空。
5.2 @Autowired 在父子容器中的行为与常见陷阱
- 正确场景 :Controller 在子容器,Service 在根容器。Controller 中
@AutowiredService,当前容器找不到,去父容器找,成功注入。 - 错误场景 1:父容器扫描范围过大 。如果根容器的
@ComponentScan范围既包含了 Service 也包含了 Controller。那么 Controller 的 Bean 将在根容器中被初始化。而DispatcherServlet的子容器也会扫描并尝试创建 Controller Bean。根据 Spring 的默认 Bean 覆盖策略(allowBeanDefinitionOverriding),可能会导致行为不一(Spring Boot 2.1+ 默认禁用覆盖并报错)。 - 错误场景 2:子容器未正确链接到父容器 。如果
DispatcherServlet在初始化时未找到根容器,它会创建一个没有父容器的孤儿容器。此时,子容器中的 Controller 试图注入 Service 时将抛出NoSuchBeanDefinitionException。
5.3 内联示例:验证访问规则
java
// ===== 根容器配置 (RootConfig.java) =====
@Configuration
@ComponentScan(basePackages = "com.example.app.service") // 只扫描 Service
public class RootConfig {
}
// ===== 业务 Service =====
// 位于 com.example.app.service 包下
@Service
public class BusinessService {
public String getMessage() {
return "Message from BusinessService";
}
}
// ===== 子容器配置 (WebConfig.java) =====
@Configuration
@ComponentScan(basePackages = "com.example.app.controller") // 只扫描 Controller
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
}
// ===== Web 层 Controller =====
// 位于 com.example.app.controller 包下
@RestController
public class DemoController {
@Autowired
private BusinessService businessService; // 此 Bean 存在于父容器
@GetMapping("/hello")
public String hello() {
// 属性注入成功,可正常访问
return businessService.getMessage();
}
}
// ===== 编程式启动 (WebAppInitializer.java) =====
public class WebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class<?>[] { RootConfig.class }; // 注册根容器配置
}
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class<?>[] { WebConfig.class }; // 注册子容器配置
}
@Override
protected String[] getServletMappings() {
return new String[] { "/" };
}
}
示例解读 :此代码清晰地建立了父子容器。RootConfig 只负责扫描业务层,而 WebConfig 仅扫描控制器层。DemoController 中的 @Autowired BusinessService 之所以能工作,完全依赖 Spring 父子容器的向上查找机制。如果将 RootConfig 的包扫描范围扩大,就可能出现重复 Bean 定义的问题。
6. Spring Boot 的简化:单一容器与自动注册
Spring Boot 通过"习惯优于配置"的理念,极大地简化了 Spring MVC 的启动过程,其中最显著的改变就是对父子容器模型的消除。
6.1 Spring Boot 的单一 Web 容器模型
在 spring-boot-starter-web 项目中,Spring Boot 默认并不创建父子容器。取而代之的是,它创建了一个单一的 AnnotationConfigServletWebServerApplicationContext。这个特殊的上下文同时承载了传统意义上的"业务 Bean"和"Web Bean"。它既是根容器,也是 Web 容器。
为何能这样做? 在微服务架构和 Self-Contained 应用(即"fat jar")大行其道的今天,需要部署到独立 Servlet 容器的场景越来越少。Spring Boot 的嵌入式 Web 服务器模式,使得一个应用天生就是一个独立进程,不再需要与其它 Web 模块共享顶层服务。因此,父子容器的隔离在多数场景下不再是强需求,反而增加了心智负担和配置复杂性。消除层次结构,让一切变得更简单、透明。
6.2 DispatcherServletAutoConfiguration 的核心逻辑
Spring Boot 如何在这个单一容器中注册 DispatcherServlet?完全通过自动配置实现。
java
// org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
@Configuration(proxyBeanMethods = false) // 1. 声明为配置类
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) // 2. 仅当是 Servlet Web 环境时生效
@ConditionalOnClass(DispatcherServlet.class) // 3. 仅当 DispatcherServlet 在类路径时生效
@EnableConfigurationProperties(ServerProperties.class) // 4. 绑定 ServerProperties
public class DispatcherServletAutoConfiguration {
public static final String DEFAULT_DISPATCHER_SERVLET_BEAN_NAME = "dispatcherServlet";
@Configuration(proxyBeanMethods = false)
@Conditional(DefaultDispatcherServletCondition.class)
@ConditionalOnClass(ServletRegistration.class)
static class DispatcherServletConfiguration {
@Bean(name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
public DispatcherServlet dispatcherServlet(WebMvcProperties webMvcProperties) {
// 5. 创建 DispatcherServlet Bean
DispatcherServlet dispatcherServlet = new DispatcherServlet();
dispatcherServlet.setDispatchOptionsRequest(webMvcProperties.isDispatchOptionsRequest());
// ... 其他初始化设置 ...
return dispatcherServlet;
}
}
@Configuration(proxyBeanMethods = false)
@Conditional(DispatcherServletRegistrationCondition.class)
static class DispatcherServletRegistrationConfiguration {
@Bean(name = "dispatcherServletRegistration")
@ConditionalOnBean(value = DispatcherServlet.class, name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
public DispatcherServletRegistrationBean dispatcherServletRegistration(DispatcherServlet dispatcherServlet) {
// 6. 创建 DispatcherServletRegistrationBean,负责将 DispatcherServlet 注册到嵌入式 Servlet 容器
DispatcherServletRegistrationBean registration = new DispatcherServletRegistrationBean(dispatcherServlet);
registration.setLoadOnStartup(serverProperties.getServlet().getLoadOnStartup());
registration.setName(DEFAULT_DISPATCHER_SERVLET_BEAN_NAME);
return registration;
}
}
}
源码解读:
- 这里没有
ContextLoaderListener,没有RootApplicationContext的概念。DispatcherServlet作为一个普通的 Bean 被创建。 DispatcherServletRegistrationBean是一个实现了ServletContextInitializer的 Smart 组件。在嵌入式容器启动时,它会将DispatcherServlet的实例注册到容器的 Servlet 上下文中。这个过程在前文"Spring Boot 嵌入式容器"篇有详细论述。- 当
dispatcherServletBean 被创建和初始化时,它的init()方法依然会执行。但是,在执行FrameworkServlet.initWebApplicationContext()时,它会发现构造函数中或外部并没有注入另一个ApplicationContext,同时从ServletContext中也找不到独立的根容器。最终,它会直接使用应用自身唯一的AnnotationConfigServletWebServerApplicationContext作为它的webApplicationContext,并且此容器的parent为null。整个应用只有一个容器,终结了父子容器的历史。
6.3 强行恢复父子容器模式的后果分析
虽然在 Spring Boot 中不推荐,但技术上可以强制创建父子容器,例如:
java
@SpringBootApplication
public class DemoApplication {
@Bean
public ServletListenerRegistrationBean<ContextLoaderListener> contextLoaderListenerBean() {
ServletListenerRegistrationBean<ContextLoaderListener> bean =
new ServletListenerRegistrationBean<>();
bean.setListener(new ContextLoaderListener());
return bean;
}
@Bean
public DispatcherServlet dispatcherServlet() {
DispatcherServlet servlet = new DispatcherServlet();
// 不设置 ApplicationContext,让它自己创建子容器
return servlet;
}
}
潜在问题:
- Bean 重复定义 :Spring Boot 主应用已经扫描了所有 Bean,
ContextLoaderListener创建的根容器如果扫描范围为全包,会导致所有 Bean 在根容器和子容器中被创建两次,可能因 Bean 覆盖策略导致启动失败。 - 困惑的注入行为:开发者需要时刻意识到哪个 Bean 在哪个容器,增加了认知负担。
- 不必要的复杂性:滥用父子容器违背了 Spring Boot 的设计哲学。
Spring Boot 中 DispatcherServlet 自动配置的序列图清晰地展示了这个简化后的流程:
直接使用 BAC 作为其 WebApplicationContext BAC->>DSRB: 4. 创建 DispatcherServletRegistrationBean (Bean) activate DSRB DSRB->>DSRB: 5. ServletContextInitializer.onStartup(ServletContext) DSRB->>WSC: 6. addServlet("dispatcherServlet", dispatcherServlet) deactivate DSRB
图表主旨概括 :此序列图展示了 Spring Boot 自动配置如何在一个单一容器中完成 DispatcherServlet 的创建和注册,省略了 ContextLoaderListener 和父子容器查找的步骤。
逐层/逐元素分解:
- 步骤 1-3 :
DispatcherServletAutoConfiguration只是一个普通的@Configuration类,它向容器注册了一个DispatcherServletBean。这个 Bean 的初始化过程依然完整,但其使用的ApplicationContext就是 Spring Boot 应用本身。 - 步骤 4-6 :
DispatcherServletRegistrationBean作为桥接器,在嵌入式 Servlet 容器启动的最后一步,将已经就绪的DispatcherServlet物理注册到 Web 服务器中。
设计原理映射 :这体现了约定优于配置(Convention over Configuration) 的设计范式。Spring Boot 的"约定"就是一个应用对应一个 Web 容器和一个 Spring 容器,除非你有极其特殊的理由,否则这就是最佳实践。它将开发者从"如何正确配置父子容器"的复杂问题中解放出来。
工程联系与关键结论 :在 Spring Boot 项目中,除非要在一个应用中启动多个独立的 Web 上下文(极其罕见),否则绝不应手动引入父子容器结构。理解 Spring Boot 的这一简化,有助于减少不必要的配置,避免因误用容器层次而导致的线上事故。
7. 生产事故排查专题
7.1 事故一:Controller 无法注入 Service(NoSuchBeanDefinitionException)
- 现象 :应用启动时控制台无异常,但第一次调用某个 REST 接口时抛出
NoSuchBeanDefinitionException,提示com.example.BusinessService未找到。或者应用直接启动失败,因为 Controller 无法完成依赖注入。 - 排查思路 :
- 检查抛异常的 Controller 是否被 Spring 管理(是否在子容器扫描路径下)。
- 检查
BusinessService是否被 Spring 管理(是否在根容器扫描路径下,是否有@Service注解)。 - 检查应用启动日志,确认 Root WebApplicationContext 和 Servlet WebApplicationContext 是否均已成功初始化。
- 利用
ApplicationContext.getBeanDefinitionNames()或 Actuator 的/beans端点,分别检查两个容器中都存在哪些 Bean。这是最关键的一步。
- 根因分析 :最常见的原因是
ContextLoaderListener的配置中,contextConfigLocation指定的包扫描范围未覆盖BusinessService所在的包。或者,虽然在web.xml中配置了ContextLoaderListener,但忘记配置对应的contextConfigLocation参数,导致根容器使用默认配置,无法找到任何 Bean。在无web.xml时代,可能是AbstractAnnotationConfigDispatcherServletInitializer.getRootConfigClasses()方法未正确返回包含 Service 扫描的配置类。 - 解决 :修正根容器的配置,确保其
@ComponentScan或 XML 配置包含了BusinessService所在的基包。 - 最佳实践 :
- 明确分层扫描 :根容器专门扫描
@Service、@Repository、@Component等业务注解,子容器专门扫描@Controller、@RestController。 - 使用 Actuator :开发阶段务必引入
spring-boot-starter-actuator,通过/beans端点可视化地检查所有 Bean 的来源(所属容器)。 - 团队约定:在团队内部形成严格的包结构和注解使用规范,避免混用。
- 明确分层扫描 :根容器专门扫描
7.2 事故二:应用启动时发现 Bean 被初始化了两次,或因 "BeanDefinitionOverride" 报错
- 现象 :应用启动时,日志中出现了同一个 Bean 的全限定名被"Creating shared instance of singleton bean"了两次。在 Spring Boot 2.1+ 或较新的 Spring Framework 5.x 中,应用可能直接启动失败,并抛出
BeanDefinitionOverrideException。 - 排查思路 :
- 确认出现重复的 Bean 的类型是什么。如果是 Service、Repository,通常是父子容器包扫描重叠所致。
- 检查根容器和子容器的配置,查看它们的
contextConfigLocation或@ComponentScan的basePackages。
- 根因分析 :这是父子容器最典型的"扫描重叠"问题。
- 案例 :根容器配置了
@ComponentScan(basePackages = "com.example"),子容器配置了@ComponentScan(basePackages = "com.example.web")。虽然看起来子容器的范围更小,但根容器的com.example已经囊括了com.example.web下的所有类。结果是,com.example.web.controller.HelloController这个 Bean,既在根容器中被扫描创建了一次,又在子容器中被扫描创建了一次。因为子容器为"子",它创建 Bean 时去查找父容器,发现已经存在,在某些策略下会直接使用父容器的 Bean,而在另一些策略下会覆盖或报错。 - 设计意图:正是为了避免这种混乱,Spring MVC 才设计了父子容器隔离。但当配置不当时,隔离会失效。
- 案例 :根容器配置了
- 解决 :
- 治本 :严格限制根容器的扫描包,使其与子容器的扫描包完全互斥,没有交集 。例如,根容器扫
com.example.app,子容器扫com.example.web。 - 治标(不推荐) :在 Spring Boot 低版本中通过
spring.main.allow-bean-definition-overriding=true临时修复启动错误,但这可能掩盖了真正的设计问题。
- 治本 :严格限制根容器的扫描包,使其与子容器的扫描包完全互斥,没有交集 。例如,根容器扫
- 最佳实践 :
- 利用 Spring Boot 的单一容器:这是终极解决方案。从架构上消灭了扫描重叠的可能。
- 包结构隔离 :严格遵循
service、repository、controller等包结构,并在配置类中显式声明精确的扫描包路径,而不是依赖默认的、范围过大的扫描。
一个典型生产事故排查序列图(Controller 无法注入 Service)
图表主旨概括:此序列图直观地复盘了一场因根容器扫描配置错误,导致子容器 Controller 无法注入 Service Bean 的生产事故。
逐层/逐元素分解:
- 步骤 1-2:根容器初始化时,扫描路径配置错误,导致业务 Bean 未被加载。
- 步骤 3-4:子容器初始化正常,Controller Bean 就绪。
- 步骤 5-10 :运行时,Controller 的依赖注入需要查找
BusinessService,它依次在自己和父容器中查找,最终失败,导致应用异常。 - 关键点 :问题不是出在父子关系上,而是出在根容器的配置遗漏上。
设计原理映射 :这次事故是"无感知"的。@Autowired 的透明代理特性让开发者误以为容器会自动处理一切,而忽略了底层层次性容器的查找机制只有"向上",没有"向下"或"横向"。
工程联系与关键结论 :容器启动成功不等于应用配置正确。 必须通过单元测试、集成测试或 Actuator 的 /beans 端点等方式,在应用发布前,明确验证所有关键 Bean 都已就绪并位于预期的容器中。
8. 面试高频专题
1.问题:Spring MVC 中父子容器的作用是什么?它们是如何创建的?
- 标准回答 :父子容器是 Spring MVC 实现组件分层隔离的核心机制。
ContextLoaderListener创建根容器,负责管理业务层、持久层等中间层 Bean。DispatcherServlet创建子容器,负责管理 Web 层的 Controller、ViewResolver 等组件。子容器以根容器为父,可以透明地访问根容器的 Bean。 - 追问 1 :为什么需要父子容器,而不是所有 Bean 都在一个容器里?
- 加分回答 :分层有助于模块化 和关注点分离 。Web 层 Bean 与 Servlet API 紧耦合,而业务 Bean 应该独立。父子容器允许在一个应用中部署多个
DispatcherServlet实例,每个都可以有自己的 Web 上下文,但共享同一个业务层上下文。
- 加分回答 :分层有助于模块化 和关注点分离 。Web 层 Bean 与 Servlet API 紧耦合,而业务 Bean 应该独立。父子容器允许在一个应用中部署多个
- 追问 2 :如果
DispatcherServlet在初始化时找不到根容器会怎样?- 加分回答 :它会创建一个没有父容器的孤儿容器。这个容器的
getParent()返回null。这通常会导致问题,因为子容器中的 Controller 将无法注入根容器管理的 Bean。
- 加分回答 :它会创建一个没有父容器的孤儿容器。这个容器的
- 追问 3 :父子容器可以嵌套多层吗?
- 加分回答 :理论上
ApplicationContext支持多层嵌套,但在标准 Spring MVC 应用中,通常只有两层。更多的层次会带来不必要的复杂性。
- 加分回答 :理论上
2.问题:DispatcherServlet 的初始化流程是怎样的?
- 标准回答 :初始化入口是
HttpServletBean.init(),它解析 Servlet 配置参数后调用FrameworkServlet.initServletBean(),进而调用initWebApplicationContext()。在该方法中,它会获取根容器作为父容器,然后创建或查找自己的子容器,刷新它,最后调用DispatcherServlet.onRefresh()钩子来初始化九大策略组件。 - 追问 1 :
FrameworkServlet.initWebApplicationContext()是如何查找和创建子容器的?- 加分回答 :它首先检查构造注入的
webApplicationContext,其次在ServletContext的属性中查找,如果都没有,则调用createWebApplicationContext(rootContext)创建一个新的。这个方法集成了查找、创建、关联、刷新的完整逻辑。
- 加分回答 :它首先检查构造注入的
- 追问 2 :
onRefresh()和initStrategies()的关系是什么?- 加分回答 :
onRefresh()是模板方法FrameworkServlet留给DispatcherServlet的扩展点。DispatcherServlet在onRefresh()中唯一做的事情就是调用initStrategies()。这是一种两级模板方法的设计,进一步解耦。
- 加分回答 :
- 追问 3 :如果在
onRefresh()执行过程中发生异常,会对容器造成什么影响?- 加分回答 :异常会向上传播,导致
DispatcherServlet初始化失败。Servlet 容器会捕获这个异常,DispatcherServlet将无法处理请求,通常返回 404 或 500 错误。
- 加分回答 :异常会向上传播,导致
3.问题:为什么通常把 Service 定义在根容器中,而 Controller 定义在子容器中?
- 标准回答:这是一种架构分层的体现。Service 是业务核心,与 Web 技术无关,放在根容器可被多个 Web 上下文共享。Controller 与 Servlet API 强相关,放在子容器中可以实现视图层与业务层的解耦。
- 追问 1 :如果我把 Service 也放在子容器中会有什么问题?
- 加分回答 :技术上没问题,但会失去分层带来的解耦优势。如果有另一个
DispatcherServlet(如处理 REST API),它无法共享这个 Service,内存中会存在多个完全相同的 Service Bean。
- 加分回答 :技术上没问题,但会失去分层带来的解耦优势。如果有另一个
- 追问 2 :如果 Controller 也被根容器扫描到了呢?
- 加分回答:这会导致 Controller Bean 被创建两次。根容器创建一次,子容器又创建一次。这可能导致 Bean 覆盖冲突,或出现一些预期外的行为,比如事务、AOP 只在根容器的 Controller 实例上生效,处理请求的子容器实例却无效。
- 追问 3 :如何避免扫描范围重叠?
- 加分回答 :使用精确的
@ComponentScan的basePackages属性,或者使用注解过滤,如excludeFilters = @ComponentScan.Filter(classes = Controller.class)。
- 加分回答 :使用精确的
4.问题:Spring Boot 是否还有父子容器?它是如何管理 Spring MVC 组件的?
- 标准回答 :Spring Boot 默认没有父子容器。它使用单一的
AnnotationConfigServletWebServerApplicationContext作为唯一的 Spring 容器。所有的 Bean,包括 Service、Controller,都在这一个容器中。DispatcherServlet通过自动配置类DispatcherServletAutoConfiguration被创建和注册到这个单一容器中。 - 追问 1 :Spring Boot 为什么可以不需要父子容器?
- 加分回答:因为嵌入式容器让应用成为独立的"fat jar",不再需要与其他模块共享服务。单一容器简化了配置,避免了许多因容器层次导致的复杂性问题。
- 追问 2 :在 Spring Boot 中,
DispatcherServlet的init()方法还会执行吗?- 加分回答 :会。
DispatcherServlet作为 Bean 被创建时,它的生命周期完全由 Spring 管理,其InitializingBean接口(通过HttpServletBean)和init()方法最终都会被执行。此时它发现的WebApplicationContext就是 Spring Boot 唯一的应用上下文。
- 加分回答 :会。
- 追问 3 :如果想在 Spring Boot 中用回传统的父子容器,怎么做?有什么风险?
- 加分回答 :需要手动注册
ContextLoaderListener并配置一个新的DispatcherServlet覆盖自动配置。风险很大,容易因扫描重叠导致 Bean 冲突,且与 Spring Boot 的设计哲学背道而驰。
- 加分回答 :需要手动注册
5.问题:如何在 Spring Boot 中手动创建父子容器结构?
- 标准回答 :需要创建一个新的
ApplicationContext作为根容器,通过ContextLoaderListener加载它,并让自动配置的DispatcherServlet引用这个新根容器。或者,更简单地,创建一个新的DispatcherServlet实例,并传入一个独立创建的子ApplicationContext。 - 追问 1 :你会推荐这种做法吗?为什么?
- 加分回答:不推荐。除非是在极特殊的遗留系统整合场景下,否则它会引入不必要的复杂性、潜在的 Bean 重复定义等问题,而这些问题恰恰是 Spring Boot 试图解决的。
- 追问 2 :如果确实需要这种结构(例如,在一个进程中同时运行一个 Web 前端和管理后台应用),有没有更好的方案?
- 加分回答 :更好的方案是考虑微服务化,将两个模块拆分为独立的 Spring Boot 应用。如果必须在同一进程,可以考虑为两个
DispatcherServlet创建不同的子容器,但它们都共用 Spring Boot 的主上下文作为父容器,而不是创建一个额外的"根容器"。
- 加分回答 :更好的方案是考虑微服务化,将两个模块拆分为独立的 Spring Boot 应用。如果必须在同一进程,可以考虑为两个
6.问题:如果 Controller 和 Service 被扫描到同一个容器中,会产生什么影响?
- 标准回答:在传统 Spring MVC 中,这通常意味着扫描范围重叠。会导致 Service 或 Controller 被实例化两次(分别在两个容器中),可能引发启动报错(Bean 定义覆盖)或运行时行为不一致(如事务失效)。
- 追问 1 :为什么事务可能会失效?
- 加分回答:如果 Service 在根容器被扫描,配置了事务的 AOP 也在根容器生效。但 Controller 在子容器被扫描。如果 Controller 错误地注入了子容器中新建的、未被 AOP 代理的 Service 实例(如果包扫描重叠导致子容器也创建了 Service),事务切面就不再生效。
- 追问 2 :Spring Boot 是否天然避免了这个问题?
- 加分回答:是的。因为在单一容器模型中,所有 Bean 都在同一个上下文,没有重复创建 Bean 的问题,Spring 的 AOP 代理能统一、正确地应用于所有需要事务的 Bean。
- 追问 3 :除了避免扫描重叠,还有什么方法可以从根本上杜绝这个问题?
- 加分回答 :采用代码审查和自动化测试。例如,编写一个集成测试,利用
ApplicationContext获取 Bean,并断言某个 Bean 的类型是否为代理(如Assert.isTrue(AopUtils.isAopProxy(bean))),来确保事务切面已生效。
- 加分回答 :采用代码审查和自动化测试。例如,编写一个集成测试,利用
7.问题:ContextLoaderListener 和 DispatcherServlet 的区别。
- 标准回答 :
ContextLoaderListener是ServletContextListener,负责创建和管理根容器(Root WebApplicationContext),与 Web 请求处理无关。DispatcherServlet是一个Servlet,负责创建子容器和处理所有进入的 HTTP 请求。 - 追问 1 :它们的生命周期一样吗?
- 加分回答 :基本一致,都绑定在
ServletContext的生命周期上。Listener是先于Servlet初始化的。
- 加分回答 :基本一致,都绑定在
- 追问 2 :它们各自的配置(
contextConfigLocation)有什么区别?- 加分回答 :
ContextLoaderListener的配置通过<context-param>指定,配置的是全局的、共享的中间层 Bean。DispatcherServlet的配置通过其自身的<init-param>指定,配置的是该 Servlet 专有的 Web 层 Bean。
- 加分回答 :
- 追问 3 :能否只使用
DispatcherServlet而不使用ContextLoaderListener?- 加分回答 :可以。
DispatcherServlet在没有父容器时也能工作,此时它需要在自己的配置文件中加载所有 Bean,包括 Service 和 Repository。
- 加分回答 :可以。
8.问题:onRefresh() 方法的作用是什么?它初始化了哪些重要组件?
- 标准回答 :
onRefresh()是DispatcherServlet的初始化钩子,它在子容器刷新后、Servlet 开始处理请求前被调用。它的作用是调用initStrategies()来初始化九大策略组件,例如HandlerMapping、HandlerAdapter和HandlerExceptionResolver等。 - 追问 1 :九大策略组件的默认实现是从哪里来的?
- 加分回答 :来自
DispatcherServlet所在包下的DispatcherServlet.properties文件。Spring 采用"约定优于配置"的方式提供了一套完整可用的默认实现。
- 加分回答 :来自
- 追问 2 :如何自定义这些策略组件?
- 加分回答 :只需在子容器(或在 Spring Boot 的单一容器)中注册一个实现了相应接口的 Bean。
DispatcherServlet的"发现即使用"机制会自动检测并替换默认实现。
- 加分回答 :只需在子容器(或在 Spring Boot 的单一容器)中注册一个实现了相应接口的 Bean。
- 追问 3 :为什么说这是模板方法模式的应用?
- 加分回答 :
FrameworkServlet定义了初始化骨架(initWebApplicationContext),并在其中调用了抽象的onRefresh()方法。DispatcherServlet作为子类,实现了这个特定的onRefresh()步骤。父类控制流程,子类实现细节,这是典型的模板方法模式。
- 加分回答 :
9.问题:简述 DispatcherServlet 通过什么设计模式实现组件的可扩展性。
- 标准回答 :主要通过策略模式 。每个策略组件(如
HandlerMapping)都定义了接口,DispatcherServlet依赖这些接口而非具体实现。在运行时,它从 IoC 容器中动态地发现这些接口的实现类,并应用它们。 - 追问 1 :IoC 容器在这里起到了什么作用?
- 加分回答 :IoC 容器是策略工厂。它负责管理策略实例的生命周期和之间的依赖,并将发现策略的责任从
DispatcherServlet转移给了自己,大大降低了两者的耦合。
- 加分回答 :IoC 容器是策略工厂。它负责管理策略实例的生命周期和之间的依赖,并将发现策略的责任从
- 追问 2 :除了策略模式,还用了哪些设计模式?
- 加分回答 :模板方法模式 (在
init和onRefresh流程中)、前端控制器模式 (DispatcherServlet本身)、适配器模式 (HandlerAdapter,使得DispatcherServlet可以适配不同类型的处理器)。
- 加分回答 :模板方法模式 (在
- 追问 3 :这种"发现即使用"的扩展方式,和我们之前在 IoC 篇讲的
BeanPostProcessor扩展点有何不同?- 加分回答 :
BeanPostProcessor是作用于 Bean 生命周期层面 的通用扩展点,可以影响几乎所有 Bean 的创建和初始化。而DispatcherServlet的"发现即使用"是特定于自身架构的、设计层面的策略选择 ,它只在onRefresh()这个特定时间点检索特定类型的 Bean。
- 加分回答 :
10.问题:为什么说 Spring MVC 是"策略模式"的典型案例?
- 标准回答 :因为 Spring MVC 将请求处理过程中的多个可变环节(映射、适配、视图解析等)都抽象成了接口(策略),并通过组合的方式让
DispatcherServlet使用这些策略。用户可以通过向容器注册自定义实现的 Bean 来替换默认策略,而无需更改主流程代码。 - 追问 1 :这种策略模式与直接写
if-else或switch-case来切换实现相比,优势在哪?- 加分回答 :符合开闭原则 。对扩展开放,对修改关闭。当需要新增一种 HandlerMapping 策略时,只需新建一个类并注册为 Bean,不用修改
DispatcherServlet的核心代码。
- 加分回答 :符合开闭原则 。对扩展开放,对修改关闭。当需要新增一种 HandlerMapping 策略时,只需新建一个类并注册为 Bean,不用修改
- 追问 2 :如果在容器中注册了多个同一种策略的实现(如多个 ViewResolver),
DispatcherServlet如何处理?- 加分回答 :它会根据
Ordered接口或@Order注解对它们进行排序,并遍历这个策略链,直到其中一个策略成功处理并返回结果。这实际上是责任链模式的体现。
- 加分回答 :它会根据
- 追问 3 :在 Spring Boot 时代,我们通常感觉不到这些策略的存在,这说明什么?
- 加分回答 :说明 Spring Boot 的自动配置为我们提供了高度合理且开箱即用的默认策略实现,如
RequestMappingHandlerMapping,封装了复杂性,让开发者聚焦于业务。
- 加分回答 :说明 Spring Boot 的自动配置为我们提供了高度合理且开箱即用的默认策略实现,如
11.问题:如何在不使用 web.xml 的情况下启动 Spring MVC?
- 标准回答 :通过实现
WebApplicationInitializer接口(或其便捷子类,如AbstractAnnotationConfigDispatcherServletInitializer)。Servlet 3.0+ 规范提供了ServletContainerInitializer,Spring 的SpringServletContainerInitializer会自动发现并执行所有实现了WebApplicationInitializer的类。 - 追问 1 :
SpringServletContainerInitializer是如何被自动发现的?- 加分回答 :它利用了 JDK 的 SPI(Service Provider Interface) 机制。在
spring-web模块的META-INF/services/javax.servlet.ServletContainerInitializer文件里,指定了SpringServletContainerInitializer全限定名。
- 加分回答 :它利用了 JDK 的 SPI(Service Provider Interface) 机制。在
- 追问 2 :
AbstractAnnotationConfigDispatcherServletInitializer为我们做了什么?- 加分回答 :它将繁琐的编程式注册过程简化了。我们只需要覆写
getRootConfigClasses和getServletConfigClasses两个方法,分别返回父容器和子容器的@Configuration类即可。它内部会自动创建ContextLoaderListener和DispatcherServlet。
- 加分回答 :它将繁琐的编程式注册过程简化了。我们只需要覆写
- 追问 3 :这个机制在 Spring Boot 中还有用吗?
- 加分回答 :在 Spring Boot 的内嵌容器模式下,它不再是主启动方式,但仍然被支持。Spring Boot 自己的
SpringBootServletInitializer在部署到外部容器时,本质上也是实现了WebApplicationInitializer的一个变种。
- 加分回答 :在 Spring Boot 的内嵌容器模式下,它不再是主启动方式,但仍然被支持。Spring Boot 自己的
12.系统设计题:设计一个支持动态注册新模块的 Web 系统,每个模块拥有自己独立的 Spring 子容器,互不干扰,但可以共享公共的基础服务(如数据源)。请说明如何利用 Spring 的容器层次结构实现,并考虑通信隔离。
- 标准回答 :
- 容器层次 :创建一个根容器 ,存放所有公共的基础服务,如
DataSource、TransactionManager、通用UtilityService等。 - 模块隔离 :为每个动态注册的模块创建一个独立的子容器,并设置根容器为其父容器。每个子容器包含其专属的 Controller、Service、Repository。
- 动态注册 :为每个模块创建一个独立的
DispatcherServlet,并使用编程式 API(如ServletContext.addServlet("moduleAServlet", new DispatcherServlet(moduleAContext)))动态注册它及对应的请求映射(如/moduleA/*)。 - 通信隔离 :模块间应避免直接通过 Spring 容器注入。
- 同步通信 :通过定义在根容器中的事件监听机制(ApplicationEvent)。模块 A 发布事件,模块 B 监听事件,事件对象只在根容器中流转。
- 异步通信 :使用根容器管理的消息队列,实现完全的模块解耦。
- 容器层次 :创建一个根容器 ,存放所有公共的基础服务,如
- 追问 1 :如果某个模块频繁加载或卸载,它的子容器如何安全地销毁,避免内存泄漏?
- 加分回答 :在卸载模块时,必须调用其子容器的
close()方法,这会销毁所有单例 Bean,释放 JDBC 连接等资源。同时,需要调用ServletContext的removeServlet方法移除DispatcherServlet。
- 加分回答 :在卸载模块时,必须调用其子容器的
- 追问 2 :如何保证每个子容器中的 Bean 定义不互相冲突?
- 加分回答:每个模块的子容器都是完全独立的应用上下文,它们的 Bean 定义默认是不可见的。只要每个模块内部的 Bean Name 不与自己冲突即可。扫描包时,确保不同模块的类在不同的基包下。
- 追问 3 :这种架构的优缺点分别是什么?
- 加分回答 :优点 是实现了极高的模块化、热插拔和故障隔离。缺点是架构复杂,JVM 内存管理和启动时间有更大挑战,跨模块的业务流程支持较难实现。这是典型的插件化架构,适用于需要动态扩展的大型平台。
文末速查表:Spring MVC 启动关键接口与类
| 接口/类 | 所属容器 | 核心作用 | 调用/执行时机 |
|---|---|---|---|
ServletContextListener |
Servlet 容器 | 监听 ServletContext 生命周期,用于启动和停止根容器。 |
Servlet 容器启动/关闭时。 |
ContextLoaderListener |
Root | 实现 ServletContextListener,委托 ContextLoader 创建根容器。 |
contextInitialized 事件触发。 |
ContextLoader |
Root | 执行根容器的创建、配置和刷新操作。 | 由 ContextLoaderListener 调用。 |
FrameworkServlet |
Servlet Web | DispatcherServlet 的父类,定义了子容器创建、查找和关联父容器的核心流程。 |
DispatcherServlet.init() 过程中。 |
DispatcherServlet |
Servlet Web | 前端控制器,初始化九大策略组件,并负责协调整个请求处理流程。 | HttpServletBean.init() → onRefresh()。 |
WebApplicationInitializer |
无 | 编程式初始化入口,替代 web.xml,用于配置 Servlet、Filter 和 Listener。 |
Servlet 容器启动时,由 SpringServletContainerInitializer 通过 SPI 发现并调用。 |
ServletContainerInitializer |
Servlet 容器 | Servlet 3.0+ 规范定义的接口,用于动态配置 Web 应用。 | 应用启动时,Servlet 容器会通过 SPI 发现并调用其 onStartup 方法。 |
DispatcherServletAutoConfiguration |
Spring Boot App Context | Boot 自动配置类,在单一容器中创建和注册 DispatcherServlet 和 DispatcherServletRegistrationBean。 |
Spring Boot 应用初始化,处理自动配置时。 |
DispatcherServletRegistrationBean |
Spring Boot App Context | 实现 ServletContextInitializer,将 DispatcherServlet 物理注册到嵌入式 Web 服务器。 |
嵌入式 Web 容器启动过程中。 |
延伸阅读
- Spring Framework 官方文档 :Web Servlet 章节,深入探讨
DispatcherServlet的配置和策略组件。 - 《Spring 实战(第 5 版)》:Craig Walls 著,提供了很多关于 Spring MVC 和 Spring Boot 的实战技巧。
- 《深度剖析 Spring 源码》系列博客 :众多技术博主对
FrameworkServlet.initWebApplicationContext()和DispatcherServlet.onRefresh()等关键方法的逐行分析。