概述
衔接前文篇章
在前文《Spring Boot 启动流程与核心阶段源码剖析》中,我们完整走过了 SpringApplication.run() 的内部逻辑,并最终抵达了 IoC 容器的 refresh() 方法。我们曾强调,refresh() 方法是 Spring IoC 容器初始化的核心,其中 onRefresh() 是一个关键的模板方法钩子。对于 Spring Boot 的 Web 应用,正是在这个钩子中,启动了对嵌入式 Web 容器的创建与初始化。
本文,我们将把镜头聚焦于 onRefresh() 的内部,系统性地揭示 ServletWebServerApplicationContext 如何利用工厂模式封装 Tomcat、Jetty、Undertow 的差异,如何将 Spring 容器中定义的 Servlet、Filter 等组件自动注册到 Web 容器中,如何通过 BeanPostProcessor 扩展点精细调整容器参数,以及如何通过事件监听机制实现优雅关闭。本文将不仅仅描述“是什么”,更会从设计哲学的高度解读“为什么如此设计”。
总结性引言
嵌入式 Web 容器是 Spring Boot“约定优于配置”理念的基石之一。它彻底颠覆了传统 Java Web 应用需要打包为 WAR 文件并部署到外部 Servlet 容器的模式,将应用简化为一个包含所有依赖、可独立运行的 java -jar 进程。这一转变不仅简化了开发、测试与部署流程,更为微服务和云原生时代的持续交付打下了坚实的基础。
Spring Boot 并未对每种容器进行孤立的集成,而是构建了一套精妙的抽象层:通过 ServletWebServerFactory 接口统一不同容器的创建过程,通过 WebServerFactoryCustomizer 机制提供灵活的配置入口,并通过与 Spring IoC 容器生命周期的精密联动,实现了从启动、请求处理到优雅关闭的全生命周期管理。本节将深入源码,从工厂模式到线程模型,从组件自动注册到优雅停机,全景式呈现嵌入式 Web 容器在 Spring Boot 中的整合艺术。
核心要点
- 工厂抽象:
ServletWebServerFactory接口及Tomcat、Jetty、Undertow三大实现,是工厂方法模式的绝佳实践,使得容器的创建与使用解耦,符合开闭原则。 - 启动时机:容器的创建严格发生在
AbstractApplicationContext.refresh()的onRefresh()钩子中,确保在BeanFactory准备就绪后、所有单例 Bean 实例化完成前,容器已准备完毕。 - 自动注册:
ServletContextInitializerBeans智能地从BeanFactory中收集所有实现了ServletContextInitializer接口的 Bean(包括Servlet、Filter、EventListener),并在容器启动时统一注册。 - 定制扩展:
WebServerFactoryCustomizerBeanPostProcessor是一个标准的BeanPostProcessor实现,它利用前文所述的扩展点机制,在ServletWebServerFactoryBean 初始化后,调用所有WebServerFactoryCustomizer对其进行定制。 - 优雅关闭:通过监听
ContextClosedEvent,触发WebServer的shutDownGracefully方法,允许正在处理的请求在指定时间内处理完毕,实现优雅下线。
文章组织架构图
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;
架构图分层说明
- 总览说明:本文的组织结构遵循嵌入式容器的完整生命周期,从静态的工厂体系总览开始,逐步深入到动态的创建、注册、定制和关闭流程,最后通过容器切换、事故排查和面试专题进行收尾,形成一个从理论到实践的闭环认知路径。
- 逐模块说明:
- 嵌入式容器总览:建立对
ServletWebServerFactory体系的第一印象,理解其设计意图。 - 容器创建与启动:剖析容器实例诞生的精确时刻和流程,这是生命周期管理的开端。
- ServletContextInitializer:揭示 Servlet 组件(Servlet、Filter)是如何从 Spring 容器“迁移”到 Web 容器的。
- WebServerFactoryCustomizer:展示如何通过 Spring 扩展点机制对容器进行非侵入式定制。
- 线程模型与连接器:深入容器内部,理解性能核心——线程模型的运作与调优。
- 优雅关闭:描述生命周期终结阶段,如何保证服务优雅退出。
- 容器选择与条件装配:解释 Spring Boot 如何实现不同容器的零配置切换。
- 生产事故排查:将理论应用于实践,分析常见问题。
- 面试高频专题:巩固知识,应对技术面试挑战。
- 嵌入式容器总览:建立对
- 关键结论:嵌入式容器的整合设计是 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 的 AbstractApplicationContext 的 refresh() 方法中,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();
}
解读:
getWebServerFactory():此方法本质上是从当前BeanFactory中获取ServletWebServerFactory的唯一 Bean 实例。如果用户没有自定义,则会使用 Spring Boot 自动配置引入的工厂实例。getSelfInitializer():这是一个非常巧妙的设计。它返回的不是一个简单的List,而是一个ServletContextInitializer的实现。这个特殊的initializer会在被 Web 容器回调时(即onStartup(ServletContext)方法被调用时),触发prepareContext()方法,其中就会去解析和执行所有通过自动装配机制收集到的其他ServletContextInitializer(包含我们的Filter、Servlet等)。这就实现了延迟执行。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
- 图表主旨概括:该序列图清晰展示了
onRefresh到getWebServer的调用链路。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+ 的 ServletContainerInitializer 和 web.xml 的核心接口。
源码位置: org.springframework.boot.web.servlet.ServletContextInitializer
@FunctionalInterface
public interface ServletContextInitializer {
void onStartup(ServletContext servletContext) throws ServletException;
}
任何需要向 Web 容器注册 Servlet、Filter、Listener 等组件的逻辑,都可以封装在一个 ServletContextInitializer 实现中。Spring Boot 为我们提供了许多现成的实现,如 DispatcherServletRegistrationBean、FilterRegistrationBean。
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 中所有类型为 Servlet、Filter 的 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)将普通的FilterBean 包装成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 容器中。DispatcherServletRegistrationBean 的 register 方法则负责添加 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如何作为BeanPostProcessor在WebServerFactoryBean 初始化之前,拦截并应用所有@Bean形式的定制器。 - 逐层/逐元素分解:
BeanPostProcessor注册:WebServerFactoryCustomizerBeanPostProcessor本身会被 Spring 自动注册。- 收集定制器:通过
ApplicationContext获取所有WebServerFactoryCustomizerBean,实现了与特定 Web 服务器工厂的解耦。 - 前置处理:在 Web 服务器工厂 Bean 初始化(调用
@PostConstruct等方法)之前,应用所有定制器的配置。这确保了后续的初始化逻辑能基于定制后的最终配置进行。
- 设计原理映射:这是策略模式和观察者模式的变体。
WebServerFactoryCustomizer定义了定制“策略”,而WebServerFactoryCustomizerBeanPostProcessor负责观察WebServerFactoryBean 的生命周期事件,并在合适的时机将“策略”们应用到目标对象上。 - 工程联系与关键结论:
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 实例会被调用其 stop 或 shutDownGracefully 方法。
源码位置: 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,调用
@PreDestroy或DisposableBean.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扫描关键类。如果一个项目同时引入了tomcat和jetty依赖,Spring Boot 有内部优先级或会因冲突而启动失败。 - 用户 Bean 检查:第二决定要素。
@ConditionalOnMissingBean提供了一个优雅的覆盖点。一旦用户定义了优先级更高的 Factory Bean,自动配置的默认实现就会退让。
- Classpath 检查:第一决定要素。Spring Boot 通过
- 设计原理映射:责任链模式/策略选择。自动配置类按照一定顺序检查多个条件,第一个匹配成功的条件将定义后续使用的策略(即哪个工厂),整个决策过程清晰且可扩展。
- 工程联系与关键结论:实现 Web 容器的切换,理论上只需两步:1. 排除
spring-boot-starter-tomcat依赖;2. 引入spring-boot-starter-jetty依赖。Spring Boot 的条件装配机制会在运行时自动适配,无需任何代码修改。
8. 生产事故排查专题
案例一:嵌入式容器端口被抢占导致应用启动失败
-
现象: 某微服务应用在发布时,采用滚动重启策略。在老旧容器实例尚未完全释放端口前,新实例尝试启动,日志中抛出
java.net.BindException: Address already in use或 Spring Boot 包装后的PortInUseException,新实例启动失败并退出,导致发布中断。 -
排查思路:
- 发布系统立刻观察到新 Pod/容器状态为
CrashLoopBackOff。 - 查看启动日志,发现包含
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.的异常堆栈。 - 确认
application.properties中配置的server.port=8080未被修改。
- 发布系统立刻观察到新 Pod/容器状态为
-
根因分析:
TomcatWebServer.start()方法内部会调用Tomcat.start(),最终会尝试绑定Connector监听的端口(默认 8080)。由于旧容器还未完全关闭,操作系统层面的TIME_WAIT或CLOSE_WAIT状态的 TCP 连接仍持有该端口,导致新进程bind()系统调用失败。这是一个经典的 TCP 端口资源竞争问题。 -
解决方案:
- 临时修复:向运维申请一段额外的端口号段,通过环境变量
SERVER_PORT为每个实例动态分配端口。 - 根本修复:优化滚动发布策略,加入
preStop钩子脚本,在停止旧容器前,先调用应用的actuator/shutdown优雅下线端点,并确保Dockerfile或K8sPod 配置中设置了足够长的优雅停止时间(terminationGracePeriodSeconds),以确保旧容器完全停止释放端口后,新容器才开始启动。
- 临时修复:向运维申请一段额外的端口号段,通过环境变量
-
最佳实践:
- 必须开启优雅关闭:
server.shutdown=graceful,并配合同等长的terminationGracePeriodSeconds。 - 使用
SO_REUSEADDR:谨慎设置此选项,它允许新连接绑定到TIME_WAIT状态的端口,但也可能带来接收脏数据的问题,通常不推荐作为首选方案。 - 端口偏移:在 PaaS 或类似环境中,为实例设置随机端口(
server.port=0)并由注册中心发现是更好的解耦方式。
- 必须开启优雅关闭:
案例二:优雅关闭超时导致请求被强制断开
-
现象: 应用在发布期间,总是有少量客户端调用方报告
500或连接被重置的错误,日志中出现未处理完毕的IOException: Broken pipe或请求处理线程中断。 -
排查思路:
- 对比发布工具和应用日志的时间戳。发现客户端报错的时间点,恰好是应用日志中
Commencing graceful shutdown...之后 30 秒左右。 - 检查配置,发现有
spring.lifecycle.timeout-per-shutdown-phase=30s。 - 分析业务逻辑,发现有一个导出报表的接口,处理时间平均在 45 秒左右。
- 对比发布工具和应用日志的时间戳。发现客户端报错的时间点,恰好是应用日志中
-
根因分析: 在发出关闭信号后,Web 容器进入优雅关闭期。它等待当前所有正在处理的请求完成。然而,由于我们有一批处理时间超过 30 秒的慢请求,30 秒的宽限期结束仍然没有完成。
TomcatWebServer会强制中断工作线程池,导致这些请求的 TCP 连接被非正常撕裂,客户端看到连接重置或 500 错误。源码中,GracefulShutdown的关闭会等待工作线程池终止,超时后会强制清空。 -
解决方案:
- 调整宽限期:将
spring.lifecycle.timeout-per-shutdown-phase调整为120s,大于我们已知的最长请求处理时间。 - 优化慢请求:从根本上对导出报表等慢请求进行异步化改造(如使用
DeferredResult或消息队列),使其不再长时间占用 Tomcat 工作线程。
- 调整宽限期:将
-
最佳实践:
- 优雅关闭的宽限期应基于全链路请求耗时的峰值(P99.9)来设定,不能凭感觉。
- 在
Filter或Interceptor中加入“优雅关闭就绪”检查,对于/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 的条件装配会自动切换。
- 加分回答:深入说明
ConfigurableServletWebServerFactory和AbstractServletWebServerFactory在这个体系中的作用,以及如何通过TomcatConnectorCustomizer等特定接口在自动配置之后进行精细的二阶段配置。
2. 嵌入式容器是在哪个阶段启动的?为什么是这个时机?
- 标准回答:在
AbstractApplicationContext的refresh()方法的末尾阶段,具体是ServletWebServerApplicationContext的finishRefresh()方法中启动。 - 多角度追问:
- 在
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 而不是直接返回FilterBean,这样可以进行全量配置。
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,它会在ServletWebServerFactoryBean 初始化之前,从 Spring 容器中收集所有的WebServerFactoryCustomizer并换个应用到工厂上。- 该处理器如何知道要应用哪个定制器?—— 通过泛型
T来限定,customizers.forEach(customizer -> customizer.customize(factory))时会在 JVM 层面进行类型检查和转换。
- 直接在
5. 优雅关闭是如何实现的?如果优雅关闭超时了怎么办?
- 标准回答:通过监听
ContextClosedEvent,调用WebServer的shutDownGracefully方法。该方法会先停止接收新请求,然后等待进行中请求处理完毕或超时。核心配置是server.shutdown=graceful和spring.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.Tomcat或org.eclipse.jetty.server.Server类,来决定加载哪个容器的自动化配置。 - 多角度追问:
- 如果 classpath 下同时有两个容器的依赖,会怎样?—— 通常会导致
NoUniqueBeanDefinitionException,因为 Spring 会尝试创建两个ServletWebServerFactory的 Bean。此时需要手动排除一个。 @ConditionalOnMissingBean(ServletWebServerFactory.class)起了什么作用?—— 它允许用户通过定义一个ServletWebServerFactory类型的 Bean 来完全覆盖自动配置逻辑,实现最高优先级的自定义。- 切换容器时,仅仅替换依赖就够了吗?—— 一般情况下足够。但如果旧容器的特有定制器(如
TomcatConnectorCustomizer的某些 Bean)仍在上下文中,它们会被忽略,不会报错,但最好清理掉,保持配置干净。
- 如果 classpath 下同时有两个容器的依赖,会怎样?—— 通常会导致
7. WebServerFactoryCustomizer 和直接自定义 ServletWebServerFactory Bean 的区别是什么?
- 标准回答:
WebServerFactoryCustomizer的优点在于可以定义多个,互相不覆盖,通过组合方式共同调整一个工厂 Bean。而直接自定义一个ServletWebServerFactoryBean 会完全替代默认的自动配置,是一种更高侵入性但也更彻底的方式。 - 多角度追问:
- 什么场景下必须使用自定义 Bean 的方式?—— 当需要完全改变工厂的行为时,例如某些设置没有通过
Configurable...Factory接口暴露出来,或者你想在代码中完全控制 Tomcat 的构建过程。 - 多个定制器的执行顺序如何控制?—— 通过
@Ordered注解或Ordered接口来控制WebServerFactoryCustomizerBean的加载顺序。 - 定制器在什么时机被执行?—— 它在
WebServerFactoryCustomizerBeanPostProcessor.postProcessBeforeInitialization阶段执行,发生在工厂 Bean 的@PostConstruct之前。
- 什么场景下必须使用自定义 Bean 的方式?—— 当需要完全改变工厂的行为时,例如某些设置没有通过
8. DispatcherServletRegistrationBean 的作用是什么?如何注册多个 DispatcherServlet?
- 标准回答:它将 Spring MVC 的核心
DispatcherServlet注册到 Servlet 容器中。默认映射为/。要注册多个,只需声明多个DispatcherServletRegistrationBeanBean,并为它们指定不同的 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(...)来设置ioThreads和workerThreads。 - 为什么 Spring Boot 默认不统一线程配置?—— 因为线程模型是各容器核心特征的体现,强行统一会丧失各容器的优势。Spring Boot 选择将这部分交由容器的具体定制接口暴露,符合务实的设计原则。
10. 如何在容器启动后执行初始化逻辑,并且要确保容器端口已经打开?
- 标准回答:实现
ApplicationRunner或CommandLineRunner接口,或者监听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 的某些工具类,怎么办?—— 这极其危险,引入了不必要的复杂度。应该严格遵循“一个应用一个容器”的原则。
- 如何彻底排除?—— 使用 Maven 的
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 的工具类。 | 启动期 |
RegistrationBean | 为 Servlet、Filter 等组件提供动态注册到容器的模板。 | 启动期 |
WebServerFactoryCustomizer | 策略接口,用于自定义 WebServerFactory。 | 配置期 |
WebServerFactoryCustomizerBeanPostProcessor | BeanPostProcessor 实现,负责将定制器应用到工厂。 | 配置期 |
GracefulShutdownCallback | 优雅关闭完成后的回调接口。 | 关闭期 |
@ConditionalOnClass | 条件注解,根据 classpath 中的类决定配置是否生效。 | 配置解析期 |
延伸阅读
- Spring Boot 官方文档 - “Embedded Web Servers” 部分
- 《Spring Boot 编程思想》 - 小马哥(mercyblitz) 著。对理解 Spring Boot 的设计哲学和自动装配有极大帮助。
- Apache Tomcat 官方文档 - NIO Connector。深入理解 Tomcat 线程模型。
- Eclipse Jetty 官方文档 - Architecture。了解 Jetty 的核心架构。
- Undertow 官方文档 - Undertow Core。了解 XNIO 和核心配置。