Spring Boot 4.2.0-RC1 给嵌入式 Tomcat 补了 TomcatServerCustomizer。声明成 Bean,或挂到 TomcatServletWebServerFactory.addServerCustomizers(...),就能直接改 org.apache.catalina.Server。同一套小 Web 工程在 4.1.2 上导入这个类型会编译失败;旧路径的 WebServerFactoryCustomizer 只能正规摸到 connector、context、engine,要动 Server 得子类化工厂。
发布说明写得很短:Tomcat 的 Server 现在可用 TomcatServerCustomizer 定制。Javadoc 标注 @since 4.2.0,对应增强议题 #50259。动机能追到 #50100:给 Server 挂 OpenSSLLifecycleListener 这类监听时,以前拿不到正规回调。
4.1.2 没有这个类型
两份几乎同构的工程,只换 parent 版本。JDK 21.0.12.1,Maven 3.9.9。
$ java -version
openjdk version "21.0.12.1" 2026-08-18
OpenJDK Runtime Environment (build 21.0.12.1+1-1-deb13u1-Debian)
OpenJDK 64-Bit Server VM (build 21.0.12.1+1-1-deb13u1-Debian, mixed mode, sharing)
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>4.1.2</version>
</parent>
另一份换成 4.2.0-RC1。两边都只依赖 spring-boot-starter-web,打成 JarLauncher 可执行包后 Manifest 分别是:
Spring-Boot-Version: 4.1.2
Spring-Boot-Version: 4.2.0-RC1
在 4.1.2 的 classpath 上编译一段使用 TomcatServerCustomizer 的配置,直接报错:
FailServerCustomizer.java:3: error: cannot find symbol
import org.springframework.boot.tomcat.TomcatServerCustomizer;
^
symbol: class TomcatServerCustomizer
location: package org.springframework.boot.tomcat
FailServerCustomizer.java:11: error: cannot find symbol
TomcatServerCustomizer shouldNotExistOn41() {
^
symbol: class TomcatServerCustomizer
location: class FailServerCustomizer
2 errors
再对两边的 spring-boot-tomcat jar 列一下自定义接口:4.1.2 有 TomcatConnectorCustomizer、TomcatContextCustomizer、TomcatProtocolHandlerCustomizer;4.2.0-RC1 多出 TomcatServerCustomizer.class。类型层面就能确认这是 4.2 新增,不是旧 API 换皮。
4.1 正规路径能摸到哪一层
4.1.2 工程里先按老习惯写 WebServerFactoryCustomizer<TomcatServletWebServerFactory>,只挂 connector / context:
@Bean
WebServerFactoryCustomizer<TomcatServletWebServerFactory> legacyLayers() {
return factory -> {
factory.addConnectorCustomizers(connector -> { /* ... */ });
factory.addContextCustomizers(context -> { /* ... */ });
// 4.1.2 没有 factory.addServerCustomizers(...)
};
}
启动日志原样如下:
[demo-4.1] WebServerFactoryCustomizer reached TomcatServletWebServerFactory; Server still unreachable without subclass
[demo-4.1] connector customizer port=18081 (no Server callback on factory)
[demo-4.1] context customizer path= (still not Server)
ConfigurableTomcatWebServerFactory 在 4.1 上能加 connector、context、engine valve、protocol handler,没有 Server 回调。这和 内置 Tomcat 启动原理 里那层工厂装配是对齐的:工厂把你送到 Connector / Context,并不把 StandardServer 暴露成 Bean 回调。
想改 Server,旧写法只能子类化 TomcatServletWebServerFactory,在 getTomcatWebServer(Tomcat) 里拿 tomcat.getServer():
@Override
protected TomcatWebServer getTomcatWebServer(Tomcat tomcat) {
Server server = tomcat.getServer();
server.setShutdown("LEGACY-SUBCLASS-SHUTDOWN");
return super.getTomcatWebServer(tomcat);
}
[demo-4.1-subclass] reached Server via subclass getTomcatWebServer; shutdown 'SHUTDOWN'->'LEGACY-SUBCLASS-SHUTDOWN' class=org.apache.catalina.core.StandardServer
curl / 读到的 probe 也写着 source=subclass-getTomcatWebServer、shutdownCommand=LEGACY-SUBCLASS-SHUTDOWN。能改,但成本是自定义工厂 Bean,自动配置那条 ObjectProvider 装配链被你整段换掉。
4.2:Bean 直接进 Server
4.2.0-RC1 侧声明一个 Bean 就够:
@Bean
TomcatServerCustomizer beanTomcatServerCustomizer() {
return server -> {
server.setShutdown("SPRING4ALL-SHUTDOWN");
if (server instanceof StandardServer standardServer) {
standardServer.setUtilityThreads(4);
}
server.addLifecycleListener(event -> {
if (Lifecycle.AFTER_START_EVENT.equals(event.getType())) {
// 打日志、核对 utilityExecutor
}
});
};
}
自动配置 TomcatServletWebServerAutoConfiguration 会把所有 TomcatServerCustomizer Bean 塞进 factory 的 getServerCustomizers()。TomcatWebServerFactory.createTomcat 在建好 connector、配完 engine 之后,循环调用:
for (TomcatServerCustomizer customizer : getServerCustomizers()) {
customizer.customize(tomcat.getServer());
}
实测启动片段:
[demo] connector customizer port=18082 scheme=http
[demo] bean-TomcatServerCustomizer customized Server: class=org.apache.catalina.core.StandardServer shutdownPort -1->-1 shutdownCmd 'SHUTDOWN'->'SPRING4ALL-SHUTDOWN' address=localhost utilityThreads=4 services=1
[demo] bean-TomcatServerCustomizer sees 1 connector(s) under Service[0]; first port=18082
[demo] factory.addServerCustomizers fired; serverClass=org.apache.catalina.core.StandardServer
[demo] context customizer path= name=
[demo] Server AFTER_START via bean-TomcatServerCustomizer; shutdown=SPRING4ALL-SHUTDOWN port=-1 utilityExecutor=org.apache.tomcat.util.threads.ScheduledThreadPoolExecutor
Tomcat started on port 18082 (http) with context path '/'
Started DemoApplication in 1.345 seconds (process running for 1.63)
curl 回包原样:
{"app":"tomcat-server-customizer-demo","probe":{"source":"bean-TomcatServerCustomizer","serverClass":"org.apache.catalina.core.StandardServer","shutdownPort":-1,"shutdownCommand":"SPRING4ALL-SHUTDOWN","address":"localhost","utilityExecutor":null,"utilityThreads":4,"serviceCount":1,"events":["connector-customizer:18082","factory-addServerCustomizers","context-customizer:","server-after-start:bean-TomcatServerCustomizer"]}}
这里能核对几件 Server 级事实:
setShutdown从默认SHUTDOWN改成了自定义口令,启动后仍在StandardServer.setUtilityThreads(4)写进去了;utility executor 在 customize 当下还是 null,AFTER_START之后才是ScheduledThreadPoolExecutor- shutdown 端口保持
-1(嵌入式默认不听关机口),地址是localhost,下面挂 1 个 Service - 从 Server 能往下看见 Service 上的 Connector,但改端口、改 Context 仍该走原来的 connector / context customizer
和 WebServerFactoryCustomizer 怎么分工
同一次 4.2 启动里,还挂了两个东西:
WebServerFactoryCustomizer里调用factory.addServerCustomizers(...),日志出现factory.addServerCustomizers fired,和 Bean 路径进的是同一份serverCustomizers集合- 同一 customizer 里继续
addConnectorCustomizers/addContextCustomizers,端口 18082 和 context path 空串都打出来了
可以这么记:
WebServerFactoryCustomizer<TomcatServletWebServerFactory>:改工厂本身,比如端口策略、额外 connector、批量注册各类 customizerTomcatConnectorCustomizer/TomcatContextCustomizer/TomcatProtocolHandlerCustomizer:分别对着 Connector、Context、ProtocolHandlerTomcatServerCustomizer:只对Server。关机口令、utility 线程、Server 级 LifecycleListener、以及必须挂在 Server 上的组件,走这里
Bean 注册和 addServerCustomizers 等价,都会在 createTomcat 那一轮执行。不必再为了碰一下 Server 去覆盖整个 factory Bean。
什么时候用得上
日常改 HTTP 端口、压缩、SSL、context path,继续用配置属性和旧 customizer 就行。真正卡在 Server 层时,例如给 Server 加生命周期监听、改 utility 线程数、改关机命令,4.2 起可以直接丢一个 TomcatServerCustomizer Bean。4.1 及更早没有这个回调,只能量到 connector / context,或者接受子类化工厂的成本。
本次对照没有动嵌套 jar、Compose、properties migrator 那些 RC1 其它条目;那些需要单独场景,之前也有各自的实测稿,比如 嵌套 jar 直查。
一起交流
分享你的思考,让讨论更进一步。