Spring MVC 启动全景:DispatcherServlet 与父子容器

概述

衔接前文: 前文已深入剖析了 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 的初始化流程,从 ContextLoaderListenerFrameworkServlet,再到 DispatcherServlet.onRefresh(),完整展现 Spring MVC 的启动全景,并结合父子容器的设计意图,分析其在实际开发中的潜在陷阱与最佳实践。

核心要点:

  • 父子容器体系ContextLoaderListener 创建根容器(Root WebApplicationContext),DispatcherServlet 创建子容器(Servlet WebApplicationContext),形成层次化上下文。
  • 初始化流程DispatcherServlet.init()FrameworkServlet.initWebApplicationContext() 创建或查找子容器 → onRefresh() 初始化 Spring MVC 九大策略组件。
  • 策略组件的初始化:采用"默认+可覆盖"的策略,即"发现即使用",体现了 IoC 容器的扩展性与模板方法模式。
  • Spring Boot 的简化 :默认使用单一 AnnotationConfigServletWebServerApplicationContext 容器,消除父子容器层次,并通过自动配置注册 DispatcherServlet

文章组织架构图

flowchart TB subgraph A ["Spring MVC 启动全景"] direction TB n1["1. 启动总览:Servlet与Spring容器的桥接"] n2["2. Root容器的创建:ContextLoaderListener与ContextLoader"] n3["3. DispatcherServlet的初始化与子容器创建"] n4["4. onRefresh:九大策略组件的初始化"] n5["5. 父子容器的Bean访问规则"] n6["6. Spring Boot的简化:单一容器与自动注册"] n7["7. 生产事故排查专题"] n8["8. 面试高频专题"] end n1 --> n2 --> n3 --> n4 --> n5 --> n6 --> n7 --> n8 style A fill:#e1f5fe,stroke:#01579b,stroke-width:2px classDef default fill:#ffffff,stroke:#01579b,stroke-width:1px,color:#333; classDef accident fill:#ffebee,stroke:#b71c1c,stroke-width:2px,color:#333; classDef interview fill:#e8f5e9,stroke:#1b5e20,stroke-width:2px,color:#333; class n7 accident; class n8 interview;

架构图说明:

  • 总览说明:全文 8 个模块遵循从传统 Servlet 与 Spring 的桥接开始,逐步深入到根容器创建、子容器初始化、策略组件加载,再对比 Spring Boot 的现代简化模型,最后通过生产事故排查和面试专题完成从理论到实践的闭环。
  • 逐模块说明
    • 模块 1 建立整体认知,厘清 Servlet 容器与 Spring IoC 容器如何通过 ServletContext 桥接。
    • 模块 2-3 详细解剖两大容器的创建过程与 DispatcherServlet 的初始化全流程。
    • 模块 4 深入 onRefresh() 方法内部,揭示九大策略组件如何利用 IoC 容器实现"策略模式"。
    • 模块 5 分析父子容器间的 Bean 访问与注入规则,为排查问题打下理论基础。
    • 模块 6 对比 Spring Boot 如何通过自动配置与单一容器优雅地解决传统部署模型的复杂性。
    • 模块 7-8 将理论落地到生产实践,解决常见事故并应对高频面试。
  • 关键结论父子容器是 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 被创建时,会调用所有注册的 ServletContextListenercontextInitialized(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 设计了两个关键组件作为桥梁:

  1. ContextLoaderListener :它实现了 ServletContextListener。在 Web 应用启动时,它负责创建 Spring 的根(Root)WebApplicationContext 。这个根容器通常管理着业务逻辑、数据访问、事务等中间层和后端层的 Bean。创建完成后,它会将根容器作为属性存入 ServletContext 中,使整个 Web 应用都可以访问到它。

  2. DispatcherServlet :它继承自 HttpServlet,是 Spring MVC 的核心。每个 DispatcherServlet 实例在初始化时,都会创建自己专属的子(Servlet)WebApplicationContext 。它会自动找到存于 ServletContext 中的根容器,并将其设置为自己的父容器。这个子容器专注于 Web 层的 Bean,例如 Controller、ViewResolver、HandlerMapping 等。

1.3 整体启动时序

一个典型的 Spring MVC 应用启动时序如下:

  1. Servlet 容器(如 Tomcat)启动,解析 web.xml 或扫描 ServletContainerInitializer
  2. 容器触发 ContextLoaderListener.contextInitialized() 回调。
  3. ContextLoaderListener 创建并刷新Root WebApplicationContext
  4. 容器初始化 DispatcherServlet
  5. DispatcherServlet.init() 被调用,在其内部:
    • 找到已存在的 Root WebApplicationContext 作为父容器。
    • 创建并刷新Servlet WebApplicationContext(子容器)。
    • onRefresh() 中初始化九大策略组件。
  6. 应用启动完成,等待处理请求。

1.4 父子容器层次结构图

这张图清晰地展示了 Bean 在两层容器中的分布和关系。

flowchart TB subgraph Servlet_Container ["Servlet容器 / Tomcat"] SC["ServletContext"] subgraph Root_Context ["Root WebApplicationContext - 根容器"] direction TB DS["DataSource & JPA Repositories"] SVC["Service & Transaction Management"] MB["Middleware Beans e.g., Cache, Message"] end subgraph DispatcherServlet_Context ["Servlet WebApplicationContext - 子容器"] direction TB C["Controllers & RestControllers"] HM["HandlerMapping"] HA["HandlerAdapter"] VR["ViewResolver"] HER["HandlerExceptionResolver"] end end SC -- "持有引用" --> Root_Context SC -- "持有引用" --> DispatcherServlet_Context DispatcherServlet_Context -- "parent" --> Root_Context C -.->|"可注入"| SVC HM -.->|"可注入"| MB SVC -.->|"无法注入"| C DS -.->|"无法注入"| C style SC fill:#e1bee7,stroke:#8e24aa style Root_Context fill:#ffecb3,stroke:#ff6f00 style DispatcherServlet_Context fill:#b3e5fc,stroke:#0277bd

图表主旨概括 :此图展示了 Spring MVC 父子容器层次结构的核心模型,直观地显示了 ServletContext、Root WebApplicationContext 和 Servlet WebApplicationContext 三者的包含与关联关系,以及各自管辖的典型 Bean 类型。

逐层/逐元素分解

  • ServletContext :作为顶层全局上下文,它不直接管理 Bean,但持有对根容器和(通常通过 DispatcherServlet 间接)对子容器的引用,是整个体系的基础设施。
  • Root WebApplicationContext 层 :包含 DataSourceServiceTransactionManager 等应用基础服务和业务逻辑 Bean。它们与 Web 层解耦,可以被任何 Web 上下文或定时任务等非 Web 场景共享。
  • Servlet WebApplicationContext 层 :包含 ControllerHandlerMappingViewResolver 等 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 应用中只有一个根容器,这是结构稳定性的保障。
  • 创建 WebApplicationContextcreateWebApplicationContext(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() 方法的调用链:从 HttpServletBeanFrameworkServlet

Servlet 的初始化从 init(ServletConfig config) 方法开始。在 Spring 中,这个调用链被精心设计,每个父类都负责一部分职责。

  1. HttpServletBean.init() :继承自 HttpServletHttpServletBean 覆写了 init() 方法。它的主要职责是将 ServletConfig 中的初始化参数(init-param)解析并设置到当前 Servlet 实例的属性上(例如,<init-param> 中配置的 contextConfigLocation)。之后,它调用 initServletBean() 模板方法,将流程移交给子类。

  2. FrameworkServlet.initServletBean()FrameworkServletDispatcherServlet 的父类,它实现了 initServletBean()。这个方法的核心任务就是创建或初始化其专属的 WebApplicationContext ,并调用 initWebApplicationContext()

  3. 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) 方法内部,首先实例化一个 XmlWebApplicationContextAnnotationConfigWebApplicationContext,然后调用 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 容器触发到策略组件初始化的完整交互过程。

sequenceDiagram participant SC as Servlet Container (Tomcat) participant H as HttpServletBean participant FS as FrameworkServlet participant DispatcherServlet participant SL as ServletContext participant WAC as WebApplicationContext (子容器) SC->>H: 1. init(ServletConfig) activate H H->>H: 2. 解析 init-param,设置属性 H->>FS: 3. initServletBean() activate FS FS->>FS: 4. initWebApplicationContext() Note over FS, SL: 查找根容器 FS->>SL: 5. getAttribute(ROOT_CONTEXT_ATTR) SL-->>FS: 6. 返回 Root WebApplicationContext alt 子容器不存在 FS->>WAC: 7. createWebApplicationContext(rootContext) activate WAC WAC-->>FS: 8. 返回新建的空子容器实例 FS->>WAC: 9. setParent(rootContext) FS->>WAC: 10. configureAndRefresh() Note over WAC: 扫描、加载Web层Bean,执行BeanFactoryPostProcessor等 WAC-->>FS: 11. 子容器刷新完毕 end FS->>DispatcherServlet: 12. onRefresh(wac) activate DispatcherServlet DispatcherServlet->>DispatcherServlet: 13. initStrategies(wac) Note over DispatcherServlet: 从wac中查找并初始化九大策略组件 deactivate DispatcherServlet deactivate WAC FS->>SL: 14. setAttribute(servletName, wac) deactivate FS deactivate H

图表主旨概括 :该序列图清晰地描绘了 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() 就是一个模板方法),FrameworkServletDispatcherServlet 分别实现特定的步骤。
  • 依赖倒置原则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 的默认实现是 BeanNameUrlHandlerMappingRequestMappingHandlerMapping

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 中 @Autowired Service,当前容器找不到,去父容器找,成功注入。
  • 错误场景 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 嵌入式容器"篇有详细论述。
  • dispatcherServlet Bean 被创建和初始化时,它的 init() 方法依然会执行。但是,在执行 FrameworkServlet.initWebApplicationContext() 时,它会发现构造函数中或外部并没有注入另一个 ApplicationContext,同时从 ServletContext 中也找不到独立的根容器。最终,它会直接使用应用自身唯一的 AnnotationConfigServletWebServerApplicationContext 作为它的 webApplicationContext,并且此容器的 parentnull整个应用只有一个容器,终结了父子容器的历史。

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

潜在问题

  1. Bean 重复定义 :Spring Boot 主应用已经扫描了所有 Bean,ContextLoaderListener 创建的根容器如果扫描范围为全包,会导致所有 Bean 在根容器和子容器中被创建两次,可能因 Bean 覆盖策略导致启动失败。
  2. 困惑的注入行为:开发者需要时刻意识到哪个 Bean 在哪个容器,增加了认知负担。
  3. 不必要的复杂性:滥用父子容器违背了 Spring Boot 的设计哲学。

Spring Boot 中 DispatcherServlet 自动配置的序列图清晰地展示了这个简化后的流程:

sequenceDiagram participant BAC as Boot Application Context participant DSC as DispatcherServletAutoConfiguration participant DSRB as DispatcherServletRegistrationBean participant WSC as WebServer Container(Tomcat) participant DS as DispatcherServlet BAC->>DSC: 1. 处理自动配置 activate DSC DSC->>BAC: 2. 注册 DispatcherServlet Bean deactivate DSC BAC->>BAC: 3. 实例化并初始化 DispatcherServlet Bean Note over BAC, DS: DispatcherServlet.init() 被调用,
直接使用 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-3DispatcherServletAutoConfiguration 只是一个普通的 @Configuration 类,它向容器注册了一个 DispatcherServlet Bean。这个 Bean 的初始化过程依然完整,但其使用的 ApplicationContext 就是 Spring Boot 应用本身。
  • 步骤 4-6DispatcherServletRegistrationBean 作为桥接器,在嵌入式 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 无法完成依赖注入。
  • 排查思路
    1. 检查抛异常的 Controller 是否被 Spring 管理(是否在子容器扫描路径下)。
    2. 检查 BusinessService 是否被 Spring 管理(是否在根容器扫描路径下,是否有 @Service 注解)。
    3. 检查应用启动日志,确认 Root WebApplicationContext 和 Servlet WebApplicationContext 是否均已成功初始化。
    4. 利用 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
  • 排查思路
    1. 确认出现重复的 Bean 的类型是什么。如果是 Service、Repository,通常是父子容器包扫描重叠所致。
    2. 检查根容器和子容器的配置,查看它们的 contextConfigLocation@ComponentScanbasePackages
  • 根因分析 :这是父子容器最典型的"扫描重叠"问题。
    • 案例 :根容器配置了 @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 的单一容器:这是终极解决方案。从架构上消灭了扫描重叠的可能。
    • 包结构隔离 :严格遵循 servicerepositorycontroller 等包结构,并在配置类中显式声明精确的扫描包路径,而不是依赖默认的、范围过大的扫描。

一个典型生产事故排查序列图(Controller 无法注入 Service)

sequenceDiagram participant App as 应用启动 participant RC as Root Container participant SC as Servlet Container participant C as HelloController participant User as 用户 App->>RC: 1. 初始化并刷新,加载配置 Note over RC: 扫描包:com.example.app.* (不含Service) RC-->>App: 2. 启动完成 (无 Service Bean) App->>SC: 3. 初始化并刷新,设置RC为父容器 Note over SC: 扫描包:com.example.web.controller SC-->>App: 4. 启动完成 (有 Controller Bean) User->>C: 5. 发送首个 /hello 请求 C->>SC: 6. 请求被处理,需注入 BusinessService SC->>SC: 7. 在自身容器查找 BusinessService Note over SC: 找不到 SC->>RC: 8. 在父容器查找 BusinessService Note over RC: 也找不到! RC-->>SC: 9. 抛出 NoSuchBeanDefinitionException SC-->>C: 10. 依赖注入失败 C-->>User: 11. HTTP 500 错误

图表主旨概括:此序列图直观地复盘了一场因根容器扫描配置错误,导致子容器 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 上下文,但共享同一个业务层上下文。
  • 追问 2 :如果 DispatcherServlet 在初始化时找不到根容器会怎样?
    • 加分回答 :它会创建一个没有父容器的孤儿容器。这个容器的 getParent() 返回 null。这通常会导致问题,因为子容器中的 Controller 将无法注入根容器管理的 Bean。
  • 追问 3 :父子容器可以嵌套多层吗?
    • 加分回答 :理论上 ApplicationContext 支持多层嵌套,但在标准 Spring MVC 应用中,通常只有两层。更多的层次会带来不必要的复杂性。

2.问题:DispatcherServlet 的初始化流程是怎样的?

  • 标准回答 :初始化入口是 HttpServletBean.init(),它解析 Servlet 配置参数后调用 FrameworkServlet.initServletBean(),进而调用 initWebApplicationContext()。在该方法中,它会获取根容器作为父容器,然后创建或查找自己的子容器,刷新它,最后调用 DispatcherServlet.onRefresh() 钩子来初始化九大策略组件。
  • 追问 1FrameworkServlet.initWebApplicationContext() 是如何查找和创建子容器的?
    • 加分回答 :它首先检查构造注入的 webApplicationContext,其次在 ServletContext 的属性中查找,如果都没有,则调用 createWebApplicationContext(rootContext) 创建一个新的。这个方法集成了查找、创建、关联、刷新的完整逻辑。
  • 追问 2onRefresh()initStrategies() 的关系是什么?
    • 加分回答onRefresh() 是模板方法 FrameworkServlet 留给 DispatcherServlet 的扩展点。DispatcherServletonRefresh() 中唯一做的事情就是调用 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 :如何避免扫描范围重叠?
    • 加分回答 :使用精确的 @ComponentScanbasePackages 属性,或者使用注解过滤,如 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 中,DispatcherServletinit() 方法还会执行吗?
    • 加分回答 :会。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 的主上下文作为父容器,而不是创建一个额外的"根容器"。

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.问题:ContextLoaderListenerDispatcherServlet 的区别。

  • 标准回答ContextLoaderListenerServletContextListener,负责创建和管理根容器(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() 来初始化九大策略组件,例如 HandlerMappingHandlerAdapterHandlerExceptionResolver 等。
  • 追问 1 :九大策略组件的默认实现是从哪里来的?
    • 加分回答 :来自 DispatcherServlet 所在包下的 DispatcherServlet.properties 文件。Spring 采用"约定优于配置"的方式提供了一套完整可用的默认实现。
  • 追问 2 :如何自定义这些策略组件?
    • 加分回答 :只需在子容器(或在 Spring Boot 的单一容器)中注册一个实现了相应接口的 Bean。DispatcherServlet 的"发现即使用"机制会自动检测并替换默认实现。
  • 追问 3 :为什么说这是模板方法模式的应用?
    • 加分回答FrameworkServlet 定义了初始化骨架(initWebApplicationContext),并在其中调用了抽象的 onRefresh() 方法。DispatcherServlet 作为子类,实现了这个特定的 onRefresh() 步骤。父类控制流程,子类实现细节,这是典型的模板方法模式。

9.问题:简述 DispatcherServlet 通过什么设计模式实现组件的可扩展性。

  • 标准回答 :主要通过策略模式 。每个策略组件(如 HandlerMapping)都定义了接口,DispatcherServlet 依赖这些接口而非具体实现。在运行时,它从 IoC 容器中动态地发现这些接口的实现类,并应用它们。
  • 追问 1 :IoC 容器在这里起到了什么作用?
    • 加分回答 :IoC 容器是策略工厂。它负责管理策略实例的生命周期和之间的依赖,并将发现策略的责任从 DispatcherServlet 转移给了自己,大大降低了两者的耦合。
  • 追问 2 :除了策略模式,还用了哪些设计模式?
    • 加分回答模板方法模式 (在 initonRefresh 流程中)、前端控制器模式DispatcherServlet本身)、适配器模式HandlerAdapter,使得 DispatcherServlet 可以适配不同类型的处理器)。
  • 追问 3 :这种"发现即使用"的扩展方式,和我们之前在 IoC 篇讲的 BeanPostProcessor 扩展点有何不同?
    • 加分回答BeanPostProcessor 是作用于 Bean 生命周期层面 的通用扩展点,可以影响几乎所有 Bean 的创建和初始化。而 DispatcherServlet 的"发现即使用"是特定于自身架构的、设计层面的策略选择 ,它只在 onRefresh() 这个特定时间点检索特定类型的 Bean。

10.问题:为什么说 Spring MVC 是"策略模式"的典型案例?

  • 标准回答 :因为 Spring MVC 将请求处理过程中的多个可变环节(映射、适配、视图解析等)都抽象成了接口(策略),并通过组合的方式让 DispatcherServlet 使用这些策略。用户可以通过向容器注册自定义实现的 Bean 来替换默认策略,而无需更改主流程代码。
  • 追问 1 :这种策略模式与直接写 if-elseswitch-case 来切换实现相比,优势在哪?
    • 加分回答 :符合开闭原则 。对扩展开放,对修改关闭。当需要新增一种 HandlerMapping 策略时,只需新建一个类并注册为 Bean,不用修改 DispatcherServlet 的核心代码
  • 追问 2 :如果在容器中注册了多个同一种策略的实现(如多个 ViewResolver),DispatcherServlet 如何处理?
    • 加分回答 :它会根据 Ordered 接口或 @Order 注解对它们进行排序,并遍历这个策略链,直到其中一个策略成功处理并返回结果。这实际上是责任链模式的体现。
  • 追问 3 :在 Spring Boot 时代,我们通常感觉不到这些策略的存在,这说明什么?
    • 加分回答 :说明 Spring Boot 的自动配置为我们提供了高度合理且开箱即用的默认策略实现,如 RequestMappingHandlerMapping,封装了复杂性,让开发者聚焦于业务。

11.问题:如何在不使用 web.xml 的情况下启动 Spring MVC?

  • 标准回答 :通过实现 WebApplicationInitializer 接口(或其便捷子类,如 AbstractAnnotationConfigDispatcherServletInitializer)。Servlet 3.0+ 规范提供了 ServletContainerInitializer,Spring 的 SpringServletContainerInitializer 会自动发现并执行所有实现了 WebApplicationInitializer 的类。
  • 追问 1SpringServletContainerInitializer 是如何被自动发现的?
    • 加分回答 :它利用了 JDK 的 SPI(Service Provider Interface) 机制。在 spring-web 模块的 META-INF/services/javax.servlet.ServletContainerInitializer 文件里,指定了 SpringServletContainerInitializer 全限定名。
  • 追问 2AbstractAnnotationConfigDispatcherServletInitializer 为我们做了什么?
    • 加分回答 :它将繁琐的编程式注册过程简化了。我们只需要覆写 getRootConfigClassesgetServletConfigClasses 两个方法,分别返回父容器和子容器的 @Configuration 类即可。它内部会自动创建 ContextLoaderListenerDispatcherServlet
  • 追问 3 :这个机制在 Spring Boot 中还有用吗?
    • 加分回答 :在 Spring Boot 的内嵌容器模式下,它不再是主启动方式,但仍然被支持。Spring Boot 自己的 SpringBootServletInitializer 在部署到外部容器时,本质上也是实现了 WebApplicationInitializer 的一个变种。

12.系统设计题:设计一个支持动态注册新模块的 Web 系统,每个模块拥有自己独立的 Spring 子容器,互不干扰,但可以共享公共的基础服务(如数据源)。请说明如何利用 Spring 的容器层次结构实现,并考虑通信隔离。

  • 标准回答
    • 容器层次 :创建一个根容器 ,存放所有公共的基础服务,如 DataSourceTransactionManager、通用 UtilityService 等。
    • 模块隔离 :为每个动态注册的模块创建一个独立的子容器,并设置根容器为其父容器。每个子容器包含其专属的 Controller、Service、Repository。
    • 动态注册 :为每个模块创建一个独立的 DispatcherServlet,并使用编程式 API(如 ServletContext.addServlet("moduleAServlet", new DispatcherServlet(moduleAContext)))动态注册它及对应的请求映射(如 /moduleA/*)。
    • 通信隔离 :模块间应避免直接通过 Spring 容器注入。
      • 同步通信 :通过定义在根容器中的事件监听机制(ApplicationEvent)。模块 A 发布事件,模块 B 监听事件,事件对象只在根容器中流转。
      • 异步通信 :使用根容器管理的消息队列,实现完全的模块解耦。
  • 追问 1 :如果某个模块频繁加载或卸载,它的子容器如何安全地销毁,避免内存泄漏?
    • 加分回答 :在卸载模块时,必须调用其子容器的 close() 方法,这会销毁所有单例 Bean,释放 JDBC 连接等资源。同时,需要调用 ServletContextremoveServlet 方法移除 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 自动配置类,在单一容器中创建和注册 DispatcherServletDispatcherServletRegistrationBean 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() 等关键方法的逐行分析。
相关推荐
绿草在线8 小时前
基于SpringBoot4+Mybatis+Thymeleaf的用户管理系统开发实战
java·spring boot·thymeleaf
麦麦大数据8 小时前
基于以太坊区块链+Spring Boot+Solidity智能合约的投票系统设计与实现
spring boot·后端·区块链·智能合约·投票系统
一 乐8 小时前
茶叶商城|基于springboot + vue茶叶商城系统(源码+数据库+文档)
java·数据库·vue.js·spring boot·论文·毕设·茶叶商城系统
鱼弦9 小时前
Git 版本控制:Spring Boot 项目的分支管理与协作
spring boot
devpotato18 小时前
Spring Boot mTLS 报 `keystore password was incorrect`:不一定是密码错了
spring boot·tls·pkcs12·mtls
keep one's resolveY19 小时前
SpringBoot实现重试机制的四种方案
java·spring boot·后端
阿丰资源21 小时前
基于Spring Boot的电影城管理系统(直接运行)
java·spring boot·后端
消失的旧时光-19431 天前
Spring Boot 工程化进阶:统一返回 + 全局异常 + AOP 通用工具包
java·spring boot·后端·aop·自定义注解