Spring Boot 嵌入式 Web 容器:Tomcat/Jetty/Undertow 生命周期整合

0 阅读34分钟

概述

衔接前文篇章

在前文《Spring Boot 启动流程与核心阶段源码剖析》中,我们完整走过了 SpringApplication.run() 的内部逻辑,并最终抵达了 IoC 容器的 refresh() 方法。我们曾强调,refresh() 方法是 Spring IoC 容器初始化的核心,其中 onRefresh() 是一个关键的模板方法钩子。对于 Spring Boot 的 Web 应用,正是在这个钩子中,启动了对嵌入式 Web 容器的创建与初始化。

本文,我们将把镜头聚焦于 onRefresh() 的内部,系统性地揭示 ServletWebServerApplicationContext 如何利用工厂模式封装 Tomcat、Jetty、Undertow 的差异,如何将 Spring 容器中定义的 ServletFilter 等组件自动注册到 Web 容器中,如何通过 BeanPostProcessor 扩展点精细调整容器参数,以及如何通过事件监听机制实现优雅关闭。本文将不仅仅描述“是什么”,更会从设计哲学的高度解读“为什么如此设计”。

总结性引言

嵌入式 Web 容器是 Spring Boot“约定优于配置”理念的基石之一。它彻底颠覆了传统 Java Web 应用需要打包为 WAR 文件并部署到外部 Servlet 容器的模式,将应用简化为一个包含所有依赖、可独立运行的 java -jar 进程。这一转变不仅简化了开发、测试与部署流程,更为微服务和云原生时代的持续交付打下了坚实的基础。

Spring Boot 并未对每种容器进行孤立的集成,而是构建了一套精妙的抽象层:通过 ServletWebServerFactory 接口统一不同容器的创建过程,通过 WebServerFactoryCustomizer 机制提供灵活的配置入口,并通过与 Spring IoC 容器生命周期的精密联动,实现了从启动、请求处理到优雅关闭的全生命周期管理。本节将深入源码,从工厂模式到线程模型,从组件自动注册到优雅停机,全景式呈现嵌入式 Web 容器在 Spring Boot 中的整合艺术。

核心要点

  • 工厂抽象ServletWebServerFactory 接口及 TomcatJettyUndertow 三大实现,是工厂方法模式的绝佳实践,使得容器的创建与使用解耦,符合开闭原则。
  • 启动时机:容器的创建严格发生在 AbstractApplicationContext.refresh()onRefresh() 钩子中,确保在 BeanFactory 准备就绪后、所有单例 Bean 实例化完成前,容器已准备完毕。
  • 自动注册ServletContextInitializerBeans 智能地从 BeanFactory 中收集所有实现了 ServletContextInitializer 接口的 Bean(包括 ServletFilterEventListener),并在容器启动时统一注册。
  • 定制扩展WebServerFactoryCustomizerBeanPostProcessor 是一个标准的 BeanPostProcessor 实现,它利用前文所述的扩展点机制,在 ServletWebServerFactory Bean 初始化后,调用所有 WebServerFactoryCustomizer 对其进行定制。
  • 优雅关闭:通过监听 ContextClosedEvent,触发 WebServershutDownGracefully 方法,允许正在处理的请求在指定时间内处理完毕,实现优雅下线。

文章组织架构图

flowchart TD
    subgraph A["1. 嵌入式容器总览"]
        direction TB
        A1["1.1 无部署哲学与ServletWebServerFactory"]
        A2["1.2 工厂模式与三大实现类"]
        A3["1.3 默认配置来源"]
    end

    subgraph B["2. 容器创建与启动"]
        direction TB
        B1["2.1 onRefresh():模板方法钩子"]
        B2["2.2 createWebServer()源码拆解"]
        B3["2.3 finishRefresh()与WebServerStartStopLifecycle"]
    end

    subgraph C["3. ServletContextInitializer"]
        direction TB
        C1["3.1 onStartup(ServletContext)"]
        C2["3.2 ServletContextInitializerBeans收集机制"]
        C3["3.3 RegistrationBean体系包装"]
    end

    subgraph D["4. WebServerFactoryCustomizer"]
        direction TB
        D1["4.1 定制器接口与函数式趋势"]
        D2["4.2 WebServerFactoryCustomizerBeanPostProcessor"]
        D3["4.3 常用定制示例"]
    end

    subgraph E["5. 线程模型与连接器"]
        direction TB
        E1["5.1 Tomcat NioEndpoint模型"]
        E2["5.2 Jetty QueuedThreadPool调优"]
        E3["5.3 Undertow XNIO配置"]
    end

    subgraph F["6. 优雅关闭"]
        direction TB
        F1["6.1 ContextClosedEvent触发机制"]
        F2["6.2 shutDownGracefully实现"]
        F3["6.3 与@PreDestroy的执行顺序"]
    end

    subgraph G["7. 容器选择与条件装配"]
        direction TB
        G1["7.1 @ConditionalOnClass决策"]
        G2["7.2 @ConditionalOnMissingBean允许覆盖"]
        G3["7.3 容器切换最佳实践"]
    end

    subgraph H["8. 生产事故排查"]
        direction TB
        H1["8.1 端口占用导致启动失败"]
        H2["8.2 优雅关闭超时导致请求中断"]
    end

    subgraph I["9. 面试高频专题"]
        direction TB
        I1["9.1 基础概念与核心接口"]
        I2["9.2 生命周期与扩展点"]
        I3["9.3 系统设计题"]
    end

    A --> B --> C --> D --> E --> F --> G --> H --> I

    classDef default fill:#f8f9fa,stroke:#333,stroke-width:1px,color:#333;
    classDef subgraphTitle fill:#e9ecef,stroke:#adb5bd,stroke-width:2px,color:#333,rx:5;
    class A,B,C,D,E,F,G,H,I subgraphTitle;

架构图分层说明

  • 总览说明:本文的组织结构遵循嵌入式容器的完整生命周期,从静态的工厂体系总览开始,逐步深入到动态的创建、注册、定制和关闭流程,最后通过容器切换、事故排查和面试专题进行收尾,形成一个从理论到实践的闭环认知路径。
  • 逐模块说明
    1. 嵌入式容器总览:建立对 ServletWebServerFactory 体系的第一印象,理解其设计意图。
    2. 容器创建与启动:剖析容器实例诞生的精确时刻和流程,这是生命周期管理的开端。
    3. ServletContextInitializer:揭示 Servlet 组件(Servlet、Filter)是如何从 Spring 容器“迁移”到 Web 容器的。
    4. WebServerFactoryCustomizer:展示如何通过 Spring 扩展点机制对容器进行非侵入式定制。
    5. 线程模型与连接器:深入容器内部,理解性能核心——线程模型的运作与调优。
    6. 优雅关闭:描述生命周期终结阶段,如何保证服务优雅退出。
    7. 容器选择与条件装配:解释 Spring Boot 如何实现不同容器的零配置切换。
    8. 生产事故排查:将理论应用于实践,分析常见问题。
    9. 面试高频专题:巩固知识,应对技术面试挑战。
  • 关键结论嵌入式容器的整合设计是 Spring Boot “约定优于配置”的最佳体现之一。它通过工厂模式封装创建细节,利用模板方法钩子将容器启动完美融入 IoC 生命周期,再结合 BeanPostProcessor 等扩展点提供定制能力,最终将复杂的基础设施管理简化为一套优雅的、可插拔的组件体系。

1. 嵌入式容器总览:ServletWebServerFactory 体系

1.1 嵌入式容器的“无部署”哲学

在传统的 Servlet 规范中,一个 Web 应用的宿命是被人为地打包成 WAR 文件,然后部署到一个独立运行、作为操作系统进程存在的 Servlet 容器(如 Tomcat、Jetty)中。这个容器负责监听端口、管理连接池、加载并运行我们的应用。

Spring Boot 的“无部署”哲学颠覆了这一点。它主张 容器就是应用的一部分。你的应用不再需要外部容器,而是从一个 main 方法启动,内嵌一个 Web 容器作为应用的普通依赖库。对于开发者和运维人员来说,应用就是一个独立的、自包含的 java -jar 进程,这极大简化了环境准备、部署和持续交付的复杂度。

为了实现这一哲学,Spring Boot 必须解决一个核心问题:如何屏蔽 Tomcat、Jetty、Undertow 等不同容器的创建和管理 API 的巨大差异?

1.2 ServletWebServerFactory 接口与工厂模式

答案就是 ServletWebServerFactory 接口。它充当了工厂方法模式中的“抽象工厂”角色,为创建 Web 服务器对象定义了一个统一的契约。

源码位置: org.springframework.boot.web.servlet.server.ServletWebServerFactory

/**
 * 用于创建 WebServer 实例的工厂接口。
 *
 * @author Phillip Webb
 * @since 2.0.0
 */
@FunctionalInterface
public interface ServletWebServerFactory {

    /**
     * 获取一个新的、已完全配置但尚未启动的 WebServer 实例。
     * 消费者(应用上下文)应该在准备好后启动该服务器。
     * @param initializers 应在服务器启动前应用到其 ServletContext 的初始化器集合
     * @return 一个已配置但未启动的 WebServer 实例
     */
    WebServer getWebServer(ServletContextInitializer... initializers);
}

这个接口极其精简,只有一个核心方法 getWebServer。它接收一个或多个 ServletContextInitializer(我们将在第3节详述),返回一个 WebServer 实例。这个设计完美体现了“最少知识原则”,调用者(ServletWebServerApplicationContext)完全不需要知道返回的 WebServer 是 Tomcat、Jetty 还是 Undertow 的实现。

下面是它的类层次结构类图:

classDiagram
    class ServletWebServerFactory {
        <<interface>>
        +getWebServer(ServletContextInitializer... initializers) WebServer
    }

    class ConfigurableServletWebServerFactory {
        <<interface>>
        +setPort(int port)
        +setContextPath(String contextPath)
        +setSession(Session session)
        +addErrorPages(ErrorPage... errorPages)
        +getWebServer(ServletContextInitializer... initializers) WebServer
    }

    class TomcatServletWebServerFactory {
        +getWebServer(ServletContextInitializer... initializers) WebServer
        +addConnectorCustomizers(TomcatConnectorCustomizer... customizers)
    }

    class JettyServletWebServerFactory {
        +getWebServer(ServletContextInitializer... initializers) WebServer
        +addServerCustomizers(JettyServerCustomizer... customizers)
    }

    class UndertowServletWebServerFactory {
        +getWebServer(ServletContextInitializer... initializers) WebServer
        +addBuilderCustomizers(UndertowBuilderCustomizer... customizers)
    }

    class AbstractServletWebServerFactory {
        <<abstract>>
        -int port
        -String contextPath
        -Session session
        +getWebServer(ServletContextInitializer... initializers) WebServer
    }

    ServletWebServerFactory <|.. ConfigurableServletWebServerFactory
    ConfigurableServletWebServerFactory <|.. AbstractServletWebServerFactory
    AbstractServletWebServerFactory <|-- TomcatServletWebServerFactory
    AbstractServletWebServerFactory <|-- JettyServletWebServerFactory
    AbstractServletWebServerFactory <|-- UndertowServletWebServerFactory
  • 图表主旨概括:该图展示了 ServletWebServerFactory 接口的完整层次结构。从顶层的功能接口,到增加配置能力的 ConfigurableServletWebServerFactory,再到提供公共实现的抽象类 AbstractServletWebServerFactory,最后落地为三个具体容器的工厂实现。
  • 逐层/逐元素分解
    • ServletWebServerFactory:系统最顶层的抽象,定义了创建WebServer的唯一方法。它是一个@FunctionalInterface,暗示其核心职责就是“生产”Web服务器。
    • ConfigurableServletWebServerFactory:扩展了顶层接口,增加了设置端口、上下文路径等通用配置的方法。这体现了接口隔离原则,基础工厂不需要关心配置。
    • AbstractServletWebServerFactory:作为支撑具体实现的抽象基类,为通用配置(如端口、上下文路径)提供了属性和基本的 getter/setter 实现,避免了在三个子类中重复编写相同代码。
    • Tomcat/Jetty/UndertowServletWebServerFactory:三个具体工厂类,各自负责构建对应容器的特有配置和实例。例如,TomcatServletWebServerFactory 持有 Tomcat 对象并调用 tomcat.start() 来启动。
  • 设计原理映射:这是经典的工厂方法模式简单工厂模式的应用。ApplicationContext 依赖抽象的 ServletWebServerFactory 接口来获取 WebServer,而不依赖任何具体实现,从而实现了创建和使用的解耦。
  • 工程联系与关键结论ApplicationContext 通过 getBean(ServletWebServerFactory.class) 即可获取当前运行时配置好的工厂实例。这套接口体系是 Spring Boot 嵌入式容器整合的基石,它使得“切换 Web 容器”从一项复杂的迁移工作简化为替换一个 Maven 依赖。

1.3 简述每种容器的默认配置来源

每个具体工厂在构造时都会设置一系列的默认值,这些默认值随后会被 application.properties/yml 中以 server. 为前缀的外部化配置所覆盖(详见本系列关于配置绑定的篇章)。

TomcatServletWebServerFactory 为例,其构造过程会设置很多 Tomcat 专有的默认值,我们通过 server.tomcat.* 配置可以精细调整这些参数。

2. 容器的创建与启动:onRefresh 的深入剖析

2.1 ServletWebServerApplicationContext.onRefresh 作为模板方法钩子

在 Spring 的 AbstractApplicationContextrefresh() 方法中,onRefresh() 是一个受保护的、空的模板方法。它允许子类在 BeanFactory 完成所有常规初始化(invokeBeanFactoryPostProcessors, registerBeanPostProcessors 等)之后,在所有单例 Bean 实例化之前,执行一些特殊的初始化逻辑。对于 Spring Boot 的 Web 上下文,这个特殊性就是“创建并初始化嵌入式 Web 容器”。

2.2 createWebServer 源码拆解

这个核心逻辑在 ServletWebServerApplicationContext 中实现。

源码位置: org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext

@Override
protected void onRefresh() {
    super.onRefresh();
    try {
        // 1. 创建 WebServer
        createWebServer();
    }
    catch (Throwable ex) {
        throw new ApplicationContextException("Unable to start web server", ex);
    }
}

private void createWebServer() {
    WebServer webServer = this.webServer;
    ServletContext servletContext = getServletContext();
    if (webServer == null && servletContext == null) {
        // 步骤1:从Bean工厂获取 ServletWebServerFactory
        ServletWebServerFactory factory = getWebServerFactory();
        // 步骤2:调用 getWebServer,传入从容器中获取的 ServletContextInitializer 集合
        this.webServer = factory.getWebServer(getSelfInitializer());
    }
    else if (servletContext != null) {
        try {
            getSelfInitializer().onStartup(servletContext);
        }
        catch (ServletException ex) {
            throw new ApplicationContextException("Cannot initialize servlet context", ex);
        }
    }
    initPropertySources();
}

解读:

  1. getWebServerFactory():此方法本质上是从当前 BeanFactory 中获取 ServletWebServerFactory 的唯一 Bean 实例。如果用户没有自定义,则会使用 Spring Boot 自动配置引入的工厂实例。
  2. getSelfInitializer():这是一个非常巧妙的设计。它返回的不是一个简单的 List,而是一个 ServletContextInitializer 的实现。这个特殊的 initializer 会在被 Web 容器回调时(即 onStartup(ServletContext) 方法被调用时),触发 prepareContext() 方法,其中就会去解析和执行所有通过自动装配机制收集到的其他 ServletContextInitializer(包含我们的 FilterServlet 等)。这就实现了延迟执行。
  3. factory.getWebServer(...):这一步是真正的“工厂制造”环节。以 TomcatServletWebServerFactory 为例,它会创建一个 Tomcat 实例,配置连接器和上下文,并将 initializers 绑定到新建的 Tomcat 实例上,最后创建并返回一个 TomcatWebServer 实例。注意,此时服务器并未启动
sequenceDiagram
    participant AppCtx as ServletWebServerAppCtx
    participant Factory as ServletWebServerFactory
    participant TomcatWServer as TomcatWebServer

    AppCtx->>AppCtx: onRefresh()
    AppCtx->>AppCtx: createWebServer()
    AppCtx->>AppCtx: getWebServerFactory()
    Note right of AppCtx: 从BeanFactory获取工厂Bean<br/>返回TomcatServletWebServerFactory
    AppCtx->>AppCtx: getSelfInitializer()
    Note right of AppCtx: 获取延迟执行的自初始化器<br/>它内部封装了对所有其他<br/>ServletContextInitializer的调用逻辑
    
    AppCtx->>Factory: getWebServer(selfInitializer)
    activate Factory
    Note right of Factory: 1. 实例化Tomcat<br/>2. 设置端口、协议等<br/>3. 创建TomcatWebServer实例<br/>4. 将selfInitializer绑定到Tomcat
    Factory->>TomcatWServer: new TomcatWebServer(tomcat, ...)
    TomcatWServer-->>Factory: 
    Factory-->>AppCtx: 返回TomcatWebServer实例
    deactivate Factory
    
    Note right of AppCtx: 此时webServer已创建但未启动<br/>赋值给 this.webServer
  • 图表主旨概括:该序列图清晰展示了 onRefreshgetWebServer 的调用链路。ApplicationContext 从其管理的 Bean 中获取工厂,并传入一个关键的 selfInitializer,最终获得一个尚未启动的 WebServer 实例。
  • 逐层/逐元素分解
    • ServletWebServerApplicationContext:作为整个流程的编排者,它知道何时需要创建 Web 容器,但不知道如何创建。
    • ServletWebServerFactory:作为被委托的对象,它知道如何创建具体的 Web 容器,所需的“原材料”(ServletContextInitializer)由编排者提供。
    • TomcatWebServer:最终被创建的产物,它内部持有真正的 org.apache.catalina.startup.Tomcat 实例,但自身作为 WebServer 接口的实现,对调用者统一了行为。
  • 设计原理映射
    • 模板方法模式AbstractApplicationContext.refresh() 是模板,onRefresh() 就是留给子类的钩子。
    • 工厂方法模式ServletWebServerFactory 是工厂接口,getWebServer() 是工厂方法。
    • 命令模式/回调机制ServletContextInitializer 可以被视为一个命令对象,它在未来的某个时机(容器初始化时)被回调执行。
  • 工程联系与关键结论onRefresh 钩子是 Spring Boot 将 Web 容器生命周期与 Spring IoC 容器生命周期“缝合”在一起的关键节点。在这个节点之后,所有和 Spring Bean 相关的扩展点(如 BeanPostProcessor)都已经就绪,这为后续的容器定制和组件注册打下了基础。

2.3 WebServer 的 start 方法调用时机

onRefresh 中创建的 WebServer 只是一个 Java 对象,其内部的 Tomcat 服务器进程并未启动。真正的启动发生在 refresh() 流程的末端。

源码位置: org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.finishRefresh()

@Override
protected void finishRefresh() {
    super.finishRefresh();
    // 这里的 this 指的是 ServletWebServerApplicationContext 实例
    WebServer webServer = startWebServer();
    if (webServer != null) {
        publishEvent(new ServletWebServerInitializedEvent(webServer, this));
    }
}

private WebServer startWebServer() {
    WebServer webServer = this.webServer;
    if (webServer != null) {
        webServer.start(); // 在此刻,Tomcat 服务真正开始监听端口
    }
    return webServer;
}

finishRefresh 在所有单例 Bean 被实例化、初始化,并且 ContextRefreshedEvent 被发布之后调用。在这个时间点启动 Web 容器是最安全的,因为它确保了所有业务 Bean(例如 Controller、Service)都已准备就绪,可以接收和处理请求。

3. ServletContextInitializer:向容器注册 Servlet 组件

3.1 ServletContextInitializer 接口

这是 Spring Boot 用来替代 Servlet 3.0+ 的 ServletContainerInitializerweb.xml 的核心接口。

源码位置: org.springframework.boot.web.servlet.ServletContextInitializer

@FunctionalInterface
public interface ServletContextInitializer {
    void onStartup(ServletContext servletContext) throws ServletException;
}

任何需要向 Web 容器注册 ServletFilterListener 等组件的逻辑,都可以封装在一个 ServletContextInitializer 实现中。Spring Boot 为我们提供了许多现成的实现,如 DispatcherServletRegistrationBeanFilterRegistrationBean

3.2 ServletContextInitializerBeans 的收集机制

ServletContextInitializerBeans 是收集和排序这些初始化器的核心类。它实现了 Collection 接口,本身就是一个 ServletContextInitializer 的集合。

源码位置: org.springframework.boot.web.servlet.ServletContextInitializerBeans 的构造方法展示了收集逻辑。

// 在 ServletContextInitializerBeans 构造函数中 (简化)
public ServletContextInitializerBeans(ListableBeanFactory beanFactory) {
    this.initializers = new LinkedMultiValueMap<>();
    // 1. 注册容器中类型为 ServletContextInitializer 的所有Bean
    addServletContextInitializerBeans(beanFactory);
    // 2. 注册由 ServletRegistrationBean, FilterRegistrationBean 等包装的组件Bean
    addAdaptableBeans(beanFactory);
    // 3. 对所有收集到的 initializer 进行排序
    List<ServletContextInitializer> sortedInitializers = this.initializers.values()
        .stream().flatMap(List::stream).collect(Collectors.toList());
    this.sortedInitializers = AnnotationAwareOrderComparator.sort(sortedInitializers);
}

private void addAdaptableBeans(ListableBeanFactory beanFactory) {
    // 获取容器中类型为 Servlet.class 的 Bean
    addAsRegistrationBean(beanFactory, Servlet.class, new ServletRegistrationBeanAdapter());
    // 获取容器中类型为 Filter.class 的 Bean
    addAsRegistrationBean(beanFactory, Filter.class, new FilterRegistrationBeanAdapter());
    // ...类似地处理 EventListener, MultipartConfigElement 等
}

addAdaptableBeans 方法非常智能,它会扫描 BeanFactory 中所有类型为 ServletFilter 的 Bean,并将它们适配为对应的 RegistrationBean,从而形成一个 ServletContextInitializer 添加到集合中。

sequenceDiagram
    participant BeanFactory as BeanFactory
    participant SCIBeans as ServletContextInitializerBeans
    participant RegBean as FilterRegistrationBean
    participant MyFilter as 自定义Filter Bean

    Note over SCIBeans: 构造方法 <br>new ServletContextInitializerBeans(beanFactory)
    SCIBeans->>BeanFactory: getBeansOfType(ServletContextInitializer.class)
    BeanFactory-->>SCIBeans: 返回如DelegatingFilterProxyRegistrationBean等
    
    SCIBeans->>BeanFactory: getBeansOfType(Servlet.class)
    BeanFactory-->>SCIBeans: 返回DispatcherServlet
    SCIBeans->>RegBean: new ServletRegistrationBean(dispatcherServlet, ...)
    RegBean-->>SCIBeans: 
    
    SCIBeans->>BeanFactory: getBeansOfType(Filter.class)
    BeanFactory-->>SCIBeans: 返回myFilter Bean
    SCIBeans->>RegBean: new FilterRegistrationBean(myFilter)
    RegBean-->>SCIBeans: 
    
    SCIBeans->>SCIBeans: AnnotationAwareOrderComparator.sort(allInitializers)
    Note over SCIBeans: 集合已排序完毕,等待被调用
  • 图表主旨概括:此序列图展示了 ServletContextInitializerBeans 构造过程中的三步收集法:直接收集 ServletContextInitializer、收集 Servlet 并包装、收集 Filter 并包装,最后统一排序。
  • 逐层/逐元素分解
    • 直接收集:收集那些本身就是 ServletContextInitializer 的 Bean,例如 DispatcherServletRegistrationBean
    • 适配收集 (Adapt):通过内置的适配器(如 FilterRegistrationBeanAdapter)将普通的 Filter Bean 包装成 FilterRegistrationBean。这些 RegistrationBean 都是 ServletContextInitializer 的子类。
    • 排序AnnotationAwareOrderComparator 会尊重 @Order 注解和 Ordered 接口,确保初始化顺序可控。
  • 设计原理映射:这是适配器模式的巧妙运用。FilterRegistrationBeanAdapter 就是一个适配器,它将 Filter 类型的 Bean 适配为 ServletContextInitializer 类型。这允许我们的业务 Filter 以纯粹 POJO 的形式存在,无需实现任何 Spring 或 Servlet 的特有接口,符合我们扩展点系列文章中强调的“无侵入”原则。
  • 工程联系与关键结论ServletContextInitializerBeans 是“将 Spring 管理的组件注入到 Servlet 容器”这一过程的自动化核心。开发者只需将自定义的 Filter 或 Service 注册为 Bean,Spring Boot 就会自动完成向 Web 容器的注册,这正是“自动配置”思想在 Web 层的延伸。

3.3 RegistrationBean 体系的包装

RegistrationBean 抽象类是 ServletContextInitializer 的子类,它为该接口增添了“动态注册”的能力。其核心方法 onStartup 会调用子类的 register 方法。

// org.springframework.boot.web.servlet.RegistrationBean (简化)
public abstract class RegistrationBean implements ServletContextInitializer, Ordered {
    @Override
    public final void onStartup(ServletContext servletContext) throws ServletException {
        register(description, servletContext);
    }
    protected abstract void register(String description, ServletContext servletContext);
}

FilterRegistrationBean 为例,它的 register 方法最终会调用 ServletContext.addFilter(),将我们的 Filter 添加到 Web 容器中。DispatcherServletRegistrationBeanregister 方法则负责添加 DispatcherServlet。这种模板方法结构保证了注册流程的一致性,同时将具体注册逻辑下放给子类。

4. WebServerFactoryCustomizer:定制容器配置

4.1 WebServerFactoryCustomizer 接口

这是一个函数式接口,用于自定义 ConfigurableServletWebServerFactory。它允许我们在 Spring 配置环境中调整嵌入式容器的配置。

源码位置: org.springframework.boot.web.server.WebServerFactoryCustomizer

@FunctionalInterface
public interface WebServerFactoryCustomizer<T extends WebServerFactory> {
    void customize(T factory);
}

4.2 WebServerFactoryCustomizerBeanPostProcessor

这是 Spring Boot 如何应用这些定制器的关键。它是一个 BeanPostProcessor这直接关联到我们第 7 篇关于 Spring 扩展点机制的文章。任何 BeanPostProcessor 都会在 Bean 实例化且属性填充后、初始化方法调用前后对 Bean 进行处理。

源码位置: org.springframework.boot.web.server.WebServerFactoryCustomizerBeanPostProcessor

public class WebServerFactoryCustomizerBeanPostProcessor implements BeanPostProcessor, ApplicationContextAware {

    private List<WebServerFactoryCustomizer<?>> customizers;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        // 从ApplicationContext中获取所有 WebServerFactoryCustomizer 类型的Bean
        this.customizers = new ArrayList<>(applicationContext
                .getBeansOfType(WebServerFactoryCustomizer.class, false, false).values());
        // 排序
        AnnotationAwareOrderComparator.sort(this.customizers);
    }

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        // 如果当前正在初始化的Bean是 WebServerFactory 的实例
        if (bean instanceof WebServerFactory) {
            // 则将收集到的所有定制器应用到该工厂Bean上
            postProcessBeforeInitialization((WebServerFactory) bean);
        }
        return bean;
    }

    private void postProcessBeforeInitialization(WebServerFactory factory) {
        // 循环调用每个定制器的 customize 方法
        this.customizers.forEach((customizer) -> customizer.customize(factory));
    }
}
sequenceDiagram
    participant AppCtx as ApplicationContext
    participant BPP as WebServerFactoryCZerBPP
    participant Customizer as MyWebServerCustomizer
    participant Factory as TomcatServletWebServerFactory

    AppCtx->>BPP: setApplicationContext(appCtx)
    activate BPP
    BPP->>AppCtx: getBeansOfType(WebServerFactoryCustomizer.class)
    AppCtx-->>BPP: [MyWebServerCustomizer]
    Note over BPP: 将MyWebServerCustomizer存入customizers列表
    deactivate BPP

    Note over Factory: Bean实例化、属性填充完毕<br/>生命周期回调之前...
    AppCtx->>BPP: postProcessBeforeInitialization(factory, "tomcatFactory")
    BPP->>BPP: 判断 factory instanceof WebServerFactory → true
    BPP->>Customizer: customize(tomcatFactory)
    activate Customizer
    Customizer->>Factory: 调用setPort(9999)、addConnectorCustomizers(...)
    deactivate Customizer
    BPP-->>AppCtx: 返回已被定制的factory
    Note over Factory: 继续后续的@PostConstruct等生命周期
  • 图表主旨概括:此序列图展示了 WebServerFactoryCustomizerBeanPostProcessor 如何作为 BeanPostProcessorWebServerFactory Bean 初始化之前,拦截并应用所有 @Bean 形式的定制器。
  • 逐层/逐元素分解
    • BeanPostProcessor 注册WebServerFactoryCustomizerBeanPostProcessor 本身会被 Spring 自动注册。
    • 收集定制器:通过 ApplicationContext 获取所有 WebServerFactoryCustomizer Bean,实现了与特定 Web 服务器工厂的解耦。
    • 前置处理:在 Web 服务器工厂 Bean 初始化(调用 @PostConstruct 等方法)之前,应用所有定制器的配置。这确保了后续的初始化逻辑能基于定制后的最终配置进行。
  • 设计原理映射:这是策略模式和观察者模式的变体。WebServerFactoryCustomizer 定义了定制“策略”,而 WebServerFactoryCustomizerBeanPostProcessor 负责观察 WebServerFactory Bean 的生命周期事件,并在合适的时机将“策略”们应用到目标对象上。
  • 工程联系与关键结论WebServerFactoryCustomizer 是在不改变自动配置代码的前提下,对嵌入式容器进行个性化配置的最佳实践。开发者只需定义一个 @Component 实现该接口,即可对整个应用容器进行定制,这再次印证了 Spring “对扩展开放,对修改关闭”的设计原则。

4.3 常用定制示例

可运行验证代码:

import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.stereotype.Component;

/**
 * 定制嵌入式Tomcat容器的示例。
 * 实现 WebServerFactoryCustomizer<ConfigurableServletWebServerFactory> 或特化版本。
 */
@Component
public class MyTomcatCustomizer implements WebServerFactoryCustomizer<TomcatServletWebServerFactory> {
    @Override
    public void customize(TomcatServletWebServerFactory factory) {
        // 设置上下文路径
        factory.setContextPath("/myapp");
        // 设置端口
        factory.setPort(9090);
        // 禁用Tomcat的默认连接器静默异常
        factory.addConnectorCustomizers(connector -> {
            // 设置连接超时时间为20秒
            connector.setProperty("connectionTimeout", "20000");
            // 设置最大线程数
            if (connector.getProtocolHandler() instanceof org.apache.coyote.http11.Http11NioProtocol) {
                ((org.apache.coyote.http11.Http11NioProtocol) connector.getProtocolHandler())
                        .setMaxThreads(500);
            }
        });
        System.out.println("⚠️ Tomcat容器已被自定义定制器调整");
    }
}

5. 线程模型与连接器定制

5.1 Tomcat NioEndpoint 模型

在 Spring Boot 2.x 中,嵌入式的 Tomcat 默认使用 NIO 连接器,其内部包含三个关键线程角色:

  • Acceptor Threads:负责接受新的 TCP 连接,并将其挂载到 Poller 的事件队列。
  • Poller Threads:负责检查已建立连接上的 I/O 事件(如是否有新数据到达),并将请求封装后交给 Worker 线程池。
  • Worker Threads:执行真正的业务逻辑,处理请求并返回响应。

通过 TomcatConnectorCustomizer 可以精细控制这些参数:

factory.addConnectorCustomizers(connector -> {
    Http11NioProtocol protocolHandler = (Http11NioProtocol) connector.getProtocolHandler();
    protocolHandler.setAcceptorThreadCount(2);  // 默认1
    protocolHandler.setPollerThreadCount(2);    // 默认每个处理器1个
    protocolHandler.setMaxThreads(200);
});

5.2 Jetty QueuedThreadPool 调优

Jetty 的线程模型相对统一,其核心是 QueuedThreadPool。它维护一个任务队列,所有 I/O 和业务处理任务都由这个线程池执行。可以通过 JettyServerCustomizer 调整:

factory.addServerCustomizers(server -> {
    QueuedThreadPool threadPool = (QueuedThreadPool) server.getThreadPool();
    threadPool.setMaxThreads(200);
    threadPool.setMinThreads(10);
    threadPool.setIdleTimeout(60000);
});

5.3 Undertow XNIO 配置

Undertow 基于 XNIO。它将线程分为 I/O threads(负责非阻塞 I/O)和 Worker threads(阻塞任务线程池)。

factory.addBuilderCustomizers(builder -> {
    builder.setIoThreads(Runtime.getRuntime().availableProcessors() * 2);
    builder.setWorkerThreads(200);
});

设计思考: Spring Boot 并没有为线程模型提供统一的抽象配置(如 server.threads.max),而是通过各个容器的特定定制器接口暴露出来。这是因为线程模型是容器性能特征的核心,高度特定化,强行统一反而会丢失关键配置能力。这是一种务实的妥协,也是对“约定优于配置”的补充——当约定不够用时,可以通过扩展点进行精确干预。

6. 优雅关闭:生命周期事件的精密联动

6.1 ContextClosedEvent 触发机制

Spring Boot 的优雅关闭能力是利用 Spring 的事件监听机制实现的。当应用上下文关闭时(例如通过 SpringApplication.exit(appContext, ...) 或发送 SIGTERM 信号),会发布一个 ContextClosedEvent

ServletWebServerApplicationContext 中,并不直接监听这个事件,而是通过一个内部的 ApplicationListener 来响应。

6.2 shutDownGracefully 实现

当上下文关闭时,WebServer 实例会被调用其 stopshutDownGracefully 方法。

源码位置: org.springframework.boot.web.embedded.tomcat.TomcatWebServer

// TomcatWebServer.shutDownGracefully()
@Override
public void shutDownGracefully(GracefulShutdownCallback callback) {
    if (this.gracefulShutdown != null) {
        this.gracefulShutdown.shutDownGracefully(callback);
    }
}

内部会委托给 Tomcat 的 GracefulShutdown 组件。它首先暂停 Connector,停止接收新请求,然后等待所有进行中的请求处理完毕,或超时。

6.3 与 @PreDestroy 的执行顺序

理解关闭顺序至关重要,可以避免资源过早释放。

sequenceDiagram
    participant AppCtx as Spring Context
    participant Listener as GracefulShutdown Listener
    participant WServer as TomcatWebServer
    participant Connector as Tomcat Connector
    participant Pool as Worker Thread Pool
    participant MyBean as 自定义@PreDestroy Bean

    Note over AppCtx: Spring应用关闭...
    AppCtx->>AppCtx: publishEvent(ContextClosedEvent)
    AppCtx->>Listener: 收到ContextClosedEvent
    activate Listener
    Listener->>WServer: shutDownGracefully(callback)
    activate WServer
    WServer->>Connector: pause() 停止接收新请求
    loop 等待进行中的请求
        WServer->>Pool: 检查活跃线程数
    end
    WServer->>Listener: callback.onShutdownComplete()
    Note over Listener: 回调中会调用webServer.stop()
    deactivate WServer
    Listener->>WServer: stop()
    deactivate Listener
    
    Note over AppCtx: 服务器完全停止...
    AppCtx->>MyBean: destroy() 或 @PreDestroy
    Note over MyBean: 执行自定义清理逻辑<br/>此时已无请求处理
  • 图表主旨概括:该图描绘了从 Spring 应用关闭到自定义 Bean 销毁的全过程,清晰地展示了 Web 容器优雅关闭和 Bean 销毁回调之间的顺序关系。
  • 逐层/逐元素分解
    • 第一阶段(绿色/红色):监听器拦截关闭事件,触发 Web 服务器的优雅关闭。此阶段 Web 容器会停止接收新请求并在宽限期内处理完存量请求。
    • 第二阶段(蓝色):Web 服务器完全停止后,Spring IoC 容器开始销毁所有管理的单例 Bean,调用 @PreDestroyDisposableBean.destroy() 方法。
  • 设计原理映射:这是 观察者模式(事件监听)和 生命周期管理模式 的结合。Spring 通过 ApplicationEvent 将关闭事件广播出去,WebServer 作为被管理的生命周期组件,响应此事件并执行自身的关闭逻辑。
  • 工程联系与关键结论这种顺序保证了在 @PreDestroy 方法中,应用内的所有请求都已经处理完毕,此时释放数据库连接池、关闭线程池等资源才是安全的。如果需要在服务器完全关闭后执行清理,可以在 ApplicationListener<ContextClosedEvent> 中,在 webServer.stop() 调用后加入逻辑。

通过 application.properties 可配置优雅关闭期:

# 开启优雅关闭,并设置等待超时时间为30秒
server.shutdown=graceful
spring.lifecycle.timeout-per-shutdown-phase=30s

7. 容器选择与条件装配:@ConditionalOnClass 的角色

7.1 @ConditionalOnClass 决策

Spring Boot 如何决定创建一个 TomcatServletWebServerFactory 还是 JettyServletWebServerFactory?答案是条件装配(关联本系列第 3 篇)。

源码位置: org.springframework.boot.autoconfigure.web.embedded.EmbeddedWebServerFactoryCustomizerAutoConfiguration

@Configuration
@ConditionalOnWebApplication
public class EmbeddedWebServerFactoryCustomizerAutoConfiguration {

    @Configuration
    @ConditionalOnClass({ Tomcat.class, UpgradeProtocol.class }) // 关键条件!
    public static class TomcatWebServerFactoryCustomizerConfiguration {
        @Bean
        public TomcatWebServerFactoryCustomizer tomcatWebServerFactoryCustomizer(
                Environment environment, ServerProperties serverProperties) {
            return new TomcatWebServerFactoryCustomizer(environment, serverProperties);
        }
    }

    @Configuration
    @ConditionalOnClass({ Server.class, ServletContextHandler.class }) // 关键条件!
    public static class JettyWebServerFactoryCustomizerConfiguration {
        // ... 创建Jetty定制器
    }

    // ...类似地配置Undertow
}

自动配置类会检查 classpath 下是否存在特定容器的核心类(如 org.apache.catalina.startup.Tomcat)。由于典型的 Spring Boot Web 项目引入了 spring-boot-starter-web,而该 Starter 默认依赖了 spring-boot-starter-tomcat,因此 Tomcat 的类存在,Tomcat 的相关自动配置会生效。

7.2 @ConditionalOnMissingBean 允许用户覆盖

如果你定义了自己的 ServletWebServerFactory Bean,那么自动配置的 Tomcat 工厂就不会创建。

@Bean
public ServletWebServerFactory myCustomFactory() {
    // 这个 Bean 的存在会让 @ConditionalOnMissingBean(ServletWebServerFactory.class)
    // 的条件不满足,从而跳过自动配置的工厂
    return new JettyServletWebServerFactory();
}

7.3 切换容器的工程实践

flowchart TD
    Start[开始] --> Check{检查classpath<br/>中的容器依赖}
    
    Check -->|存在 Tomcat.class| LoadTomcat[加载Tomcat自动配置]
    LoadTomcat --> CheckMissing{是否存在自定义<br/>ServletWebServerFactory Bean?}
    CheckMissing -->|否| CreateTomcat[创建TomcatServletWebServerFactory Bean]
    CheckMissing -->|是| SkipAuto[跳过自动创建<br/>使用用户自定义Bean]
    
    Check -->|存在 Server.class<br/>且无Tomcat| LoadJetty[加载Jetty自动配置]
    LoadJetty --> CheckMissing
    
    Check -->|存在 Undertow.class<br/>且无Tomcat/Jetty| LoadUndertow[加载Undertow自动配置]
    LoadUndertow --> CheckMissing
    
    CreateTomcat --> End[应用启动]
    SkipAuto --> End
  • 图表主旨概括:此决策流程图展示了 Spring Boot 在启动时,依据 classpath 和 Spring 容器中的 Bean 定义,自动决定创建哪个 Web 容器工厂的完整逻辑通路。
  • 逐层/逐元素分解
    • Classpath 检查:第一决定要素。Spring Boot 通过 @ConditionalOnClass 扫描关键类。如果一个项目同时引入了 tomcatjetty 依赖,Spring Boot 有内部优先级或会因冲突而启动失败。
    • 用户 Bean 检查:第二决定要素。@ConditionalOnMissingBean 提供了一个优雅的覆盖点。一旦用户定义了优先级更高的 Factory Bean,自动配置的默认实现就会退让。
  • 设计原理映射责任链模式/策略选择。自动配置类按照一定顺序检查多个条件,第一个匹配成功的条件将定义后续使用的策略(即哪个工厂),整个决策过程清晰且可扩展。
  • 工程联系与关键结论实现 Web 容器的切换,理论上只需两步:1. 排除 spring-boot-starter-tomcat 依赖;2. 引入 spring-boot-starter-jetty 依赖。Spring Boot 的条件装配机制会在运行时自动适配,无需任何代码修改。

8. 生产事故排查专题

案例一:嵌入式容器端口被抢占导致应用启动失败

  • 现象: 某微服务应用在发布时,采用滚动重启策略。在老旧容器实例尚未完全释放端口前,新实例尝试启动,日志中抛出 java.net.BindException: Address already in use 或 Spring Boot 包装后的 PortInUseException,新实例启动失败并退出,导致发布中断。

  • 排查思路

    1. 发布系统立刻观察到新 Pod/容器状态为 CrashLoopBackOff
    2. 查看启动日志,发现包含 The Tomcat connector configured to listen on port 8080 failed to start. The port may already be in use or the connector may be misconfigured. 的异常堆栈。
    3. 确认 application.properties 中配置的 server.port=8080 未被修改。
  • 根因分析TomcatWebServer.start() 方法内部会调用 Tomcat.start(),最终会尝试绑定 Connector 监听的端口(默认 8080)。由于旧容器还未完全关闭,操作系统层面的 TIME_WAITCLOSE_WAIT 状态的 TCP 连接仍持有该端口,导致新进程 bind() 系统调用失败。这是一个经典的 TCP 端口资源竞争问题。

  • 解决方案

    • 临时修复:向运维申请一段额外的端口号段,通过环境变量 SERVER_PORT 为每个实例动态分配端口。
    • 根本修复:优化滚动发布策略,加入 preStop 钩子脚本,在停止旧容器前,先调用应用的 actuator/shutdown 优雅下线端点,并确保 DockerfileK8s Pod 配置中设置了足够长的优雅停止时间(terminationGracePeriodSeconds),以确保旧容器完全停止释放端口后,新容器才开始启动。
  • 最佳实践

    1. 必须开启优雅关闭server.shutdown=graceful,并配合同等长的 terminationGracePeriodSeconds
    2. 使用 SO_REUSEADDR:谨慎设置此选项,它允许新连接绑定到 TIME_WAIT 状态的端口,但也可能带来接收脏数据的问题,通常不推荐作为首选方案。
    3. 端口偏移:在 PaaS 或类似环境中,为实例设置随机端口(server.port=0)并由注册中心发现是更好的解耦方式。

案例二:优雅关闭超时导致请求被强制断开

  • 现象: 应用在发布期间,总是有少量客户端调用方报告 500 或连接被重置的错误,日志中出现未处理完毕的 IOException: Broken pipe 或请求处理线程中断。

  • 排查思路

    1. 对比发布工具和应用日志的时间戳。发现客户端报错的时间点,恰好是应用日志中 Commencing graceful shutdown... 之后 30 秒左右。
    2. 检查配置,发现有 spring.lifecycle.timeout-per-shutdown-phase=30s
    3. 分析业务逻辑,发现有一个导出报表的接口,处理时间平均在 45 秒左右。
  • 根因分析: 在发出关闭信号后,Web 容器进入优雅关闭期。它等待当前所有正在处理的请求完成。然而,由于我们有一批处理时间超过 30 秒的慢请求,30 秒的宽限期结束仍然没有完成。TomcatWebServer 会强制中断工作线程池,导致这些请求的 TCP 连接被非正常撕裂,客户端看到连接重置或 500 错误。源码中,GracefulShutdown 的关闭会等待工作线程池终止,超时后会强制清空。

  • 解决方案

    1. 调整宽限期:将 spring.lifecycle.timeout-per-shutdown-phase 调整为 120s,大于我们已知的最长请求处理时间。
    2. 优化慢请求:从根本上对导出报表等慢请求进行异步化改造(如使用 DeferredResult 或消息队列),使其不再长时间占用 Tomcat 工作线程。
  • 最佳实践

    1. 优雅关闭的宽限期应基于全链路请求耗时的峰值(P99.9)来设定,不能凭感觉。
    2. FilterInterceptor 中加入“优雅关闭就绪”检查,对于 /health 等探测端口,在关闭期内返回非 200 状态码,让 K8s Service/负载均衡器提前摘除流量,减少新请求流入。

9. 面试高频专题

1. Spring Boot 是如何集成嵌入式 Tomcat的?核心接口是什么?

  • 标准回答:主要通过 ServletWebServerFactory 接口及其实现 TomcatServletWebServerFactory 进行集成。Spring 上下文通过工厂获取 WebServer 实例,而 TomcatWebServer 内部封装了对原生 Tomcat API 的操作。
  • 多角度追问
    • ServletWebServerFactory 这个接口的核心方法是什么?—— getWebServer(ServletContextInitializer... initializers)
    • 为什么要有这个接口?—— 屏蔽底层容器差异,提供统一抽象,遵循开闭原则。
    • 用户如何替换掉默认的 Tomcat?—— 排除 Tomcat 依赖,引入 Jetty Starter,Spring Boot 的条件装配会自动切换。
  • 加分回答:深入说明 ConfigurableServletWebServerFactoryAbstractServletWebServerFactory 在这个体系中的作用,以及如何通过 TomcatConnectorCustomizer 等特定接口在自动配置之后进行精细的二阶段配置。

2. 嵌入式容器是在哪个阶段启动的?为什么是这个时机?

  • 标准回答:在 AbstractApplicationContextrefresh() 方法的末尾阶段,具体是 ServletWebServerApplicationContextfinishRefresh() 方法中启动。
  • 多角度追问
    • onRefresh 里不是已经 createWebServer 了吗?—— createWebServer 只是 new 了一个 TomcatWebServer 对象,并没有调用 start,相当于准备好了一个配置完毕但未开机的服务器。
    • 为什么在 finishRefresh 启动?—— 此时所有单例 Bean 已初始化完毕,ContextRefreshedEvent 已发布,确保服务器启动后所有业务组件都已就绪。
    • 如果我想在容器启动后立即执行一段代码,有哪些方式可以确保端口已开?—— 监听 ApplicationReadyEvent 或使用 ApplicationRunner/CommandLineRunner,它们都在 finishRefresh 之后调用。

3. 自定义的 Filter 是如何自动注册到 Web 容器的?

  • 标准回答:通过 ServletContextInitializerBeans,它会扫描 BeanFactory 中的所有 Filter 类型的 Bean,并使用 FilterRegistrationBeanAdapter 将其包装成一个 FilterRegistrationBean(它是一个 ServletContextInitializer),最终在 Web 容器启动时,通过调用 FilterRegistrationBean.onStartup() 方法将其动态添加到 ServletContext 中。
  • 多角度追问
    • FilterRegistrationBean@WebFilter 注解有什么区别?—— 前者是 Spring Boot 的方式,与 Spring 容器集成更好,可以依赖注入、排序等;后者是原生 Servlet 3.0+ 注解,需配合 @ServletComponentScan 使用,能力相对受限。
    • 多个 Filter 的顺序如何保证?—— 通过实现 Ordered 接口或使用 @Order 注解,ServletContextInitializerBeans 在构造时会对所有初始化器排序。
    • 如果我想对 Filter 的 dispatcherTypes 进行设置怎么办?—— 返回一个 FilterRegistrationBean 类型的 Bean 而不是直接返回 Filter Bean,这样可以进行全量配置。

4. 如何定制嵌入式 Tomcat 的最大线程数?

  • 标准回答:定义一个 WebServerFactoryCustomizer<TomcatServletWebServerFactory> Bean,在 customize 方法内通过 factory.addConnectorCustomizers 获取 Http11NioProtocol 并设置 setMaxThreads
  • 多角度追问
    • 直接在 application.properties 里配置 server.tomcat.threads.max 不行吗?—— Spring Boot 2.x 默认配置中并没有这项。server.tomcat.* 下提供的是通用的 Tomcat 配置,而线程数被认为是高度定制项,需要通过定制器完成。
    • WebServerFactoryCustomizerBeanPostProcessor 是怎么工作的?—— 它是一个 BeanPostProcessor,它会在 ServletWebServerFactory Bean 初始化之前,从 Spring 容器中收集所有的 WebServerFactoryCustomizer 并换个应用到工厂上。
    • 该处理器如何知道要应用哪个定制器?—— 通过泛型 T 来限定,customizers.forEach(customizer -> customizer.customize(factory)) 时会在 JVM 层面进行类型检查和转换。

5. 优雅关闭是如何实现的?如果优雅关闭超时了怎么办?

  • 标准回答:通过监听 ContextClosedEvent,调用 WebServershutDownGracefully 方法。该方法会先停止接收新请求,然后等待进行中请求处理完毕或超时。核心配置是 server.shutdown=gracefulspring.lifecycle.timeout-per-shutdown-phase
  • 多角度追问
    • 关闭时,@PreDestroy 和优雅关闭谁的优先级高?—— 优雅关闭先发生。Web 容器先停止,然后才销毁 bean。
    • 超时后会发生什么?—— 容器会被强制停止,所有未完成的请求会被中断,客户端将收到连接错误。
    • 如果我的业务中有处理时间非常长的请求(比如 WebSocket 长连接),如何实现更复杂的关闭逻辑?—— 可以在关闭前先让注册中心摘除服务,然后利用 ApplicationListener<ContextClosedEvent> 自定义关闭逻辑,例如先断开所有 WebSocket 连接,再等待一段时间,最后才让 Spring 容器继续关闭。

6. Spring Boot 是如何决定使用 Tomcat 还是 Jetty 的?

  • 标准回答:通过 @ConditionalOnClass 条件注解。Spring Boot 的自动配置会检查 classpath 下是否存在 org.apache.catalina.startup.Tomcatorg.eclipse.jetty.server.Server 类,来决定加载哪个容器的自动化配置。
  • 多角度追问
    • 如果 classpath 下同时有两个容器的依赖,会怎样?—— 通常会导致 NoUniqueBeanDefinitionException,因为 Spring 会尝试创建两个 ServletWebServerFactory 的 Bean。此时需要手动排除一个。
    • @ConditionalOnMissingBean(ServletWebServerFactory.class) 起了什么作用?—— 它允许用户通过定义一个 ServletWebServerFactory 类型的 Bean 来完全覆盖自动配置逻辑,实现最高优先级的自定义。
    • 切换容器时,仅仅替换依赖就够了吗?—— 一般情况下足够。但如果旧容器的特有定制器(如 TomcatConnectorCustomizer 的某些 Bean)仍在上下文中,它们会被忽略,不会报错,但最好清理掉,保持配置干净。

7. WebServerFactoryCustomizer 和直接自定义 ServletWebServerFactory Bean 的区别是什么?

  • 标准回答WebServerFactoryCustomizer 的优点在于可以定义多个,互相不覆盖,通过组合方式共同调整一个工厂 Bean。而直接自定义一个 ServletWebServerFactory Bean 会完全替代默认的自动配置,是一种更高侵入性但也更彻底的方式。
  • 多角度追问
    • 什么场景下必须使用自定义 Bean 的方式?—— 当需要完全改变工厂的行为时,例如某些设置没有通过 Configurable...Factory 接口暴露出来,或者你想在代码中完全控制 Tomcat 的构建过程。
    • 多个定制器的执行顺序如何控制?—— 通过 @Ordered 注解或 Ordered 接口来控制WebServerFactoryCustomizer Bean的加载顺序。
    • 定制器在什么时机被执行?—— 它在 WebServerFactoryCustomizerBeanPostProcessor.postProcessBeforeInitialization 阶段执行,发生在工厂 Bean 的 @PostConstruct 之前。

8. DispatcherServletRegistrationBean 的作用是什么?如何注册多个 DispatcherServlet?

  • 标准回答:它将 Spring MVC 的核心 DispatcherServlet 注册到 Servlet 容器中。默认映射为 /。要注册多个,只需声明多个 DispatcherServletRegistrationBean Bean,并为它们指定不同的 Servlet 名称和 URL 映射。
  • 多角度追问
    • 多个 DispatcherServlet 的场景是什么?—— 例如为 REST API(映射 /api/*)和前端页面(映射 /app/*)使用不同的上下文配置。
    • 它们之间的 ApplicationContext 有何关系?—— 它们是独立的子上下文,可以拥有完全不同的 Bean 工厂和配置,实现模块隔离。
    • 如何确保 Spring Boot 的自动配置只影响主 DispatcherServlet 的上下文?—— 自动配置只在根上下文中工作。对于多个 DispatcherServlet 场景,通常需要手动构建子上下文。

9. Undertow 和 Tomcat 在 Spring Boot 中的默认线程模型有什么不同?

  • 标准回答:Tomcat 默认使用 NIO 模型,有专门的 Acceptor、Poller 和 Worker 线程;而 Undertow 基于 XNIO,将线程明确分为 I/O 线程和 Worker 线程,I/O 线程处理非阻塞任务,Worker 线程处理阻塞 Servlet 请求。
  • 多角度追问
    • 这种差异对性能有什么影响?—— Undertow 在大并发连接和长连接场景下通常有更低的内存占用和更优表现,因为它避免了线程与连接的强绑定。Tomcat NIO 模型也非常成熟,经受了广大生态考验,社区支持更丰富。
    • 如何定制 Undertow 的 I/O 和 Worker 线程数?—— 通过实现 WebServerFactoryCustomizer<UndertowServletWebServerFactory>,在其 customize 方法中 factory.addBuilderCustomizers(...) 来设置 ioThreadsworkerThreads
    • 为什么 Spring Boot 默认不统一线程配置?—— 因为线程模型是各容器核心特征的体现,强行统一会丧失各容器的优势。Spring Boot 选择将这部分交由容器的具体定制接口暴露,符合务实的设计原则。

10. 如何在容器启动后执行初始化逻辑,并且要确保容器端口已经打开?

  • 标准回答:实现 ApplicationRunnerCommandLineRunner 接口,或者监听 ApplicationReadyEvent 事件。它们都在 finishRefresh 完成、Web 服务器完全启动之后执行。
  • 多角度追问
    • ApplicationRunner@PostConstruct 的区别?—— @PostConstruct 在 Bean 初始化时执行,此时 Web 服务器很可能还未启动。ApplicationRunner 在应用完全就绪后执行,端口已监听。
    • 如果我的初始化逻辑需要发 HTTP 请求到自己应用,这几个方法都能保证成功吗?—— 只有 ApplicationRunner/CommandLineRunner/ApplicationReadyEvent 才能保证,因为它们执行时端口已监听。
    • 如何控制多个 ApplicationRunner 的执行顺序?—— 实现 Ordered 接口或使用 @Order 注解。

11. 切换容器时,为什么有时仍会触发旧容器的条件配置?

  • 标准回答:最可能的原因是旧容器的依赖未被完全排除,其类仍然存在于 classpath 中,导致 @ConditionalOnClass 条件依然成立。
  • 多角度追问
    • 如何彻底排除?—— 使用 Maven 的 dependency:tree 命令分析依赖树,找出引入旧容器的传递性依赖,并进行排除。
    • 除了排除依赖,还有其他方案吗?—— 可以使用 @EnableAutoConfiguration(exclude = ...) 显式地排除旧容器的自动配置类,但这治标不治本。
    • 如果我既想用 Tomcat 又想用 Jetty 的某些工具类,怎么办?—— 这极其危险,引入了不必要的复杂度。应该严格遵循“一个应用一个容器”的原则。

12. (系统设计题)设计一个支持动态切换 Web 容器的 Starter

  • 标准回答
    // 1. 核心配置类
    @Configuration
    public class SwappableWebContainerAutoConfiguration {
    
        // 2. 自定义条件注解,基于我们的配置项
        @Conditional(OnContainerTypeCondition.class)
        @Bean
        @ConditionalOnMissingBean
        public ServletWebServerFactory customWebServerFactory() {
            // 根据 environment.getProperty("web.container.type") 来读取配置
            String type = environment.getProperty("web.container.type", "tomcat");
            if ("jetty".equals(type)) {
                return new JettyServletWebServerFactory();
            } else if ("undertow".equals(type)) {
                return new UndertowServletWebServerFactory();
            } else {
                return new TomcatServletWebServerFactory();
            }
        }
    }
    
    // 3. 实现自定义 Condition 类
    public class OnContainerTypeCondition implements Condition {
        @Override
        public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
            // 只有当配置项明确指定了容器类型时,这个条件才匹配,从而激活上面的工厂
            return context.getEnvironment().containsProperty("web.container.type");
        }
    }
    
    // 4. 在 META-INF/spring.factories 中注册该自动配置
    // org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
    // com.example.SwappableWebContainerAutoConfiguration
    
  • 多角度追问
    • 如果用户没配 web.container.type,你的 Starter 和 Spring Boot 默认的自动配置都会生效,怎么办?—— 这正是设计的关键。我们的自定义 Condition (OnContainerTypeCondition) 会在没有配置时返回 false。同时,Spring Boot 默认的 EmbeddedWebServerFactoryCustomizerAutoConfiguration 上的 @ConditionalOnMissingBean(ServletWebServerFactory.class) 会在我们没提供工厂 Bean 时正常生效。
    • 如何为不同的容器加载不同的定制器?—— 可以在配置类中定义自定义的定制器 Bean,并且在方法上使用 @ConditionalOnBean(JettyServletWebServerFactory.class) 等条件注解,使其精准匹配。
    • 如果配置项在运行时动态变更了,比如通过配置中心,你的方案还能工作吗?—— 不能。ServletWebServerFactory 的创建发生在 onRefresh 阶段,这是应用的一次性初始化过程。要动态切换容器,需要在启动脚本中修改环境变量或 JVM 参数,然后重启应用。

附录:嵌入式容器核心接口速查表

核心接口/类作用在生命周期中的位置
ServletWebServerFactory抽象工厂接口,用于创建 WebServer 实例。创建期
Tomcat/Jetty/UndertowServletWebServerFactory具体工厂实现,封装了特定容器的构建细节。创建期
WebServer ( TomcatWebServer )代表一个正在运行或可管理的 Web 服务器实例。启动/运行/关闭
ServletContextInitializer回调接口,用于在 ServletContext 启动时执行初始化逻辑。启动期
ServletContextInitializerBeans收集并排序应用中所有 ServletContextInitializer 的工具类。启动期
RegistrationBeanServletFilter 等组件提供动态注册到容器的模板。启动期
WebServerFactoryCustomizer策略接口,用于自定义 WebServerFactory配置期
WebServerFactoryCustomizerBeanPostProcessorBeanPostProcessor 实现,负责将定制器应用到工厂。配置期
GracefulShutdownCallback优雅关闭完成后的回调接口。关闭期
@ConditionalOnClass条件注解,根据 classpath 中的类决定配置是否生效。配置解析期

延伸阅读

  1. Spring Boot 官方文档 - “Embedded Web Servers” 部分
  2. 《Spring Boot 编程思想》 - 小马哥(mercyblitz) 著。对理解 Spring Boot 的设计哲学和自动装配有极大帮助。
  3. Apache Tomcat 官方文档 - NIO Connector。深入理解 Tomcat 线程模型。
  4. Eclipse Jetty 官方文档 - Architecture。了解 Jetty 的核心架构。
  5. Undertow 官方文档 - Undertow Core。了解 XNIO 和核心配置。