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

2 阅读41分钟

概述

衔接前文: 前文已深入剖析了 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 事件时,它便开始工作。

// 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,加载哪些配置,并最终刷新容器。

// 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 中,根容器的配置如下:

<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() 方法实现了查找或创建子容器,并与根容器建立父子关系的完整逻辑。

// 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,将初始化具体策略的细节封装在里面。

// 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* 方法都遵循几乎完全相同的逻辑。

// 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 方法是所有依赖查找的起点。

// 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 内联示例:验证访问规则

// ===== 根容器配置 (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?完全通过自动配置实现。

// 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 中不推荐,但技术上可以强制创建父子容器,例如:

@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() 被调用,<br/>直接使用 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 启动关键接口与类

接口/类所属容器核心作用调用/执行时机
ServletContextListenerServlet 容器监听 ServletContext 生命周期,用于启动和停止根容器Servlet 容器启动/关闭时。
ContextLoaderListenerRoot实现 ServletContextListener,委托 ContextLoader 创建根容器。contextInitialized 事件触发。
ContextLoaderRoot执行根容器的创建、配置和刷新操作。ContextLoaderListener 调用。
FrameworkServletServlet WebDispatcherServlet 的父类,定义了子容器创建、查找和关联父容器的核心流程DispatcherServlet.init() 过程中。
DispatcherServletServlet Web前端控制器,初始化九大策略组件,并负责协调整个请求处理流程。HttpServletBean.init()onRefresh()
WebApplicationInitializer编程式初始化入口,替代 web.xml,用于配置 Servlet、Filter 和 Listener。Servlet 容器启动时,由 SpringServletContainerInitializer 通过 SPI 发现并调用。
ServletContainerInitializerServlet 容器Servlet 3.0+ 规范定义的接口,用于动态配置 Web 应用应用启动时,Servlet 容器会通过 SPI 发现并调用其 onStartup 方法。
DispatcherServletAutoConfigurationSpring Boot App ContextBoot 自动配置类,在单一容器中创建和注册 DispatcherServletDispatcherServletRegistrationBeanSpring Boot 应用初始化,处理自动配置时。
DispatcherServletRegistrationBeanSpring 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() 等关键方法的逐行分析。