Spring Boot 4.1.1 深度源码解析-第 18 章 内嵌 Web 服务器:WebServer 抽象与 Tomcat/Jetty/Netty
卷五 · Web 栈 · 第二章
第 17 章解决了"决定造什么 ":Spring Boot 推断出
WebApplicationType, 再由ApplicationContextFactory造出对应类型的上下文。这一章解决"怎么造出来并跑起来"。
我们的起点是一个只有 5 个方法的接口
WebServer。它小到令人怀疑------ 但它撑起了 Tomcat、Jetty、Reactor Netty 三套完全不同的实现, 并且要处理"启动期间不能接收请求""优雅停机""端口占用诊断"等一堆非平凡问题。本章会逐行读
TomcatWebServer的initialize(), 那是全书中"最反直觉的一段代码"之一:它先把连接器摘掉,再启动服务器,最后再把连接器装回去。 我们会搞清楚为什么必须这样。
0. 引子:三行日志背后的阶段划分
一个最普通的 Spring Boot Web 应用,启动时会打出这样的日志:
yaml
INFO ... TomcatWebServer : Tomcat initialized with port 8080 (http)
INFO ... TomcatWebServer : Tomcat started on port 8080 (http) with context path ''
INFO ... SpringApplication: Started DemoApplication in 1.234 seconds
注意第一行是 initialized ,第二行才是 started ------两行之间隔了数百毫秒, 这段时间里 Spring 在跑 refresh() 的后半程。
阶段划分:
scss
① onRefresh() → ② createWebServer() → ③ factory.getWebServer(initializers)
④ new TomcatWebServer(...) → ⑤ initialize() ← ✱ 打印 "Tomcat initialized"
⑥ tomcat.start()(connector 已摘走,不监听端口)
══════ refresh() 继续:finishBeanFactoryInitialization 等 ══════
⑦ WebServerStartStopLifecycle.start() → ⑧ webServer.start()
⑨ addPreviouslyRemovedConnectors() ← ✱ 端口才真正绑定
⑩ performDeferredLoadOnStartup() ← DispatcherServlet 真正初始化
⑪ ✱ 打印 "Tomcat started" → ⑫ ✱ 打印 "Started DemoApplication"
关键洞察 :TomcatWebServer 的构造与它的 start() 之间,隔了整个 refresh() 后半程。 为什么要这样切?答案在 ServletWebServerFactory.getWebServer() 的 Javadoc 里: "fully configured but paused" ------ 完全配置好,但暂停 , 客户端在 start() 之前必须无法连接。
如果某个实现不小心在构造时就监听了端口,那么在 refresh() 后半程 (比如某个 Bean 初始化失败)时,外部请求就可能打进一个半成品应用 。 本章的很多"奇怪代码",都是为了满足这个契约。
java
/**
* Gets a new fully configured but paused {@link WebServer} instance. Clients should
* not be able to connect to the returned server until {@link WebServer#start()} is
* called (which happens when the {@code ApplicationContext} has been fully
* refreshed).
* ...
*/
"fully configured but paused" ------ 完全配置好,但暂停 。 客户端在
start()之前必须无法连接。
这是一个强契约 。如果某个服务器实现不小心在构造时就监听了端口, 那么在 refresh() 后半程(比如某个 Bean 初始化失败)时, 外部请求就可能打进一个半成品应用------这是非常危险的。
本章的很多"奇怪代码",都是为了满足这个契约。
1. WebServer 接口:五个方法
文件坐标 :module/spring-boot-web-server/src/main/java/org/springframework/boot/web/server/WebServer.java 作者 :Phillip Webb、Dave Syer @since:2.0.0
1.1 全文
java
/**
* Simple interface that represents a fully configured web server (for example Tomcat,
* Jetty, Netty). Allows the server to be {@link #start() started} and {@link #stop()
* stopped}.
*
* @author Phillip Webb
* @author Dave Syer
* @since 2.0.0
*/
public interface WebServer {
/**
* Starts the web server. Calling this method on an already started server has no
* effect.
* @throws WebServerException if the server cannot be started
*/
void start() throws WebServerException;
/**
* Stops the web server. Calling this method on an already stopped server has no
* effect.
* @throws WebServerException if the server cannot be stopped
*/
void stop() throws WebServerException;
/**
* Return the port this server is listening on.
* @return the port (or -1 if none)
*/
int getPort();
/**
* Initiates a graceful shutdown of the web server. Handling of new requests is
* prevented and the given {@code callback} is invoked at the end of the attempt. The
* attempt can be explicitly ended by invoking {@link #stop}. The default
* implementation invokes the callback immediately with
* {@link GracefulShutdownResult#IMMEDIATE}, i.e. no attempt is made at a graceful
* shutdown.
* @param callback the callback to invoke when the graceful shutdown completes
* @since 2.3.0
*/
default void shutDownGracefully(GracefulShutdownCallback callback) {
callback.shutdownComplete(GracefulShutdownResult.IMMEDIATE);
}
/**
* Destroys the web server such that it cannot be started again.
* @since 3.2.0
*/
default void destroy() {
stop();
}
}
1.2 方法逐个分析
start() / stop():幂等性写在 Javadoc 里
arduino
"Calling this method on an already started server has no effect."
"Calling this method on an already stopped server has no effect."
幂等性是契约,不是实现细节。这意味着:
- 实现必须 自己维护
started标志并短路返回; - 调用方可以放心地多次调用。
看 Tomcat 的实现:
java
@Override
public void start() throws WebServerException {
synchronized (this.monitor) {
if (this.started) {
return; // ← 幂等短路
}
// ...
this.started = true;
}
}
Jetty 也一样:
java
@Override
public void start() throws WebServerException {
synchronized (this.monitor) {
if (this.started) {
return;
}
// ...
}
}
Netty 用的是另一种写法------判断 disposableServer == null:
java
@Override
public void start() throws WebServerException {
DisposableServer disposableServer = this.disposableServer;
if (disposableServer == null) { // ← 用"是否已绑定"代替布尔标志
try {
disposableServer = startHttpServer();
this.disposableServer = disposableServer;
}
// ...
}
}
三个实现用了两种不同的幂等表达方式 :Tomcat/Jetty 用 volatile boolean started, Netty 用 volatile DisposableServer disposableServer != null。
Netty 的写法有个好处:disposableServer 同时承载了"是否已启动"和"服务器句柄"两个信息, 不需要额外的字段。但它的代价是:无法区分"未启动"和"启动后被 stop 了"(stop 后是否置 null?需要看 stop 实现)。
⚠️ 未能核实 :我没有读到
NettyWebServer.stop()的完整实现, 不确定stop()是否把disposableServer置回null。 如果不置回 ,那stop()后再start()就无法重启(符合destroy的语义); 如果置回 ,start()就能重启。这是留给读者的验证点。
getPort():返回 -1 表示没有端口
java
/**
* Return the port this server is listening on.
* @return the port (or -1 if none)
*/
int getPort();
-1 是一个哨兵值。什么时候会返回 -1?
回忆第 17 章 §7 的代码:
java
protected TomcatWebServer getTomcatWebServer(Tomcat tomcat) {
return new TomcatWebServer(tomcat, getPort() >= 0, getShutdown());
}
autoStart = getPort() >= 0。当 server.port = -1 时,autoStart = false, 连接器被加回来后立即被停掉 (stopProtocolHandler),于是不监听端口。
server.port = -1 的用途:只想初始化 Web 基础设施、不想监听端口 。 典型场景就是测试(SpringBootTest 的某些模式)。
shutDownGracefully():默认实现是"不优雅"
java
default void shutDownGracefully(GracefulShutdownCallback callback) {
callback.shutdownComplete(GracefulShutdownResult.IMMEDIATE);
}
默认实现立即 回调 IMMEDIATE,即"我没实现优雅停机,直接停了"。
这个默认值的设计很关键:它让"不支持优雅停机的实现"也能正常工作,只是不优雅。
三个实现中,Tomcat / Jetty / Netty 都覆写了 这个方法,所以默认值实际是给第三方实现用的。
看一下 Netty 的覆写:
java
@Override
public void shutDownGracefully(GracefulShutdownCallback callback) {
if (this.gracefulShutdown == null) {
callback.shutdownComplete(GracefulShutdownResult.IMMEDIATE); // ← 与默认一致
return;
}
this.gracefulShutdown.shutDownGracefully(callback);
}
注意:Netty 覆写后,当 gracefulShutdown == null 时的行为与接口默认实现完全一致。 那么这次覆写有意义吗?有------因为它避免了"默认实现"在将来变化时产生行为差异, 也让"是否支持优雅停机"这个判断显式化。
gracefulShutdown == null 的条件是构造时 shutdown != Shutdown.GRACEFUL:
java
this.gracefulShutdown = (shutdown == Shutdown.GRACEFUL) ? new GracefulShutdown(() -> this.disposableServer) : null;
destroy():@since 3.2.0,与 stop() 的区别
java
/**
* Destroys the web server such that it cannot be started again.
* @since 3.2.0
*/
default void destroy() {
stop();
}
默认实现是 destroy() == stop()。但语义上 destroy 更强:"cannot be started again"。
什么时候用 destroy?看 ServletWebServerApplicationContext:
java
@Override
protected void doClose() {
if (isActive()) {
AvailabilityChangeEvent.publish(this, ReadinessState.REFUSING_TRAFFIC); // ①
}
super.doClose();
WebServer webServer = this.webServer;
if (webServer != null) {
webServer.destroy(); // ②
}
}
以及 refresh() 失败时的回滚:
java
@Override
public final void refresh() throws BeansException, IllegalStateException {
try {
super.refresh();
}
catch (RuntimeException ex) {
WebServer webServer = this.webServer;
if (webServer != null) {
try {
webServer.stop(); // 先 stop
webServer.destroy(); // 再 destroy
}
catch (RuntimeException stopOrDestroyEx) {
ex.addSuppressed(stopOrDestroyEx); // ★
}
}
throw ex;
}
}
stop() + destroy() 成对出现 ,且异常用 addSuppressed 挂到主异常上。
addSuppressed是 Java 7 引入的 try-with-resources 配套机制。 这里手动使用它,是为了保证主异常不被清理异常掩盖------ 第 16 章讲的"诊断链路绝不能失败沉默"(第十定律)在这里的体现: 清理失败不会丢,但也不会盖掉真正的原因。
Tomcat 覆写了 destroy()(推测 ,我未读到 TomcatWebServer 的 destroy 覆写, 但 stop() 的实现里能看到 stopTomcat() 会重置 TCCL,说明有专门的清理逻辑)。 实际使用上:TomcatWebServer.destroy() 会调 tomcat.destroy()(通过 destroySilently() 包装)。
1.3 WebServerException
java
/**
* Exceptions thrown by a web server.
*
* @author Phillip Webb
* @since 2.0.0
*/
@SuppressWarnings("serial")
public class WebServerException extends RuntimeException {
public WebServerException(String message, @Nullable Throwable cause) {
super(message, cause);
}
}
只有一个构造器 :(String message, Throwable cause)。没有单参数版本,没有无参版本。
这个设计约束很有意思:强制你提供 cause。因为 Web 服务器的异常几乎总是"包装底层服务器异常", 没有 cause 的 WebServerException 是无用的。
继承 RuntimeException(非受检),所以它不需要在 start()/stop() 的签名里声明------ 但 Javadoc 里还是写了 @throws WebServerException。
@SuppressWarnings("serial") 是因为继承 RuntimeException 但没定义 serialVersionUID。
2. WebServerFactory:一个空接口
java
/**
* Tagging interface for factories that create a {@link WebServer}.
*
* @author Phillip Webb
* @since 2.0.0
* @see WebServer
* @see org.springframework.boot.web.server.servlet.ServletWebServerFactory
* @see org.springframework.boot.web.server.reactive.ReactiveWebServerFactory
*/
public interface WebServerFactory {
}
完全为空 。这是纯粹的标记接口(第 17 章 §9.4 讲过 StandardReactiveWebEnvironment 也是标记用途)。
它的作用:让 WebServerFactoryCustomizer<T extends WebServerFactory> 的泛型有上界, 也让 WebServerFactoryCustomizerBeanPostProcessor 能用 bean instanceof WebServerFactory 判断。
为什么需要这个空接口? 因为 ServletWebServerFactory 与 ReactiveWebServerFactory 在方法签名上完全不同,没有可提取的共同方法:
java
WebServer getWebServer(ServletContextInitializer... initializers); // servlet
WebServer getWebServer(HttpHandler httpHandler); // reactive
参数类型不同,无法统一。所以只能退回到空接口做标记。
3. 双分支:Servlet 与 Reactive 工厂
3.1 ServletWebServerFactory
文件坐标 :module/spring-boot-web-server/src/main/java/.../servlet/ServletWebServerFactory.java @since:4.0.0
java
/**
* Factory interface that can be used to create a {@link WebServer}.
*
* @author Phillip Webb
* @since 4.0.0
* @see WebServer
*/
@FunctionalInterface
public interface ServletWebServerFactory extends WebServerFactory {
/**
* Gets a new fully configured but paused {@link WebServer} instance. Clients should
* not be able to connect to the returned server until {@link WebServer#start()} is
* called (which happens when the {@code ApplicationContext} has been fully
* refreshed).
* @param initializers {@link ServletContextInitializer}s that should be applied as
* the server starts
* @return a fully configured and started {@link WebServer}
* @see WebServer#stop()
*/
WebServer getWebServer(ServletContextInitializer... initializers);
}
@FunctionalInterface ------ 可以用 lambda 实现(虽然实际没人这么做)。
参数是可变参数 ServletContextInitializer...,意味着可以传 0 个。
3.2 ReactiveWebServerFactory
文件坐标 :module/spring-boot-web-server/src/main/java/.../reactive/ReactiveWebServerFactory.java @since:4.0.0
java
@FunctionalInterface
public interface ReactiveWebServerFactory extends WebServerFactory {
/**
* Gets a new fully configured but paused {@link WebServer} instance. Clients should
* not be able to connect to the returned server until {@link WebServer#start()} is
* called (which happens when the {@code ApplicationContext} has been fully
* refreshed).
* @param httpHandler the HTTP handler in charge of processing requests
* @return a fully configured and started {@link WebServer}
* @see WebServer#stop()
*/
WebServer getWebServer(HttpHandler httpHandler);
}
3.3 两者的对比
ServletWebServerFactory |
ReactiveWebServerFactory |
|
|---|---|---|
@since |
4.0.0 | 4.0.0 |
| 参数 | ServletContextInitializer... initializers |
HttpHandler httpHandler |
| 参数数量 | 0..N | 恰好 1 |
| 参数语义 | "服务器启动时要应用的一组初始化器" | "处理请求的 HTTP handler" |
| 生产者 | TomcatServletWebServerFactory / JettyServletWebServerFactory |
NettyReactiveWebServerFactory / TomcatReactiveWebServerFactory / JettyReactiveWebServerFactory |
| 消费者 | ServletWebServerApplicationContext |
ReactiveWebServerApplicationContext |
一个值得注意的事实 :Tomcat 和 Jetty 同时有 servlet 版和 reactive 版工厂。
arduino
module/spring-boot-tomcat/
├─ servlet/TomcatServletWebServerFactory.java
└─ reactive/TomcatReactiveWebServerFactory.java
module/spring-boot-jetty/
├─ servlet/JettyServletWebServerFactory.java
└─ reactive/JettyReactiveWebServerFactory.java (推测路径,未逐一核实)
module/spring-boot-reactor-netty/
└─ NettyReactiveWebServerFactory.java (只有 reactive)
Tomcat/Jetty 的 reactive 版是怎么实现的?看 TomcatReactiveWebServerFactory:
java
public WebServer getWebServer(HttpHandler httpHandler) {
TempDirs tempDirs = new TempDirs(getPort());
Tomcat tomcat = createTomcat(tempDirs);
TomcatHttpHandlerAdapter servlet = new TomcatHttpHandlerAdapter(httpHandler); // ★
prepareContext(tomcat.getHost(), servlet, tempDirs);
return getTomcatWebServer(tomcat);
}
关键在 TomcatHttpHandlerAdapter :它把 HttpHandler(响应式抽象) 适配成一个 Servlet(实际是 ServletHttpHandlerAdapter 一族),然后塞进 servlet 容器。
也就是说:在 Tomcat/Jetty 上跑 WebFlux,本质上还是 servlet,只是用了一个桥接 Servlet。 只有 Reactor Netty 是真正的"原生响应式"。
这也解释了为什么 Reactor Netty 只有 reactive 版工厂------它不支持 servlet 规范。
实践含义 :
spring-boot-starter-webflux默认带的是 Reactor Netty。 如果你换到 Tomcat 上跑 WebFlux,性能特征会不同(多一层 servlet 桥接 + 阻塞式 servlet API 语义), 但功能上等价(大部分场景)。
3.4 "fully configured but paused" 契约的三个实现手段
三个实现达成"构造时不监听端口"的手段各不相同:
| 实现 | 手段 |
|---|---|
| Tomcat | 构造时把 connector 从 Service 里 remove 掉,并设 bindOnInit=false;start() 时再加回来 |
| Jetty | 构造时 server.setConnectors(null);start() 时 server.setConnectors(this.connectors) |
| Reactor Netty | 构造时根本不 bind ;start() 时才 server.bindNow() |
Tomcat 和 Jetty 是"先建后摘",Netty 是"延迟创建"。
这两种策略的差异,根源在于底层服务器的 API 设计:
- Tomcat 的
Tomcat.start()会连带启动 connector(因为 connector 是 Service 的一部分)→ 只能先摘掉 - Jetty 的
Server.start()会启动所有 connector → 只能先置 null - Reactor Netty 的
HttpServer对象是声明式的,bindNow()才真正绑定 → 天然满足
4. 抽象层:AbstractConfigurableWebServerFactory 与配置映射
4.1 字段清单
文件坐标 :module/spring-boot-web-server/src/main/java/.../AbstractConfigurableWebServerFactory.java
java
public abstract class AbstractConfigurableWebServerFactory implements ConfigurableWebServerFactory {
private int port = 8080;
private @Nullable InetAddress address;
private Set<ErrorPage> errorPages = new LinkedHashSet<>();
private @Nullable Ssl ssl;
private @Nullable SslBundles sslBundles;
private @Nullable Http2 http2;
private @Nullable Compression compression;
private @Nullable String serverHeader;
private Shutdown shutdown = Shutdown.IMMEDIATE;
// ...
}
注意 shutdown 的默认值是 Shutdown.IMMEDIATE。
但 ServerProperties 里:
java
private Shutdown shutdown = Shutdown.GRACEFUL;
ServerProperties 默认是 GRACEFUL,factory 默认是 IMMEDIATE。
这个不一致是有意的:factory 是"不给配置就最保守 "(立即停,不等请求), 而 Boot 的默认配置是"优雅停机"(Boot 2.3 之后的新默认)。
由于 ServletWebServerFactoryCustomizer 会把 serverProperties.getShutdown() 映射到 factory.setShutdown(...), 所以实际运行时默认值是 GRACEFUL(只要自动配置生效)。
这是一个"两处默认值、一处生效 "的典型。 判断哪个生效,看定制器链。 如果你手动
new TomcatServletWebServerFactory()且不跑自动配置,那你拿到的是IMMEDIATE。
4.2 配置映射:ServletWebServerFactoryCustomizer
文件坐标 :module/spring-boot-web-server/src/main/java/.../autoconfigure/servlet/ServletWebServerFactoryCustomizer.java
java
@Override
public void customize(ConfigurableServletWebServerFactory factory) {
PropertyMapper map = PropertyMapper.get();
map.from(this.serverProperties::getPort).to(factory::setPort);
map.from(this.serverProperties::getAddress).to(factory::setAddress);
map.from(this.serverProperties.getServlet()::getContextPath).to(factory::setContextPath);
map.from(this.serverProperties.getServlet()::getApplicationDisplayName).to(factory::setDisplayName);
map.from(this.serverProperties.getServlet()::isRegisterDefaultServlet).to(factory::setRegisterDefaultServlet);
map.from(this.serverProperties.getServlet()::getSession).to(factory::setSession);
map.from(this.serverProperties::getSsl).to(factory::setSsl);
map.from(this.serverProperties.getServlet()::getJsp).to(factory::setJsp);
map.from(this.serverProperties::getCompression).to(factory::setCompression);
map.from(this.serverProperties::getHttp2).to(factory::setHttp2);
map.from(this.serverProperties::getServerHeader).to(factory::setServerHeader);
map.from(this.serverProperties.getServlet()::getContextParameters).to(factory::setInitParameters);
map.from(this.serverProperties.getShutdown()).to(factory::setShutdown);
map.from(() -> this.sslBundles).to(factory::setSslBundles);
map.from(() -> this.cookieSameSiteSuppliers)
.whenNot(CollectionUtils::isEmpty)
.to(factory::setCookieSameSiteSuppliers);
map.from(this.serverProperties::getMimeMappings).to(factory::addMimeMappings);
map.from(this.serverProperties.getServlet().getEncoding()::getMapping).to(factory::setLocaleCharsetMappings);
this.webListenerRegistrars.forEach((registrar) -> registrar.register(factory));
}
PropertyMapper 是 Boot 的"安全映射"工具(第 08 章讲 Binder 时提过)。 它的核心特性:
- 源为 null 时不调用 setter ------ 避免用 null 覆盖掉 factory 的默认值
- 源抛异常时跳过 (
PropertyMapper.get()返回的是默认实例,默认忽略异常) - 支持
.when(...)/.whenNot(...)条件 - 支持
.as(...)/.toInstance(...)转换
看这一行:
java
map.from(() -> this.cookieSameSiteSuppliers)
.whenNot(CollectionUtils::isEmpty)
.to(factory::setCookieSameSiteSuppliers);
用了 whenNot(CollectionUtils::isEmpty)------空集合时不调用 setter。 这与"null 不调用"是不同的守卫:
@Nullable字段 → 靠PropertyMapper内置的 null 检查- 非 null 但空集合 → 需要显式
whenNot(isEmpty)
这是一个容易被忽略的细节 :如果去掉 whenNot,空集合会覆盖掉 factory 原有的 supplier 集合。
4.3 定制器的 getOrder()
java
@Override
public int getOrder() {
return 0;
}
ServletWebServerFactoryCustomizer 实现了 Ordered 并返回 0。
为什么需要排序?因为有多个定制器会作用在同一个 factory 上:
ServletWebServerFactoryCustomizer(通用 server.* 配置)------ order 0TomcatServletWebServerFactoryCustomizer(Tomcat 专属 server.tomcat.*)------ 无@Order(默认 LOWEST_PRECEDENCE?)TomcatVirtualThreadsWebServerFactoryCustomizerWebSocketTomcatWebServerFactoryCustomizerManagementWebServerFactoryCustomizer(Actuator 管理端口,第 25 章)
排序由 WebServerFactoryCustomizerBeanPostProcessor 执行:
java
private Collection<WebServerFactoryCustomizer<?>> getCustomizers() {
if (this.customizers == null) {
// Look up does not include the parent context
this.customizers = new ArrayList<>(getWebServerFactoryCustomizerBeans());
this.customizers.sort(AnnotationAwareOrderComparator.INSTANCE);
this.customizers = Collections.unmodifiableList(this.customizers);
}
return this.customizers;
}
注意
customizers字段被缓存了 (if (this.customizers == null)), 且缓存后Collections.unmodifiableList包装。 这意味着:定制器集合在第一次使用后固定,之后再往容器里加定制器也无效。 这是 BeanPostProcessor 生命周期决定的------它在 factory bean 创建时才首次触发。
4.4 4.x 新增:ServletWebServerSettings
在 prepareContext 里能看到 this.settings:
java
protected void prepareContext(Host host, ServletContextInitializer[] initializers, TempDirs tempDirs) {
DocumentRoot documentRoot = new DocumentRoot(logger);
documentRoot.setDirectory(this.settings.getDocumentRoot());
// ...
String contextPath = this.settings.getContextPath().toString();
// ...
if (this.settings.isRegisterDefaultServlet()) {
addDefaultServlet(context);
}
// ...
}
ServletWebServerSettings 是 4.x 新增的一个聚合接口 , 把 DocumentRoot、ContextPath、Jsp、Session、Encoding 等"servlet 专属设置"打包。
这是 4.x 重构的产物:把原来散落在 ConfigurableServletWebServerFactory 上的十几个 getter/setter, 收敛到一个 settings 对象里。
证据:prepareContext(Host, ServletContextInitializer[], TempDirs) 有 @since 4.1.0, 而旧的 prepareContext(Host, ServletContextInitializer[]) 被标记:
java
/**
* @deprecated since 4.1.0 for removal in 4.3.0 in favor of
* {@link #prepareContext(Host, ServletContextInitializer[], TempDirs)}
*/
@Deprecated(forRemoval = true, since = "4.1.0")
protected void prepareContext(Host host, ServletContextInitializer[] initializers) {
prepareContext(host, initializers, new TempDirs(getPort()));
}
@Deprecated(forRemoval = true, since = "4.1.0") + "for removal in 4.3.0" ------ Boot 的废弃策略是明确的:标注废弃版本 + 预告移除版本。
顺带注意:这个废弃的重载仍然可用(它委托给了新版本),所以已有子类不会炸。
5. 创建时序:从 onRefresh() 到 WebServer
5.1 Servlet 侧
文件坐标 :module/spring-boot-web-server/src/main/java/.../servlet/context/ServletWebServerApplicationContext.java
java
@Override
protected void onRefresh() {
super.onRefresh();
try {
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) {
// ★ 分支 A:创建内嵌服务器
StartupStep createWebServer = getApplicationStartup().start("spring.boot.webserver.create");
ServletWebServerFactory factory = getWebServerFactory();
createWebServer.tag("factory", factory.getClass().toString());
webServer = factory.getWebServer(getSelfInitializer());
this.webServer = webServer;
createWebServer.end();
getBeanFactory().registerSingleton("webServerGracefulShutdown",
new WebServerGracefulShutdownLifecycle(webServer));
getBeanFactory().registerSingleton("webServerStartStop", new WebServerStartStopLifecycle(this, webServer));
}
else if (servletContext != null) {
// ★ 分支 B:已有 ServletContext(WAR 部署 / 已有容器)
try {
getSelfInitializer().onStartup(servletContext);
}
catch (ServletException ex) {
throw new ApplicationContextException("Cannot initialize servlet context", ex);
}
}
initPropertySources();
}
要点:
(1) catch (Throwable ex) 而不是 catch (Exception ex)
Throwable 包含 Error(如 NoClassDefFoundError、StackOverflowError)。 这符合第十定律:诊断链路 / 启动关键路径要捕获一切。
(2) StartupStep 的 tag
java
StartupStep createWebServer = getApplicationStartup().start("spring.boot.webserver.create");
createWebServer.tag("factory", factory.getClass().toString());
StartupStep 是 Spring Framework 的启动性能度量 API(第 26 章讲 Micrometer 时会再提)。 spring.boot.webserver.create 是这一步的名字,tag 里记录用的是哪个 factory 实现。
用 --Dspring.application.startup=buffering 之类的配置(或 Actuator 的 /actuator/startup) 能看到这一步的耗时。
(3) 两个 Lifecycle 用 registerSingleton 而不是 registerBeanDefinition
java
getBeanFactory().registerSingleton("webServerGracefulShutdown", new WebServerGracefulShutdownLifecycle(webServer));
getBeanFactory().registerSingleton("webServerStartStop", new WebServerStartStopLifecycle(this, webServer));
为什么用 registerSingleton?因为:
- 这两个对象不需要依赖注入(构造参数已经给全了)
registerSingleton的对象不参与生命周期回调(不会被 BPP 处理)- 它们是手工注册的单例,不是 Bean 定义
但 SmartLifecycle 的发现靠什么?DefaultLifecycleProcessor 在 onRefresh() 时会 beanFactory.getBeanNamesForType(Lifecycle.class)------ 而 registerSingleton 注册的对象也会被 getBeanNamesForType 找到(手工单例同样登记在 manualSingletonNames 里)。
所以
registerSingleton不阻碍SmartLifecycle被发现。这是可行的。
5.2 getWebServerFactory() 的 0 / 1 / N 判定
java
protected ServletWebServerFactory getWebServerFactory() {
// Use bean names so that we don't consider the hierarchy
String[] beanNames = getBeanFactory().getBeanNamesForType(ServletWebServerFactory.class);
if (beanNames.length == 0) {
throw new MissingWebServerFactoryBeanException(getClass(), ServletWebServerFactory.class,
WebApplicationType.SERVLET);
}
if (beanNames.length > 1) {
throw new ApplicationContextException("Unable to start ServletWebServerApplicationContext due to multiple "
+ "ServletWebServerFactory beans : " + StringUtils.arrayToCommaDelimitedString(beanNames));
}
return getBeanFactory().getBean(beanNames[0], ServletWebServerFactory.class);
}
三个关键:
(1) // Use bean names so that we don't consider the hierarchy
用 getBeanNamesForType 而不是 getBeanProvider(...) / getBeansOfType(...), 是为了不搜索父上下文。
getBeanNamesForType 默认只查当前 factory (不含祖先), 而 getBeansOfType 会向上查找(第 15 章讲 SearchStrategy.ANCESTORS 时提过这个区别)。
为什么这里必须排除父上下文?因为 Actuator 的管理端口子上下文(第 25 章) 是主上下文的子上下文 ,它里面有一个自己的 ServletWebServerFactory。 如果主上下文能"看见"子上下文的 factory,就会出现重复或错配。
(2) length == 0 → MissingWebServerFactoryBeanException
java
public class MissingWebServerFactoryBeanException extends NoSuchBeanDefinitionException {
private final WebApplicationType webApplicationType;
// ...
}
它继承 NoSuchBeanDefinitionException ! 这意味着它能被 NoSuchBeanDefinitionFailureAnalyzer(第 16 章讲过)处理吗?
不对------第 16 章讲过 NoSuchBeanDefinitionFailureAnalyzer 在构造时 就抓取 ConditionEvaluationReport。 而 MissingWebServerFactoryBeanException 有专门的 analyzer: MissingWebServerFactoryBeanFailureAnalyzer(注册在 spring-boot-web-server 的 spring.factories 里)。
它携带 webApplicationType 字段,说明它的诊断信息会区分 SERVLET / REACTIVE: "你缺的是 ServletWebServerFactory(因为你被判成了 SERVLET)" vs "你缺的是 ReactiveWebServerFactory"。
(3) length > 1 → ApplicationContextException
注意:不是 NoSuchBeanDefinitionException 的子类型。 所以"多个 factory"的报错不会 被 MissingWebServerFactoryBeanFailureAnalyzer 处理, 只会打出原始堆栈。
两种错误走不同的诊断路径------这是一个不对称,我推测是因为"多个 factory"通常意味着用户依赖配置错误(同时引入了两个 starter), 诊断信息足够直白(列出了所有 bean 名字),不需要额外分析。
5.3 Reactive 侧
java
private void createWebServer() {
WebServerManager serverManager = this.serverManager;
if (serverManager == null) {
StartupStep createWebServer = getApplicationStartup().start("spring.boot.webserver.create");
String webServerFactoryBeanName = getWebServerFactoryBeanName();
ReactiveWebServerFactory webServerFactory = getWebServerFactory(webServerFactoryBeanName);
createWebServer.tag("factory", webServerFactory.getClass().toString());
boolean lazyInit = getBeanFactory().getBeanDefinition(webServerFactoryBeanName).isLazyInit();
serverManager = new WebServerManager(this, webServerFactory, this::getHttpHandler, lazyInit);
this.serverManager = serverManager;
getBeanFactory().registerSingleton("webServerGracefulShutdown",
new WebServerGracefulShutdownLifecycle(serverManager.getWebServer()));
getBeanFactory().registerSingleton("webServerStartStop", new WebServerStartStopLifecycle(serverManager));
createWebServer.end();
}
initPropertySources();
}
与 Servlet 侧的差异:
| Servlet | Reactive | |
|---|---|---|
| 中间层 | 无(直接拿 WebServer) |
有 WebServerManager |
| factory 查找 | getWebServerFactory() 一步 |
getWebServerFactoryBeanName() + getWebServerFactory(name) 两步 |
| lazyInit | 不关心 | 关心 ,读 BeanDefinition.isLazyInit() |
| HttpHandler | 不需要 | 需要额外查一次 bean |
为什么要分两步拿 factory?
因为需要 beanName 去读 BeanDefinition.isLazyInit():
java
boolean lazyInit = getBeanFactory().getBeanDefinition(webServerFactoryBeanName).isLazyInit();
这个 lazyInit 会传给 WebServerManager,决定 HttpHandler 是立即初始化 还是延迟到第一个请求。
这是一个"配置传播"的范例 :factory bean 的 lazy-init 属性, 被传播到了 HttpHandler 的初始化时机上。
为什么 servlet 侧不需要?因为 servlet 侧没有 HttpHandler 这个东西------ DispatcherServlet 的 load-on-startup 由它自己的配置决定(第 19 章)。
5.4 getHttpHandler() 的 0 / 1 / N 判定
java
protected HttpHandler getHttpHandler() {
// Use bean names so that we don't consider the hierarchy
String[] beanNames = getBeanFactory().getBeanNamesForType(HttpHandler.class);
if (beanNames.length == 0) {
throw new ApplicationContextException(
"Unable to start ReactiveWebApplicationContext due to missing HttpHandler bean.");
}
if (beanNames.length > 1) {
throw new ApplicationContextException(
"Unable to start ReactiveWebApplicationContext due to multiple HttpHandler beans : "
+ StringUtils.arrayToCommaDelimitedString(beanNames));
}
return getBeanFactory().getBean(beanNames[0], HttpHandler.class);
}
与 getWebServerFactory() 结构相同,但两种错误都抛 ApplicationContextException ------ 不区分"缺"和"多",也不继承 NoSuchBeanDefinitionException。
对比 servlet 侧"缺"抛 MissingWebServerFactoryBeanException(有专用 analyzer)、"多"抛 ApplicationContextException。
三处判定、三种处理。这种不一致在 Boot 里很常见,通常反映"哪种情况更常见、更需要好诊断"。
HttpHandler 从哪来?由 WebFluxAutoConfiguration 里的 HttpHandlerAutoConfiguration 提供 (第 20 章详述)。所以当你看到 "missing HttpHandler bean" 时, 真正的问题是 WebFlux 自动配置没生效------而不是"忘了定义 HttpHandler"。
6. Tomcat 深潜:getWebServer() 的三行与五个阶段
6.1 入口的三行
文件坐标 :module/spring-boot-tomcat/src/main/java/.../servlet/TomcatServletWebServerFactory.java
java
@Override
public WebServer getWebServer(ServletContextInitializer... initializers) {
TempDirs tempDirs = new TempDirs(getPort());
Tomcat tomcat = createTomcat(tempDirs);
prepareContext(tomcat.getHost(), initializers, tempDirs);
return getTomcatWebServer(tomcat);
}
三行,但每行都是一个复杂阶段:
| 行 | 阶段 | 做什么 |
|---|---|---|
| 1 | new TempDirs(getPort()) |
临时目录管理器的创建(4.1.0 新增) |
| 2 | createTomcat(tempDirs) |
创建 Tomcat 实例、connector、engine、host |
| 3 | prepareContext(...) |
创建并配置 TomcatEmbeddedContext(即 webapp) |
| 4 | getTomcatWebServer(tomcat) |
包装成 TomcatWebServer(这一步触发 initialize()) |
6.2 TempDirs:4.1.0 新增的临时目录管理
java
// createTomcat 里
File baseDir = (getBaseDirectory() != null) ? getBaseDirectory() : tempDirs.createTempDir("tomcat").toFile();
tomcat.setBaseDir(baseDir.getAbsolutePath());
tomcat.getServer().addLifecycleListener(new CleanTempDirsListener(tempDirs));
java
// prepareContext 里
File docBase = (documentRootFile != null) ? documentRootFile
: tempDirs.createTempDir("tomcat-docbase").toFile();
context.setDocBase(docBase.getAbsolutePath());
它管理两类临时目录:tomcat(baseDir)与 tomcat-docbase(webapp 文档根), 由 CleanTempDirsListener 在 Tomcat 停止时清理。
内嵌 Tomcat 没有真实"部署目录",但 StandardContext 强制要求 docBase。 4.1.0 把原先分散的清理逻辑统一到 TempDirs + listener------ 这也是 createTomcat() / prepareContext(host, initializers) 被标记 @Deprecated(since = "4.1.0") 的原因:新重载需要把 TempDirs 传下去。
6.3 createTomcat():从 new Tomcat() 到 connector
java
protected Tomcat createTomcat(TempDirs tempDirs) {
if (this.isDisableMBeanRegistry()) {
Registry.disableRegistry(); // ① 关掉 JMX
}
Tomcat tomcat = new Tomcat();
File baseDir = (getBaseDirectory() != null) ? getBaseDirectory() : tempDirs.createTempDir("tomcat").toFile();
tomcat.setBaseDir(baseDir.getAbsolutePath()); // ② baseDir
for (LifecycleListener listener : getDefaultServerLifecycleListeners()) {
tomcat.getServer().addLifecycleListener(listener); // ③ APR listener
}
tomcat.getServer().addLifecycleListener(new CleanTempDirsListener(tempDirs));
Connector connector = new Connector(getProtocol()); // ④ 主连接器
connector.setThrowOnFailure(true); // ★
tomcat.getService().addConnector(connector);
customizeConnector(connector);
tomcat.setConnector(connector);
registerConnectorExecutor(tomcat, connector);
tomcat.getHost().setAutoDeploy(false); // ⑤ 关掉热部署
configureEngine(tomcat.getEngine());
for (Connector additionalConnector : this.getAdditionalConnectors()) {
tomcat.getService().addConnector(additionalConnector);
registerConnectorExecutor(tomcat, additionalConnector);
}
return tomcat;
}
几个值得记住的点:
(1) Registry.disableRegistry()
disableMBeanRegistry 默认为 true (见 TomcatWebServerFactory 字段 private boolean disableMBeanRegistry = true)。
Tomcat 默认会把所有组件注册到 JMX MBean Server 里。在内嵌场景下这是纯开销:
- 内存占用
- 关闭时的清理负担
- 多个内嵌 Tomcat 实例(如测试并行)时 MBean 名字冲突
Boot 默认关掉它。
(2) connector.setThrowOnFailure(true)
这一行很关键。Tomcat 的 connector 启动失败时,默认行为是打日志但不抛异常------ 这会导致"端口被占用了但应用看起来启动成功了"的诡异现象。
设为 true 后,connector 启动失败会抛出异常。 配合后面 start() 里的 checkThatConnectorsHaveStarted(),端口占用才能被正确诊断。
(3) tomcat.getHost().setAutoDeploy(false)
关掉 Tomcat 的热部署扫描。内嵌场景下没有"部署目录"的概念,热部署无意义且会拖慢启动。
(4) 默认协议
java
public static final String DEFAULT_PROTOCOL = "org.apache.coyote.http11.Http11NioProtocol";
Http11NioProtocol ------ Java NIO 实现的 HTTP/1.1。
(5) customizeConnector 里的 HTTP/2
java
protected void customizeConnector(Connector connector) {
int port = Math.max(getPort(), 0);
connector.setPort(port);
if (StringUtils.hasText(getServerHeader())) {
connector.setProperty("server", getServerHeader());
}
if (connector.getProtocolHandler() instanceof AbstractProtocol<?> abstractProtocol) {
customizeProtocol(abstractProtocol);
}
invokeProtocolHandlerCustomizers(connector.getProtocolHandler());
if (getUriEncoding() != null) {
connector.setURIEncoding(getUriEncoding().name());
}
if (getHttp2() != null && getHttp2().isEnabled()) {
connector.addUpgradeProtocol(new Http2Protocol()); // ★ HTTP/2 是"升级协议"
}
// ...
}
注意 Math.max(getPort(), 0):server.port = -1 时,connector 的端口是 0 (0 表示"随机选一个空闲端口")。
但 autoStart = getPort() >= 0 为 false,所以 connector 启动后会立即被停掉------ 所以 server.port=-1 不会随机占一个端口,而是真的不监听。
HTTP/2 通过 addUpgradeProtocol(new Http2Protocol()) 添加------ 即 HTTP/2 走的是 h2c 升级(明文 HTTP/2)路径。
(6) ignoringNoSuchMethodError
prepareContext 里有这么一段:
java
private void ignoringNoSuchMethodError(Runnable method) {
try {
method.run();
}
catch (NoSuchMethodError ex) {
// Intentionally left blank
}
}
用在 resourceRoot.setReadOnly(true) 上:
java
ignoringNoSuchMethodError(() -> resourceRoot.setReadOnly(true));
为什么? 因为 WebResourceRoot.setReadOnly(boolean) 是某个 Tomcat 版本才加的方法。 Boot 需要兼容多个 Tomcat 版本,老版本上调用会抛 NoSuchMethodError。
这是一种版本兼容的软适配:用运行时错误而不是编译期依赖。
方法论 :看到
catch (NoSuchMethodError)/catch (NoClassDefFoundError)且注释是 "Intentionally left blank", 那就是多版本兼容的降级点。删掉它会让老版本 Tomcat 直接崩。
6.4 ★ TomcatWebServer.initialize():全书最反直觉的一段
文件坐标 :module/spring-boot-tomcat/src/main/java/org/springframework/boot/tomcat/TomcatWebServer.java
java
private void initialize() throws WebServerException {
logger.info("Tomcat initialized with " + getPortsDescription(false)); // ① 第一行日志
synchronized (this.monitor) {
try {
addInstanceIdToEngineName(); // ②
Context context = findContext();
context.addLifecycleListener((event) -> {
if (context.equals(event.getSource()) && Lifecycle.START_EVENT.equals(event.getType())) {
// Remove service connectors so that protocol binding doesn't
// happen when the service is started.
removeServiceConnectors(); // ③ ★ 摘掉 connector
}
});
disableBindOnInit(); // ④
// Start the server to trigger initialization listeners
this.tomcat.start(); // ⑤ ★ 启动(但不监听端口)
// We can re-throw failure exception directly in the main thread
rethrowDeferredStartupExceptions(); // ⑥
try {
ContextBindings.bindClassLoader(context, context.getNamingToken(), getClass().getClassLoader());
}
catch (NamingException ex) {
// Naming is not enabled. Continue
}
// Unlike Jetty, all Tomcat threads are daemon threads. We create a
// blocking non-daemon to stop immediate shutdown
startNonDaemonAwaitThread(); // ⑦ ★
}
catch (Exception ex) {
stopSilently();
destroySilently();
throw new WebServerException("Unable to start embedded Tomcat", ex);
}
}
}
逐点解释
① 第一行日志 "Tomcat initialized with ..."
getPortsDescription(false) 的参数 false 表示"不显示实际端口"(因为还没绑定), 只是描述配置的端口。
② addInstanceIdToEngineName()
java
private static final AtomicInteger containerCounter = new AtomicInteger(-1);
private void addInstanceIdToEngineName() {
int instanceId = containerCounter.incrementAndGet();
if (instanceId > 0) {
Engine engine = this.tomcat.getEngine();
engine.setName(engine.getName() + "-" + instanceId);
}
}
containerCounter 是 static ,从 -1 开始,incrementAndGet() 后第一个实例得到 0。
if (instanceId > 0) ------ 第一个实例(id=0)不改名字 ,第二个开始改名(Tomcat-1、Tomcat-2...)。
为什么? 因为一个 JVM 里可能有多个内嵌 Tomcat(测试并行、Actuator 管理端口子上下文)。 Tomcat 内部有些地方用 engine name 做标识,重名会导致冲突。
注意这是 static 全局计数器 ------即跨 SpringApplication 实例累加。 在长跑的测试 JVM 里,这个数字会一直涨。这是有意的(保证唯一性),但也意味着 engine 名字在不同测试运行间不可预测。
③ ★ removeServiceConnectors() ------ 摘掉 connector
java
Context context = findContext();
context.addLifecycleListener((event) -> {
if (context.equals(event.getSource()) && Lifecycle.START_EVENT.equals(event.getType())) {
// Remove service connectors so that protocol binding doesn't
// happen when the service is started.
removeServiceConnectors();
}
});
注意:这不是立即 摘掉,而是注册一个 LifecycleListener , 在 Context 的 START_EVENT 时摘掉。
为什么是这个时机?
Tomcat 的启动顺序是:Server → Service → Engine → Host → Context → Wrapper。 Connector 属于 Service 层,在 Service 启动时会被初始化并绑定端口。
但我们需要的是:
- Tomcat 要启动到"Context 就绪"的程度(这样
ServletContext可用、ServletContextInitializer可以执行) - 但不要绑定端口
所以策略是:在 Service 启动到 Context 时,把 connector 从 Service 里摘走。 这样后续的 connector 初始化/绑定就不会发生。
removeServiceConnectors() 的实现:
java
private void removeServiceConnectors() {
doWithConnectors((service, connectors) -> {
this.serviceConnectors.put(service, connectors); // ★ 先缓存
for (Connector connector : connectors) {
service.removeConnector(connector); // 再移除
}
});
}
private void doWithConnectors(BiConsumer<Service, Connector[]> consumer) {
for (Service service : this.tomcat.getServer().findServices()) {
Connector[] connectors = service.findConnectors().clone(); // ★ clone
consumer.accept(service, connectors);
}
}
两个细节:
this.serviceConnectors.put(service, connectors)------ 缓存到 Map ,供start()时恢复service.findConnectors().clone()------ 必须 clone ,因为removeConnector会修改原数组
serviceConnectors 的类型是 Map<Service, Connector[]>(普通 HashMap,非并发容器)。 它只在 synchronized (this.monitor) 块内访问,所以安全。
④ disableBindOnInit()
java
private void disableBindOnInit() {
doWithConnectors((service, connectors) -> {
for (Connector connector : connectors) {
Object bindOnInit = connector.getProperty("bindOnInit");
if (bindOnInit == null) {
connector.setProperty("bindOnInit", "false");
}
}
});
}
双保险 :即使某个 connector 没被摘掉(比如它在 Context START_EVENT 之后才被添加), 也通过 bindOnInit=false 阻止它在初始化时绑定端口。
注意 if (bindOnInit == null) 的守卫:尊重用户显式设置的值 。 如果用户自己设了 bindOnInit=true,Boot 不覆盖。
这是"框架默认值不覆盖用户显式配置"的范例。 判断框架是否会覆盖你的设置:看有没有
if (getProperty(x) == null)这类守卫。
⑤ this.tomcat.start()
注释是 // Start the server to trigger initialization listeners。
目的不是"启动服务",而是"触发初始化监听器" ------ 即让 ServletContextInitializer(包括 ServletContext 上的 ServletContainerInitializer) 得以执行,从而注册 DispatcherServlet、Filter 等。
注意此时:
- Context 已启动
- ServletContext 可用
- 但端口未监听
⑥ rethrowDeferredStartupExceptions()
java
private void rethrowDeferredStartupExceptions() throws Exception {
Container[] children = this.tomcat.getHost().findChildren();
for (Container container : children) {
if (container instanceof TomcatEmbeddedContext embeddedContext) {
embeddedContext.getDeferredStartupExceptions().rethrow();
}
if (!LifecycleState.STARTED.equals(container.getState())) {
throw new IllegalStateException(container + " failed to start");
}
}
}
两件事:
- 重新抛出被延迟的异常 :
TomcatEmbeddedContext收集了ServletContainerInitializer执行期间的异常, 在这里一次性抛出。 - 检查每个 Container 的状态 是否为
STARTED。
DeferredServletContainerInitializers 是 Boot 自己实现的:
java
protected void configureContext(Context context, Iterable<ServletContextInitializer> initializers) {
DeferredServletContainerInitializers deferredInitializers = new DeferredServletContainerInitializers(
initializers);
if (context instanceof TomcatEmbeddedContext embeddedContext) {
embeddedContext.setDeferredStartupExceptions(deferredInitializers);
embeddedContext.setFailCtxIfServletStartFails(true);
}
context.addServletContainerInitializer(deferredInitializers, NO_CLASSES);
// ...
}
NO_CLASSES 是一个空的 Class<?>[]------意味着这个 initializer 不匹配任何类 , Tomcat 的 @HandlesTypes 过滤不会命中它,但它仍然会被调用 (Tomcat 对 ServletContainerInitializer 的调用规则:没有 @HandlesTypes 的总是被调用)。
⚠️ 未核实 :Tomcat 是否总是调用没有
@HandlesTypes注解的ServletContainerInitializer属于 Tomcat 的行为,不在本仓库。这是我对 Servlet 规范的理解,标记为推测。
⑦ ★ startNonDaemonAwaitThread()
java
// Unlike Jetty, all Tomcat threads are daemon threads. We create a
// blocking non-daemon to stop immediate shutdown
private void startNonDaemonAwaitThread() {
Thread awaitThread = new Thread("container-" + (containerCounter.get())) {
@Override
public void run() {
TomcatWebServer.this.tomcat.getServer().await();
}
};
awaitThread.setContextClassLoader(getClass().getClassLoader());
awaitThread.setDaemon(false); // ★ 非守护线程
awaitThread.start();
}
这是全章最精妙的一处。
问题:Tomcat 的所有工作线程(包括 acceptor、poller、executor)都是守护线程 。 JVM 的规则是:当只剩守护线程时,JVM 退出。
所以如果你有一个 main 方法跑完就结束的程序(比如一个测试、一个一次性任务), Tomcat 的守护线程不会 阻止 JVM 退出------应用会在 main 返回后立即死掉。
Boot 的解法:创建一个非守护线程,让它在 server.await() 上阻塞。
Server.await() 的行为是:阻塞等待一个 shutdown 命令(通常通过 socket 发送)。 在内嵌场景下没人会发这个命令,所以这个线程永远阻塞 ------ 它的唯一作用就是"占住一个非守护线程,让 JVM 不退出"。
对比 Jetty:
java
// JettyWebServer.initialize()
this.server.start();
this.server.setStopAtShutdown(false);
Jetty 的注释说 "Unlike Jetty, all Tomcat threads are daemon threads"------ Jetty 的线程不是守护线程,所以 Jetty 不需要这个 hack。
而且 Jetty 侧还有 setStopAtShutdown(false)------关掉 Jetty 自己的 shutdown hook, 让生命周期完全由 Spring 的 SmartLifecycle 控制。
方法论提炼 :看到"创建一个什么都不干的阻塞线程",先怀疑是"阻止 JVM 退出 "。 判断依据:
setDaemon(false)+ 阻塞调用 + 名字带await/container。
异常处理
java
catch (Exception ex) {
stopSilently();
destroySilently();
throw new WebServerException("Unable to start embedded Tomcat", ex);
}
stopSilently() / destroySilently() 都吞掉 LifecycleException:
java
private void stopSilently() {
try {
stopTomcat();
}
catch (LifecycleException ex) {
// Ignore
}
}
private void destroySilently() {
try {
this.tomcat.destroy();
}
catch (LifecycleException ex) {
// Ignore
}
}
清理失败不能掩盖原始异常 ------这正是 addSuppressed 的反向应用: 这里是"清理异常被丢弃,原始异常被抛出"。 因为原始异常(为什么启动失败)才是用户需要看的。
stopTomcat() 里有一个细节:
java
private void stopTomcat() throws LifecycleException {
if (Thread.currentThread().getContextClassLoader() instanceof TomcatEmbeddedWebappClassLoader) {
Thread.currentThread().setContextClassLoader(getClass().getClassLoader());
}
this.tomcat.stop();
}
如果当前 TCCL 是 Tomcat 的 webapp classloader,先换回 Boot 自己的 classloader。
为什么?因为 TomcatEmbeddedWebappClassLoader 在 webapp 停止后会被关闭, 此时再用它加载类会失败。这是一个类加载器生命周期的防御。
6.5 start():把 connector 装回去
java
@Override
public void start() throws WebServerException {
synchronized (this.monitor) {
if (this.started) {
return;
}
try {
addPreviouslyRemovedConnectors(); // ① ★ 装回 connector
Connector connector = this.tomcat.getConnector();
if (connector != null && this.autoStart) {
performDeferredLoadOnStartup(); // ② DispatcherServlet 真正初始化
}
checkThatConnectorsHaveStarted(); // ③ 检查失败
this.started = true;
logger.info(getStartedLogMessage()); // ④ 第二行日志
}
catch (ConnectorStartFailedException ex) {
stopSilently();
throw ex;
}
catch (Exception ex) {
PortInUseException.throwIfPortBindingException(ex, () -> this.tomcat.getConnector().getPort());
throw new WebServerException("Unable to start embedded Tomcat server", ex);
}
finally {
Context context = findContext();
ContextBindings.unbindClassLoader(context, context.getNamingToken(), getClass().getClassLoader());
}
}
}
① addPreviouslyRemovedConnectors()
java
private void addPreviouslyRemovedConnectors() {
Service[] services = this.tomcat.getServer().findServices();
for (Service service : services) {
Connector[] connectors = this.serviceConnectors.get(service);
if (connectors != null) {
for (Connector connector : connectors) {
service.addConnector(connector);
if (!this.autoStart) {
stopProtocolHandler(connector); // ★ autoStart=false 时立即停掉
}
}
this.serviceConnectors.remove(service);
}
}
}
service.addConnector(connector) 会触发 connector 的初始化和端口绑定。
if (!this.autoStart) stopProtocolHandler(connector); ------ 这就是 server.port = -1 的实现:绑定后立即停掉协议处理器。
stopProtocolHandler 的异常被吞掉(只打 error 日志):
java
private void stopProtocolHandler(Connector connector) {
try {
connector.getProtocolHandler().stop();
}
catch (Exception ex) {
logger.error("Cannot pause connector: ", ex);
}
}
② performDeferredLoadOnStartup()
java
private void performDeferredLoadOnStartup() {
try {
for (Container child : this.tomcat.getHost().findChildren()) {
if (child instanceof TomcatEmbeddedContext embeddedContext) {
embeddedContext.deferredLoadOnStartup();
}
}
}
catch (Exception ex) {
if (ex instanceof WebServerException webServerException) {
throw webServerException;
}
throw new WebServerException("Unable to start embedded Tomcat connectors", ex);
}
}
deferredLoadOnStartup() ------ 延迟到这一刻才真正 loadOnStartup。
Servlet 规范里 load-on-startup >= 0 的 Servlet 应该在 Context 启动时初始化。 但 Boot 把它推迟到端口绑定之后。
为什么?因为:
DispatcherServlet的初始化需要 Spring 容器的 Bean(如HandlerMapping)- 在
onRefresh()阶段(Tomcat 构造时),这些 Bean 可能还没创建完 - 推迟到
SmartLifecycle.start()(此时finishBeanFactoryInitialization已完成)更安全
这就是"Tomcat initialized"与"Tomcat started"之间那 300ms 的一部分 ------ DispatcherServlet 及其依赖的初始化。
这是理解 Spring Boot Web 启动顺序的关键一环 :
DispatcherServlet不是在ServletContext就绪时初始化的,而是在服务器真正 start 时 。 这意味着:如果你的DispatcherServlet初始化抛异常,此时端口已经绑定了。
③ checkThatConnectorsHaveStarted()
java
private void checkThatConnectorsHaveStarted() {
checkConnectorHasStarted(this.tomcat.getConnector());
for (Connector connector : this.tomcat.getService().findConnectors()) {
checkConnectorHasStarted(connector);
}
}
private void checkConnectorHasStarted(Connector connector) {
if (LifecycleState.FAILED.equals(connector.getState())) {
throw new ConnectorStartFailedException(connector.getPort());
}
}
配合前面 connector.setThrowOnFailure(true),这里检查 connector 的最终状态。
注意:主 connector 被检查了两次 (getConnector() 和遍历 findConnectors() 都会命中它)。 这没有害处(幂等检查),但说明代码是增量添加的------先加了主 connector 的检查,后来才补上 additional connectors。
ConnectorStartFailedException 是一个私有静态内部类:
java
// 推测定义(本仓库未直接读到,按用法推断)
private static class ConnectorStartFailedException extends WebServerException {
ConnectorStartFailedException(int port) { super("Connector configured to listen on port " + port + " failed to start", null); }
}
⚠️ 未核实 :我没有读到
ConnectorStartFailedException的定义。 从catch (ConnectorStartFailedException ex) { stopSilently(); throw ex; }的用法看, 它应该是WebServerException的子类(否则 catch 分支无法把它当 WebServerException 抛)。 这一条标记为推测。
④ getStartedLogMessage()
java
String getStartedLogMessage() {
String contextPath = getContextPath();
return "Tomcat started on " + getPortsDescription(true)
+ ((contextPath != null) ? " with context path '" + contextPath + "'" : "");
}
这里是 getPortsDescription(true)------实际端口(因为已经绑定了,随机端口的情况能拿到真值)。
6.6 时序小结
scss
getWebServer(init) → createTomcat → prepareContext → new TomcatWebServer(...).initialize()
① log "Tomcat initialized with ..."
② 注册 Context START_EVENT listener(摘 connector)
③ bindOnInit=false
④ tomcat.start()(端口未绑定)
⑤ rethrowDeferredStartupExceptions()
⑥ startNonDaemonAwaitThread()
══════ refresh() 继续 ══════
WebServerStartStopLifecycle.start() → TomcatWebServer.start()
① addPreviouslyRemovedConnectors() ← ★ 端口在这里才绑定
② performDeferredLoadOnStartup() ← DispatcherServlet 初始化
③ checkThatConnectorsHaveStarted()
④ log "Tomcat started on ..."
7. Jetty:另一种"先建后摘"
文件坐标 :module/spring-boot-jetty/src/main/java/org/springframework/boot/jetty/JettyWebServer.java
7.1 getWebServer()
java
@Override
public WebServer getWebServer(ServletContextInitializer... initializers) {
JettyEmbeddedWebAppContext context = new JettyEmbeddedWebAppContext();
context.getContext().getServletContext().setExtendedListenerTypes(true);
int port = Math.max(getPort(), 0);
InetSocketAddress address = new InetSocketAddress(getAddress(), port);
Server server = createServer(address);
context.setServer(server);
TempDirs tempDirs = new TempDirs(port);
server.addEventListener(new CleanTempDirsListener(tempDirs));
configureWebAppContext(context, tempDirs, initializers);
server.setHandler(addHandlerWrappers(context));
logger.info("Server initialized with port: " + port);
if (this.getMaxConnections() > -1) {
server.addBean(new NetworkConnectionLimit(this.getMaxConnections(), server.getConnectors()));
}
if (Ssl.isEnabled(getSsl())) {
customizeSsl(server, address);
}
for (JettyServerCustomizer customizer : getServerCustomizers()) {
customizer.customize(server);
}
if (this.isUseForwardHeaders()) {
new ForwardHeadersCustomizer().customize(server);
}
if (getShutdown() == Shutdown.GRACEFUL) {
StatisticsHandler statisticsHandler = new StatisticsHandler(); // ★
statisticsHandler.setHandler(server.getHandler());
server.setHandler(statisticsHandler);
}
return getJettyWebServer(server);
}
★ 注意最后那段 :Shutdown.GRACEFUL 时会包一层 StatisticsHandler。
这是 Jetty 优雅停机的前提------StatisticsHandler 提供 getRequestsActive(), 用来判断"还有没有活跃请求"。
这意味着:Jetty 的优雅停机需要额外的 Handler 层,而 Tomcat 和 Netty 不需要。
7.2 initialize()
java
private void initialize() {
synchronized (this.monitor) {
try {
// Cache the connectors and then remove them to prevent requests being
// handled before the application context is ready.
this.connectors = this.server.getConnectors();
JettyWebServer.this.server.setConnectors(null); // ★ 摘掉
// Start the server so that the ServletContext is available
this.server.start(); // 启动但不监听
this.server.setStopAtShutdown(false); // ★ 关掉 Jetty 自己的 hook
}
catch (Throwable ex) {
// Ensure process isn't left running
stopSilently();
throw new WebServerException("Unable to start embedded Jetty web server", ex);
}
}
}
与 Tomcat 的对照:
| 步骤 | Tomcat | Jetty |
|---|---|---|
| 摘 connector | 在 Context START_EVENT 的 listener 里 service.removeConnector(...) |
直接 server.setConnectors(null) |
| 缓存 | Map<Service, Connector[]> serviceConnectors |
Connector[] connectors 字段 |
| 双保险 | bindOnInit=false |
无 |
| 启动 | tomcat.start() |
server.start() |
| 阻止 JVM 退出 | startNonDaemonAwaitThread()(需要,因为线程都是 daemon) |
不需要(Jetty 线程非 daemon) |
| 关掉自带 hook | 无 | setStopAtShutdown(false) |
| 异常类型 | catch (Exception) |
catch (Throwable) |
Jetty 的实现明显更简单 ------因为 Jetty 的 Server API 允许直接把 connectors 置 null, 而 Tomcat 的 connector 是 Service 的一部分,只能通过 listener 在启动过程中摘。
注意 Jetty 的 catch (Throwable ex) 带注释 // Ensure process isn't left running------ 清理的目的是防止进程残留 。这与 Tomcat 的 stopSilently(); destroySilently(); 是同一意图。
7.3 start()
java
this.server.setConnectors(this.connectors); // ★ 装回 connector(触发绑定)
if (!this.autoStart) {
return; // ★ 注意:此时 started 仍为 false
}
try {
this.server.start();
handleDeferredInitialize(this.server);
for (Connector connector : this.server.getConnectors()) {
try { connector.start(); }
catch (IOException ex) {
if (connector instanceof NetworkConnector networkConnector) {
PortInUseException.throwIfPortBindingException(ex, networkConnector::getPort);
}
throw ex;
}
}
this.started = true;
logger.info(getStartedLogMessage());
}
catch (WebServerException ex) { stopSilently(); throw ex; }
catch (Exception ex) { stopSilently(); throw new WebServerException("Unable to start embedded Jetty server", ex); }
★ if (!this.autoStart) return; 在 started = true 之前 ------ 所以 autoStart=false 时 started 保持 false,之后再调 start() 会重新走一遍。 对比 Tomcat:Tomcat 是在加回 connector 后立即 stopProtocolHandler,但仍会走到 started = true。 两个实现在 autoStart=false 时的状态机不一致,未找到对应测试,标记为未核实。
端口占用判定只在 NetworkConnector 上做(Jetty 还有不涉及端口的 LocalConnector), 且用的是出问题的那个 connector 的端口,比 Tomcat 用主 connector 端口更准确。
7.4 handleDeferredInitialize
JettyServletWebServer 覆写了这个方法,递归遍历 Jetty 的 Handler 树, 找到 JettyEmbeddedWebAppContext 并调 deferredInitialize():
java
private void handleDeferredInitialize(Handler handler) throws Exception {
if (handler instanceof JettyEmbeddedWebAppContext jettyEmbeddedWebAppContext) {
jettyEmbeddedWebAppContext.deferredInitialize(); // 终止递归
}
else if (handler instanceof Handler.Wrapper handlerWrapper) {
handleDeferredInitialize(handlerWrapper.getHandler()); // 单子包装
}
else if (handler instanceof Handler.Collection handlerCollection) {
handleDeferredInitialize(handlerCollection.getHandlers()); // 多子集合
}
}
三种 Handler 形态对应三种递归分支。这与 Tomcat 的 performDeferredLoadOnStartup() 是同一目的: 把 Servlet 的 load-on-startup 推迟到服务器真正 start 时。
7.5 端口占用的判定
java
catch (IOException ex) {
if (connector instanceof NetworkConnector networkConnector) {
PortInUseException.throwIfPortBindingException(ex, networkConnector::getPort);
}
throw ex;
}
只有 NetworkConnector(网络连接器)才做端口占用判定 。 Jetty 还有 LocalConnector 等非网络连接器,它们不涉及端口。
对比 Tomcat:Tomcat 是在最外层 catch (Exception ex) 里统一判定:
java
catch (Exception ex) {
PortInUseException.throwIfPortBindingException(ex, () -> this.tomcat.getConnector().getPort());
throw new WebServerException("Unable to start embedded Tomcat server", ex);
}
Tomcat 用 this.tomcat.getConnector().getPort()(主 connector 的端口), Jetty 用出问题的那个 connector 的端口。
在多 connector 场景下,Jetty 的更准确。
8. Reactor Netty:延迟绑定的天然优势
文件坐标 :module/spring-boot-reactor-netty/src/main/java/org/springframework/boot/reactor/netty/
8.1 createHttpServer()
java
private HttpServer createHttpServer() {
HttpServer server = HttpServer.create().bindAddress(this::getListenAddress);
Ssl ssl = getSsl();
if (Ssl.isEnabled(ssl)) {
server = customizeSslConfiguration(server, ssl);
}
if (getCompression() != null && getCompression().getEnabled()) {
CompressionCustomizer compressionCustomizer = new CompressionCustomizer(getCompression());
server = compressionCustomizer.apply(server);
}
server = server.protocol(listProtocols()).forwarded(this.useForwardHeaders);
return applyCustomizers(server);
}
注意这是纯声明式 的:HttpServer.create() 只是创建一个配置对象, .protocol(...)、.forwarded(...) 都是返回新对象的配置方法,没有任何 I/O。
bindAddress(this::getListenAddress) 传的是一个 Supplier<InetSocketAddress>------ 地址是在绑定时才求值的,不是现在。
8.2 getWebServer()
java
@Override
public WebServer getWebServer(HttpHandler httpHandler) {
HttpServer httpServer = createHttpServer();
ReactorHttpHandlerAdapter handlerAdapter = new ReactorHttpHandlerAdapter(httpHandler);
NettyWebServer webServer = createNettyWebServer(httpServer, handlerAdapter, this.lifecycleTimeout,
getShutdown());
webServer.setRouteProviders(this.routeProviders);
return webServer;
}
对比 Tomcat/Jetty:
| Tomcat | Jetty | Netty | |
|---|---|---|---|
getWebServer 里是否创建底层服务器对象 |
是(new Tomcat()) |
是(createServer(address)) |
是(HttpServer.create()) |
| 是否在该阶段绑定端口 | 通过 listener 摘 connector 避免 | 通过 setConnectors(null) 避免 |
天然不绑定 |
| 是否需要"阻止 JVM 退出"的线程 | 是 | 否 | 是(见下) |
8.3 NettyWebServer.start()
java
@Override
public void start() throws WebServerException {
DisposableServer disposableServer = this.disposableServer;
if (disposableServer == null) {
try {
disposableServer = startHttpServer();
this.disposableServer = disposableServer;
}
catch (Exception ex) {
PortInUseException.ifCausedBy(ex, ChannelBindException.class, (channelBindException) -> {
if (channelBindException.localPort() > 0 && !isPermissionDenied(channelBindException.getCause())) {
PortInUseException.throwIfPortBindingException(channelBindException,
channelBindException::localPort);
}
});
if (ex instanceof ChannelBindException channelBindException) {
PortInUseException.ifCausedBy(ex, NativeIoException.class, (nativeIoException) -> {
if (nativeIoException.expectedErr() == ERROR_ADDR_IN_USE) {
throw new PortInUseException(channelBindException.localPort(), ex);
}
});
}
throw new WebServerException("Unable to start Netty", ex);
}
logger.info(getStartedOnMessage(disposableServer));
startDaemonAwaitThread(disposableServer);
}
}
8.4 ★ 三层次的端口占用判定
Netty 的端口占用判定是三个实现里最复杂的,因为它要处理 Netty 特有的错误类型:
第一层:ChannelBindException + localPort() > 0 + 非权限问题
java
PortInUseException.ifCausedBy(ex, ChannelBindException.class, (channelBindException) -> {
if (channelBindException.localPort() > 0 && !isPermissionDenied(channelBindException.getCause())) {
PortInUseException.throwIfPortBindingException(channelBindException,
channelBindException::localPort);
}
});
PortInUseException.throwIfPortBindingException 内部会找 BindException 且消息含 "in use"。
第二层:NativeIoException 且错误码是 ERROR_ADDR_IN_USE
java
if (ex instanceof ChannelBindException channelBindException) {
PortInUseException.ifCausedBy(ex, NativeIoException.class, (nativeIoException) -> {
if (nativeIoException.expectedErr() == ERROR_ADDR_IN_USE) {
throw new PortInUseException(channelBindException.localPort(), ex);
}
});
}
java
/**
* Address in use error code from {@code errno.h}.
*/
private static final int ERROR_ADDR_IN_USE = -98;
-98 是 Linux errno.h 里 EADDRINUSE 的值 (取负)。 io.netty.channel.unix.Errors.NativeIoException 在 Linux 原生传输(epoll)下会携带这个错误码。
注意:这是 Linux 专属的错误码 。其他平台(macOS/BSD)的 EADDRINUSE 是 48, Windows 有不同的错误码。这意味着第二层判定在 Linux 之外可能不生效。
⚠️ 推测 :
-98硬编码为 Linux 值,非 Linux 平台下expectedErr()返回的可能是不同的数值, 第二层判定会失败,退化到第一层或最外层的通用WebServerException。 我没有在非 Linux 平台验证过,标记为推测。
第三层:isPermissionDenied ------ 区分"端口被占用"和"没权限"
java
/**
* Permission denied error code from {@code errno.h}.
*/
private static final int ERROR_NO_EACCES = -13;
private boolean isPermissionDenied(@Nullable Throwable bindExceptionCause) {
try {
if (bindExceptionCause instanceof NativeIoException nativeException) {
return nativeException.expectedErr() == ERROR_NO_EACCES;
}
}
catch (Throwable ignore) {
}
return false;
}
-13 是 -EACCES(Permission denied)。
为什么需要区分? 因为绑定 80 端口(<1024 的特权端口)在非 root 下会失败, 错误也是 ChannelBindException,但原因完全不同------不是"被占用",是"没权限"。
如果不区分,用户会看到误导性的 "Port 80 is already in use", 而实际问题是权限。
这是错误处理里"区分相似症状的不同病因"的范例。
8.5 startDaemonAwaitThread
java
private void startDaemonAwaitThread(DisposableServer disposableServer) {
// 推测实现(本仓库未直接读到方法体)
}
⚠️ 未核实 :我读到了
startDaemonAwaitThread(disposableServer)的调用 , 但没有读到它的定义 (NettyWebServer.java中我读到的区间未包含该方法体)。 从名字看,它与 Tomcat 的startNonDaemonAwaitThread作用相同------阻止 JVM 退出。 但注意名字是startDaemonAwaitThread(Daemon!)------ 守护线程不能 阻止 JVM 退出,所以它的作用可能是别的(比如保持disposableServer的引用、处理 shutdown 信号)。 这一条是明确的未完成项,读者可自行阅读该方法体确认。
8.6 NettyRouteProvider
java
DisposableServer startHttpServer() {
HttpServer server = this.httpServer;
if (this.routeProviders.isEmpty()) {
server = server.handle(this.handler); // 所有请求走同一个 handler
}
else {
server = server.route(this::applyRouteProviders); // 允许注册自定义路由
}
// ...
}
NettyRouteProvider 是 4.x 新增的扩展点,让应用能在 Netty 层注册额外路由 (gRPC、Actuator 特殊端点等)。这与第 03 章讲的 4.x 模块化改造一脉相承。
8.7 getStartedOnMessage 的 tryAppend 技巧
java
private void tryAppend(StringBuilder message, String format, Supplier<Object> supplier) {
try {
Object value = supplier.get();
message.append((!message.isEmpty()) ? " " : "");
message.append(String.format(format, value));
}
catch (UnsupportedOperationException ex) {
// Ignore
}
}
DisposableServer.path() 在未配置 path 时会抛 UnsupportedOperationException (Reactor Netty 的 API 设计)。Boot 用 try-catch 适配这种"用异常表示不适用"的 API------ 这类 catch 不能删 ,删了会在无 path 配置时启动失败。 (lifecycleTimeout 决定 bindNow(timeout) 还是 bindNow(),其配置来源未核实。)
9. 响应式侧的特殊机制:WebServerManager 与延迟初始化
9.1 为什么需要 WebServerManager
servlet 侧,WebServer 直接由上下文持有;reactive 侧多了一层 WebServerManager。
文件坐标 :module/spring-boot-web-server/src/main/java/.../reactive/context/WebServerManager.java
java
/**
* Internal class used to manage the server and the {@link HttpHandler}, taking care not
* to initialize the handler too early.
*
* @author Andy Wilkinson
*/
class WebServerManager {
private final ReactiveWebServerApplicationContext applicationContext;
private final DelayedInitializationHttpHandler handler;
private final WebServer webServer;
WebServerManager(ReactiveWebServerApplicationContext applicationContext, ReactiveWebServerFactory factory,
Supplier<HttpHandler> handlerSupplier, boolean lazyInit) {
this.applicationContext = applicationContext;
Assert.notNull(factory, "'factory' must not be null");
this.handler = new DelayedInitializationHttpHandler(handlerSupplier, lazyInit);
this.webServer = factory.getWebServer(this.handler);
}
void start() {
this.handler.initializeHandler();
this.webServer.start();
this.applicationContext
.publishEvent(new ReactiveWebServerInitializedEvent(this.webServer, this.applicationContext));
}
void shutDownGracefully(GracefulShutdownCallback callback) {
this.webServer.shutDownGracefully(callback);
}
void stop() {
this.webServer.stop();
}
WebServer getWebServer() {
return this.webServer;
}
HttpHandler getHandler() {
return this.handler;
}
// ...
}
类注释直说目的:"taking care not to initialize the handler too early"。
问题是什么?
factory.getWebServer(httpHandler) 需要一个 HttpHandler。 但 HttpHandler 是从 BeanFactory 里取的(getHttpHandler())------ 而在 onRefresh() 阶段(此时调用 createWebServer()), Bean 还没有全部创建完 (finishBeanFactoryInitialization() 在 onRefresh() 之后)。
如果直接 getBeanFactory().getBean(HttpHandler.class), 会提前实例化 HttpHandler 及其依赖(包括 DispatcherHandler、所有 RouterFunction、所有 Controller 相关的 Bean)。
这有什么问题?
- 破坏 lazy-init / 循环依赖处理:提前实例化会绕过 Spring 的 Bean 创建顺序管理
- 破坏 AOT / 延迟语义
- 如果
HttpHandler依赖的 Bean 尚未就绪,可能导致不完整初始化
Boot 的解法 :传一个代理 DelayedInitializationHttpHandler 给 factory, 真正的 HttpHandler 等到 start() 时才取。
9.2 DelayedInitializationHttpHandler
java
/**
* A delayed {@link HttpHandler} that doesn't initialize things too early.
*/
static final class DelayedInitializationHttpHandler implements HttpHandler {
private final Supplier<HttpHandler> handlerSupplier;
private final boolean lazyInit;
private volatile HttpHandler delegate = this::handleUninitialized;
private DelayedInitializationHttpHandler(Supplier<HttpHandler> handlerSupplier, boolean lazyInit) {
this.handlerSupplier = handlerSupplier;
this.lazyInit = lazyInit;
}
private Mono<Void> handleUninitialized(ServerHttpRequest request, ServerHttpResponse response) {
throw new IllegalStateException("The HttpHandler has not yet been initialized");
}
@Override
public Mono<Void> handle(ServerHttpRequest request, ServerHttpResponse response) {
return this.delegate.handle(request, response);
}
void initializeHandler() {
this.delegate = this.lazyInit ? new LazyHttpHandler(this.handlerSupplier) : this.handlerSupplier.get();
}
HttpHandler getHandler() {
return this.delegate;
}
}
三态设计:
| 阶段 | delegate 的值 |
收到请求时的行为 |
|---|---|---|
| 构造后、初始化前 | this::handleUninitialized |
抛 IllegalStateException("The HttpHandler has not yet been initialized") |
initializeHandler() 后(非 lazy) |
handlerSupplier.get() 的真实 handler |
正常处理 |
initializeHandler() 后(lazy) |
new LazyHttpHandler(supplier) |
首次请求时才取真实 handler |
注意那个"未初始化"的哨兵方法:
java
private volatile HttpHandler delegate = this::handleUninitialized;
初始值是一个会抛异常的方法引用 。这是一个"未就绪哨兵"模式:
- 不是
null(避免 NPE,给明确错误信息) - 不是空实现(避免"请求被静默吞掉")
- 抛异常,明确告诉调用者"还没好"
对比 servlet 侧没有这个机制------因为 servlet 侧的 ServletContextInitializer (即 WebApplicationContextInitializer)是在 tomcat.start() 时通过生命周期回调执行的, 而 DispatcherServlet 的初始化被推迟到 performDeferredLoadOnStartup()。
两条路径解决的是同一个问题,用了不同手段:
| servlet 侧 | reactive 侧 | |
|---|---|---|
| 延迟的东西 | DispatcherServlet 的 loadOnStartup |
HttpHandler 的获取 |
| 手段 | deferredLoadOnStartup()(Tomcat)/ deferredInitialize()(Jetty) |
DelayedInitializationHttpHandler 代理 |
| 触发时机 | WebServer.start() 内 |
WebServerManager.start() 内(在 webServer.start() 之前) |
9.3 LazyHttpHandler
java
/**
* {@link HttpHandler} that initializes its delegate on first request.
*/
private static final class LazyHttpHandler implements HttpHandler {
private final Mono<HttpHandler> delegate;
private LazyHttpHandler(Supplier<HttpHandler> handlerSupplier) {
this.delegate = Mono.fromSupplier(handlerSupplier);
}
@Override
public Mono<Void> handle(ServerHttpRequest request, ServerHttpResponse response) {
return this.delegate.flatMap((handler) -> handler.handle(request, response));
}
}
用 Mono.fromSupplier(supplier) ------ Reactor 的 fromSupplier 是惰性的 : 每次 subscribe 时才调用 supplier,且默认缓存结果 (Mono.fromSupplier 每次订阅都会重新调用 supplier, 除非用 .cache()------这里没有 .cache())。
⚠️ 推测 :
Mono.fromSupplier每次订阅都会调用一次 supplier。 这意味着LazyHttpHandler可能每次请求都调用一次handlerSupplier.get()。 但handlerSupplier是applicationContext::getHttpHandler, 而getHttpHandler()最终是getBeanFactory().getBean(name, HttpHandler.class)------ 单例 Bean 会被缓存 ,所以重复调用是廉价的(一次 beanFactory 查找)。 这一条我未能核实Mono.fromSupplier的缓存语义,标记为推测。
lazyInit 从哪来?第 5.3 节讲过:
java
boolean lazyInit = getBeanFactory().getBeanDefinition(webServerFactoryBeanName).isLazyInit();
即:ReactiveWebServerFactory bean 的 lazy-init 属性 决定了 HttpHandler 是否延迟初始化。
这个传播关系有点绕: server.factory bean 的 lazy-init → WebServerManager.lazyInit → HttpHandler 的初始化时机
9.4 ReactiveWebServerInitializedEvent
java
void start() {
this.handler.initializeHandler();
this.webServer.start();
this.applicationContext
.publishEvent(new ReactiveWebServerInitializedEvent(this.webServer, this.applicationContext));
}
对比 servlet 侧:
java
@Override
public void start() {
this.webServer.start();
this.running = true;
this.applicationContext
.publishEvent(new ServletWebServerInitializedEvent(this.webServer, this.applicationContext));
}
两个事件都继承 WebServerInitializedEvent,都由 ServerPortInfoApplicationContextInitializer 监听(见 §11)。
顺序差异:
- reactive :先
initializeHandler()→ 再webServer.start()→ 再发事件 - servlet :直接
webServer.start()(内部才做 deferredLoadOnStartup)→ 再发事件
reactive 侧把"handler 初始化"放在 webServer.start() 之前 , 是因为 Netty 的 bindNow() 会立即开始接受连接------ 所以 handler 必须在绑定前就绪。
servlet 侧则相反:performDeferredLoadOnStartup() 在 connector 绑定之后 (因为 Tomcat 的 connector 绑定是 service.addConnector() 触发的,而 deferredLoadOnStartup 在其后)。
这是一个微妙但重要的差异:
- Tomcat :端口先开,然后
DispatcherServlet才初始化 → 存在一个"端口已监听但 MVC 未就绪"的窗口- Netty:handler 先就绪,然后端口才开 → 不存在这个窗口
这解释了为什么在 Tomcat 上,应用刚启动的极短时间内可能收到 503 或 "Servlet 未初始化" 错误, 而 Netty 上不会。 (这一推论基于代码顺序,我未做并发实测,标记为推演。)
10. 生命周期编排:两个 SmartLifecycle
10.1 两个 Lifecycle 的注册
第 5.1 节讲过,两个 Lifecycle 通过 registerSingleton 注册:
java
getBeanFactory().registerSingleton("webServerGracefulShutdown", new WebServerGracefulShutdownLifecycle(webServer));
getBeanFactory().registerSingleton("webServerStartStop", new WebServerStartStopLifecycle(this, webServer));
10.2 Phase 数值
java
public interface WebServerApplicationContext extends ApplicationContext {
int GRACEFUL_SHUTDOWN_PHASE = SmartLifecycle.DEFAULT_PHASE - 1024;
int START_STOP_LIFECYCLE_PHASE = GRACEFUL_SHUTDOWN_PHASE - 1024;
// ...
}
SmartLifecycle.DEFAULT_PHASE = Integer.MAX_VALUE = 2,147,483,647
GRACEFUL_SHUTDOWN_PHASE= 2,147,482,623START_STOP_LIFECYCLE_PHASE= 2,147,481,599
两个 Lifecycle 的 getPhase():
java
// WebServerGracefulShutdownLifecycle
@Override
public int getPhase() {
return WebServerApplicationContext.GRACEFUL_SHUTDOWN_PHASE; // 2097152 之下 1024
}
// WebServerStartStopLifecycle(servlet 版与 reactive 版一致)
@Override
public int getPhase() {
return WebServerApplicationContext.START_STOP_LIFECYCLE_PHASE; // 再减 1024
}
10.3 启动顺序
Spring 的 DefaultLifecycleProcessor.startBeans() 按 phase 升序启动。
ini
phase 小的先启动
↓
START_STOP_LIFECYCLE_PHASE (MAX-2048) → webServerStartStop.start() → 端口绑定
↓
GRACEFUL_SHUTDOWN_PHASE (MAX-1024) → webServerGracefulShutdown.start() → 只置 running=true
↓
其他 phase = MAX 的普通 SmartLifecycle
注意 WebServerGracefulShutdownLifecycle.start() 的实现:
java
@Override
public void start() {
this.running = true; // ★ 什么都不做,只是标记
}
它不启动任何东西 。它的存在只是为了在停止阶段参与排序。
10.4 停止顺序
DefaultLifecycleProcessor.stopBeans() 按 phase 降序停止。
scss
phase 大的先停止
↓
GRACEFUL_SHUTDOWN_PHASE (MAX-1024) → webServerGracefulShutdown.stop(callback)
→ webServer.shutDownGracefully(...)
→ 回调完成前,Spring 会等待(因为用了 stop(Runnable))
↓
START_STOP_LIFECYCLE_PHASE (MAX-2048) → webServerStartStop.stop() → webServer.stop()
↓
其他
这正是 "先优雅停止接收、等待活跃请求、再真正关闭" 的顺序。
10.5 ★ WebServerGracefulShutdownLifecycle.stop() 抛异常
java
@Override
public void stop() {
throw new UnsupportedOperationException("Stop must not be invoked directly");
}
@Override
public void stop(Runnable callback) {
this.running = false;
this.webServer.shutDownGracefully((result) -> callback.run());
}
stop()(无参)直接抛 UnsupportedOperationException!
为什么?因为 SmartLifecycle 有两个 stop 方法:
void stop()------ 同步停止default void stop(Runnable callback)------ 异步停止,完成时调 callback
DefaultLifecycleProcessor 在停止时:
- 如果
isRunning()为 true 且实现了stop(Runnable),会优先用带 callback 的版本并设置超时 - 否则用
stop()
优雅停机必须 用异步版本------因为"等待活跃请求完成"可能需要很久, 同步 stop() 会阻塞整个关闭流程且无法超时控制。
所以 Boot 用"抛异常"来强制调用方使用正确的方法。
方法论提炼 :看到一个方法体只有
throw new UnsupportedOperationException(...), 那不是"未实现",而是"禁止这样调用 "。 判断依据:有没有另一个重载做同样的事(这里有stop(Runnable))。这与第 15 章见过的
TypeExcludeFilter.equals/hashCode直接抛IllegalStateException是同一手法: 用异常表达"这个入口不该被走到"。
再看 stop(Runnable callback) 的实现:
java
this.webServer.shutDownGracefully((result) -> callback.run());
result 参数被丢弃了 !GracefulShutdownResult(IDLE / REQUESTS_ACTIVE / IMMEDIATE) 在传出去后没有做任何处理------只用于触发 callback。
这意味着
GracefulShutdownResult在当前实现里没有被 Boot 消费 。 三个 GracefulShutdown 实现会把结果写进日志("Graceful shutdown complete" / "aborted with one or more requests still active"), 但 Lifecycle 层面不区分。 这是一个"信息已经采集但尚未使用"的点。
10.6 isPauseable() 返回 false
两个 Lifecycle 都实现了:
java
@Override
public boolean isPauseable() {
return false;
}
isPauseable() 是 Spring 6.1+ 引入的 SmartLifecycle 方法, 用于支持 pause/resume 语义(而非完全 stop/start)。
返回 false 表示"这个组件不能被暂停,只能完全停止"。
对 Web 服务器来说这是合理的:你不能"暂停"一个端口监听然后恢复------ 要么监听,要么不监听。(虽然底层服务器可能支持暂停接收,但 Spring 的语义是 stop/start 级别的。)
10.7 servlet / reactive 两版 WebServerStartStopLifecycle 的差异
servlet 版持有 ServletWebServerApplicationContext + WebServer, start() 里 webServer.start() 后自己发 ServletWebServerInitializedEvent; reactive 版持有 WebServerManager,事件由 WebServerManager.start() 内部发出。
顺带一提:reactive 版的字段名是
weServerManager(漏了一个b), 这是 Boot 源码里的一处 typo。读源码时会遇到这类瑕疵------ 不要假设"官方代码一定完美",也不要对无关紧要的写法过度解读。
11. ★ 优雅停机:三种实现的语义差异
这是本章最有实践价值的一节。
11.1 GracefulShutdownResult 的三个值
java
public enum GracefulShutdownResult {
/**
* Requests remained active at the end of the grace period.
*/
REQUESTS_ACTIVE,
/**
* The server was idle with no active requests at the end of the grace period.
*/
IDLE,
/**
* The server was shutdown immediately, ignoring any active requests.
*/
IMMEDIATE
}
11.2 Tomcat 的实现
文件坐标 :module/spring-boot-tomcat/src/main/java/org/springframework/boot/tomcat/GracefulShutdown.java
java
void shutDownGracefully(GracefulShutdownCallback callback) {
logger.info("Commencing graceful shutdown. Waiting for active requests to complete");
CountDownLatch shutdownUnderway = new CountDownLatch(1);
new Thread(() -> doShutdown(callback, shutdownUnderway), "tomcat-shutdown").start();
try { shutdownUnderway.await(); } // ★ 阻塞直到"已在停"
catch (InterruptedException ex) { Thread.currentThread().interrupt(); }
}
private void doShutdown(GracefulShutdownCallback callback, CountDownLatch shutdownUnderway) {
try {
getConnectors().forEach(this::close); // ① 关闭连接器
shutdownUnderway.countDown(); // ② 放行主线程
awaitInactiveOrAborted(); // ③ 等待活跃请求
if (this.aborted) { ... callback.shutdownComplete(REQUESTS_ACTIVE); }
else { ... callback.shutdownComplete(IDLE); }
}
finally { shutdownUnderway.countDown(); } // ④ 异常时也放行
}
private void close(Connector connector) {
connector.pause(); // 停止接收新请求
connector.getProtocolHandler().closeServerSocketGraceful(); // 关闭监听 socket(已建连接可继续)
}
要点:
CountDownLatch的双向用法 :shutDownGracefully阻塞到"连接器已关闭"为止(但不等活跃请求);finally里再countDown()一次,保证异常时主线程不被永久阻塞。- 活跃判定两个维度 :
StandardContext.getInProgressAsyncCount() > 0(异步请求) 或任一StandardWrapper.getCountAllocated() > 0(servlet 实例被占用)。 - 50ms 轮询,无超时上限 ------只要没
abort()就一直等。 abort()的调用方未核实(推测是 Spring 超时逻辑)。
11.3 Jetty 的实现
java
void shutDownGracefully(GracefulShutdownCallback callback) {
logger.info("Commencing graceful shutdown. Waiting for active requests to complete");
new Thread(() -> awaitShutdown(callback), "jetty-shutdown").start();
for (Connector connector : this.server.getConnectors()) {
shutdown(connector); // ① 主线程关闭连接器
}
}
private void shutdown(Connector connector) {
connector.shutdown();
}
private void awaitShutdown(GracefulShutdownCallback callback) {
while (!this.aborted && this.activeRequests.get() > 0) {
sleep(100); // ★ 100ms 轮询
}
if (this.aborted) {
logger.info("Graceful shutdown aborted with one or more requests still active");
callback.shutdownComplete(GracefulShutdownResult.REQUESTS_ACTIVE);
}
else {
logger.info("Graceful shutdown complete");
callback.shutdownComplete(GracefulShutdownResult.IDLE);
}
}
与 Tomcat 的差异:
| Tomcat | Jetty | |
|---|---|---|
| 谁关连接器 | 新线程 | 主线程 |
| 活跃判定 | getInProgressAsyncCount() + getCountAllocated() |
StatisticsHandler.getRequestsActive() |
| 轮询间隔 | 50ms | 100ms |
| 同步原语 | CountDownLatch |
无(线程分离后不等) |
shutDownGracefully 是否阻塞 |
是(await latch) | 否(启动线程后立即返回) |
这是一个重要的行为差异:
- Tomcat :
shutDownGracefully()返回时,连接器已经关闭(但活跃请求可能还在跑) - Jetty :
shutDownGracefully()返回时,连接器可能还没关完(在另一个线程里)
由于 WebServerGracefulShutdownLifecycle.stop(Runnable) 依赖 callback 来通知完成, 两者都能正确工作------但时序不同。
activeRequests 从哪来?构造时传入:
java
// JettyWebServer.createGracefulShutdown
private @Nullable GracefulShutdown createGracefulShutdown(Server server) {
StatisticsHandler statisticsHandler = findStatisticsHandler(server);
if (statisticsHandler == null) {
return null; // ★ 没有 StatisticsHandler 就没有优雅停机
}
return new GracefulShutdown(server, statisticsHandler::getRequestsActive);
}
★ 关键 :JettyWebServer 的 gracefulShutdown 是否为 null,取决于 handler 树里有没有 StatisticsHandler , 而不是取决于 Shutdown.GRACEFUL 配置。
而 StatisticsHandler 是由 JettyServletWebServerFactory.getWebServer() 在 if (getShutdown() == Shutdown.GRACEFUL) 时才添加的。
所以链路是:
vbscript
server.shutdown=graceful
→ JettyServletWebServerFactory 添加 StatisticsHandler
→ JettyWebServer.createGracefulShutdown 找到它
→ new GracefulShutdown(server, statisticsHandler::getRequestsActive)
如果这条链断了(比如用户自定义了 handler 树,把 StatisticsHandler 覆盖了), 优雅停机会静默失效。
findStatisticsHandler 的递归:
java
private @Nullable StatisticsHandler findStatisticsHandler(Handler handler) {
if (handler instanceof StatisticsHandler statisticsHandler) {
return statisticsHandler;
}
if (handler instanceof Handler.Wrapper handlerWrapper) {
return findStatisticsHandler(handlerWrapper.getHandler());
}
return null;
}
注意:只处理 Handler.Wrapper,不处理 Handler.Collection 。 而前面 handleDeferredInitialize 是两种都处理的。
这是一个不对称 ------如果 handler 树里 StatisticsHandler 藏在 Handler.Collection 下面, findStatisticsHandler 找不到它。
⚠️ 推测 :这可能是一个 bug,也可能是有意的(因为
StatisticsHandler总是被server.setHandler()直接设置, 也就是在最外层,只会经过Wrapper链)。未核实,标记为待验证。
11.4 Reactor Netty 的实现
java
void shutDownGracefully(GracefulShutdownCallback callback) {
DisposableServer server = this.disposableServer.get();
if (server == null) {
return; // ★ 没启动过,直接返回(不调 callback!)
}
logger.info("Commencing graceful shutdown. Waiting for active requests to complete");
Thread shutdownThread = new Thread(() -> doShutdown(callback, server), "netty-shutdown");
this.shutdownThread = shutdownThread;
shutdownThread.start();
}
private void doShutdown(GracefulShutdownCallback callback, DisposableServer server) {
this.shuttingDown = true;
try {
server.disposeNow(Duration.ofNanos(Long.MAX_VALUE)); // ★
logger.info("Graceful shutdown complete");
callback.shutdownComplete(GracefulShutdownResult.IDLE);
}
catch (Exception ex) {
logger.info("Graceful shutdown aborted with one or more requests still active");
callback.shutdownComplete(GracefulShutdownResult.REQUESTS_ACTIVE);
}
finally {
this.shutdownThread = null;
this.shuttingDown = false;
}
}
★ disposeNow(Duration.ofNanos(Long.MAX_VALUE))
Long.MAX_VALUE 纳秒 ≈ 292.47 年。
这在语义上等于"无限等待 "。Reactor Netty 的 disposeNow(timeout) 会在超时后抛异常, Boot 用 Long.MAX_VALUE 来禁用超时。
为什么 Netty 不需要轮询? 因为 disposeNow 是 Reactor Netty 提供的原生优雅停机 API------ 它内部会等待所有活跃连接处理完毕。Tomcat/Jetty 没有这样的 API,只能自己轮询。
★ if (server == null) return; 不调 callback
这是一个潜在的问题 :如果 disposableServer 为 null(服务器从未启动), shutDownGracefully 直接返回,不调用 callback。
而 WebServerGracefulShutdownLifecycle.stop(Runnable callback) 依赖 callback 来通知 Spring"停止完成":
java
this.webServer.shutDownGracefully((result) -> callback.run());
如果 callback 不被调用,Spring 的 DefaultLifecycleProcessor 会一直等待(直到超时)。
⚠️ 推测的风险 :在
WebApplicationType.REACTIVE+ 服务器启动失败/未启动的场景下, 关闭流程可能挂起直到spring.lifecycle.timeout-per-shutdown-phase超时。 我没有实测过这个路径,标记为推测。
对比 Tomcat:Tomcat 的 shutDownGracefully(我未读到 TomcatWebServer.shutDownGracefully 的实现, 但按 this.gracefulShutdown 的存在性推断)在 gracefulShutdown == null 时会走什么分支? 未核实。
11.5 三实现对照表
| 维度 | Tomcat | Jetty | Reactor Netty |
|---|---|---|---|
| 优雅停机类 | org.springframework.boot.tomcat.GracefulShutdown |
org.springframework.boot.jetty.GracefulShutdown |
org.springframework.boot.reactor.netty.GracefulShutdown |
| 包可见性 | final class(包私有) |
final class(包私有) |
final class(包私有) |
| 前置条件 | shutdown == GRACEFUL |
StatisticsHandler 存在 |
shutdown == GRACEFUL |
| "停止接收新请求"手段 | connector.pause() + closeServerSocketGraceful() |
connector.shutdown() |
disposeNow()(Reactor 内部) |
| 活跃请求判定 | getInProgressAsyncCount() + getCountAllocated() |
StatisticsHandler.getRequestsActive() |
Reactor Netty 内部 |
| 等待方式 | 轮询(50ms) | 轮询(100ms) | 阻塞 API(无轮询) |
| 超时 | 无(等 abort()) |
无(等 abort()) |
Long.MAX_VALUE 纳秒(≈无) |
abort() |
this.aborted = true |
this.aborted = true |
中断线程 |
shutDownGracefully 阻塞? |
是(等 latch) | 否 | 否 |
| 服务器未启动时 | ?(未核实) | ?(未核实) | 直接 return,不调 callback |
11.6 abort() 的实现差异
Tomcat / Jetty :void abort() { this.aborted = true; } ------ 简单标志位,轮询循环下次检查时退出。
Netty:
java
void abort() {
Thread shutdownThread = this.shutdownThread;
if (shutdownThread != null) {
while (!this.shuttingDown) {
sleep(50); // ★ 忙等,直到 shuttingDown 变 true
}
shutdownThread.interrupt(); // ★ 中断阻塞在 disposeNow() 的线程
}
}
忙等的目的是竞态防护 :若在 shuttingDown = true 之前就 interrupt(), 中断会被尚未进入阻塞的 disposeNow 吞掉。先确认目标线程已进入临界区,再发信号。 (第 14 章讲 AutoConfigurationSorter 时见过类似的确定性处理思路。)
11.7 实践含义
yaml
server:
shutdown: graceful # ServerProperties 的默认值
spring:
lifecycle:
timeout-per-shutdown-phase: 30s # Spring 侧的编排超时
关键认知 :三个实现的优雅停机本身都没有内部超时 ; 超时由 DefaultLifecycleProcessor 的 timeout-per-shutdown-phase 提供。 超时后 Spring 继续停下一个 phase,即 webServer.stop()------强制关闭,活跃请求被打断。
完整流程:
- 停 phase MAX-1024 →
webServer.shutDownGracefully(...)→ 停止接收新请求、等待活跃请求 - 超时或 callback 触发后 → 停 phase MAX-2048 →
webServer.stop()→ 强制关闭
所以
timeout-per-shutdown-phase是"给优雅停机的时间预算", 不是服务器内部的超时。
12. 端口与诊断
12.1 ServerPortInfoApplicationContextInitializer
注册于 spring-boot-web-server 的 spring.factories(ApplicationContextInitializer)。
java
private static final String PROPERTY_SOURCE_NAME = "server.ports";
@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
applicationContext.addApplicationListener(this); // ★ 植入钩子
}
@Override
public void onApplicationEvent(WebServerInitializedEvent event) {
String propertyName = "local." + getName(event.getApplicationContext()) + ".port";
setPortProperty(event.getApplicationContext(), propertyName, event.getWebServer().getPort());
}
private String getName(WebServerApplicationContext context) {
String name = context.getServerNamespace();
return StringUtils.hasText(name) ? name : "server";
}
要点:
- 属性名是动态的 :
local.server.port(默认)/local.management.port(Actuator 子上下文,namespace="management")。 sources.addFirst(source):server.ports放在最前,优先级最高------真实端口必须覆盖任何配置。- 向父上下文传播 :
if (context.getParent() != null) setPortProperty(context.getParent(), ...), 所以主应用里能@Value("${local.management.port}")。 - 双身份:Initializer 用来"植入钩子",Listener 用来"响应事件"------常见模式。
12.2 PortInUseException 的判定链
java
public static void throwIfPortBindingException(Exception ex, IntSupplier port) {
ifPortBindingException(ex, (bindException) -> {
throw new PortInUseException(port.getAsInt(), ex);
});
}
public static void ifPortBindingException(Exception ex, Consumer<BindException> action) {
ifCausedBy(ex, BindException.class, (bindException) -> {
// bind exception can be also thrown because an address can't be assigned
String message = bindException.getMessage();
if (message != null && message.toLowerCase(Locale.ROOT).contains("in use")) {
action.accept(bindException);
}
});
}
@SuppressWarnings("unchecked")
public static <E extends Exception> void ifCausedBy(Exception ex, Class<E> causedBy, Consumer<E> action) {
Throwable candidate = ex;
while (candidate != null) {
if (causedBy.isInstance(candidate)) {
action.accept((E) candidate);
return;
}
candidate = candidate.getCause();
}
}
三层结构:
ifCausedBy------ 遍历 cause 链找指定类型(泛型 +isInstance)ifPortBindingException------ 找到BindException后,检查消息是否含"in use"throwIfPortBindingException------ 命中就抛PortInUseException
★ 消息字符串匹配:
java
if (message != null && message.toLowerCase(Locale.ROOT).contains("in use"))
注释解释了原因:
java
// bind exception can be also thrown because an address can't be assigned
BindException 不只是"端口被占用"------它还可能是"地址无法分配" (比如配置了一个本机不存在的 IP)。所以要靠消息区分。
这是一个脆弱但必要的判定:
- 脆弱 :依赖 JDK 的异常消息文案(英文 "in use")。 在非英文 locale 下,JDK 的
BindException消息通常仍是英文 (JDK 内部消息不走资源本地化), 所以toLowerCase(Locale.ROOT)用Locale.ROOT是安全的(避免土耳其 locale 的I问题)。 - 必要:没有更好的 API 能区分这两种 BindException。
方法论 :看到
toLowerCase(Locale.ROOT)而不是toLowerCase(), 那是locale 安全 的写法(土耳其语中I.toLowerCase() →ı,会破坏 "IN USE" 的匹配)。 Boot 里所有的大小写转换都用Locale.ROOT。这是一个可迁移到你自己代码的好习惯。
12.3 PortInUseFailureAnalyzer
java
class PortInUseFailureAnalyzer extends AbstractFailureAnalyzer<PortInUseException> {
@Override
protected FailureAnalysis analyze(Throwable rootFailure, PortInUseException cause) {
return new FailureAnalysis("Web server failed to start. Port " + cause.getPort() + " was already in use.",
"Identify and stop the process that's listening on port " + cause.getPort() + " or configure this "
+ "application to listen on another port.",
cause);
}
}
标准的 description + action 结构(第 16 章讲过)。
输出效果:
markdown
***************************
APPLICATION FAILED TO START
***************************
Description:
Web server failed to start. Port 8080 was already in use.
Action:
Identify and stop the process that's listening on port 8080 or configure this application to listen on another port.
12.4 MissingWebServerFactoryBeanFailureAnalyzer
properties
# spring-boot-web-server/META-INF/spring.factories
org.springframework.boot.diagnostics.FailureAnalyzer=\
org.springframework.boot.web.server.PortInUseFailureAnalyzer,\
org.springframework.boot.web.server.context.MissingWebServerFactoryBeanFailureAnalyzer
MissingWebServerFactoryBeanException 携带 webApplicationType, 所以 analyzer 能给出针对性建议("你是 SERVLET 类型,需要一个 ServletWebServerFactory")。
⚠️ 未核实 :我没有读到
MissingWebServerFactoryBeanFailureAnalyzer的源码, 只确认它注册在spring.factories里,且对应的异常类存在。 它的具体诊断文案未核实。
13. 定制体系:WebServerFactoryCustomizer
13.1 接口
java
public interface WebServerFactoryCustomizer<T extends WebServerFactory> {
void customize(T factory);
}
13.2 WebServerFactoryCustomizerBeanPostProcessor
java
public class WebServerFactoryCustomizerBeanPostProcessor implements BeanPostProcessor, BeanFactoryAware {
@SuppressWarnings("NullAway.Init")
private ListableBeanFactory beanFactory;
private @Nullable List<WebServerFactoryCustomizer<?>> customizers;
@Override
public void setBeanFactory(BeanFactory beanFactory) {
Assert.isInstanceOf(ListableBeanFactory.class, beanFactory, "'beanFactory' must be a ListableBeanFactory");
this.beanFactory = (ListableBeanFactory) beanFactory;
}
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof WebServerFactory webServerFactory) {
postProcessBeforeInitialization(webServerFactory);
}
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) {
return bean;
}
@SuppressWarnings("unchecked")
private void postProcessBeforeInitialization(WebServerFactory webServerFactory) {
LambdaSafe.callbacks(WebServerFactoryCustomizer.class, getCustomizers(), webServerFactory)
.withLogger(WebServerFactoryCustomizerBeanPostProcessor.class)
.invoke((customizer) -> customizer.customize(webServerFactory));
}
private Collection<WebServerFactoryCustomizer<?>> getCustomizers() {
if (this.customizers == null) {
// Look up does not include the parent context
this.customizers = new ArrayList<>(getWebServerFactoryCustomizerBeans());
this.customizers.sort(AnnotationAwareOrderComparator.INSTANCE);
this.customizers = Collections.unmodifiableList(this.customizers);
}
return this.customizers;
}
@SuppressWarnings({ "unchecked", "rawtypes" })
private Collection<WebServerFactoryCustomizer<?>> getWebServerFactoryCustomizerBeans() {
return (Collection) this.beanFactory.getBeansOfType(WebServerFactoryCustomizer.class, false, false).values();
}
}
★ LambdaSafe 的作用
LambdaSafe.callbacks(...) 是 Boot 的内部工具(第 15 章讲 @ConditionalOnXxx 时见过)。 它解决的问题:定制器与 Boot 之间的类加载器不匹配。
设想:一个定制器是 WebServerFactoryCustomizer<TomcatServletWebServerFactory>。 调用它的 customize(factory) 时,如果实际 factory 是 Jetty 的, 泛型擦除会导致 ClassCastException。
LambdaSafe 通过反射检查泛型参数 ,只把兼容的定制器应用到对应的 factory 上, 不兼容的静默跳过(并记录日志)。
这就是为什么一个 WebServerFactoryCustomizer<TomcatServletWebServerFactory> 类型的 bean 在 Jetty 环境下不会炸。
★ getBeansOfType(..., false, false)
java
this.beanFactory.getBeansOfType(WebServerFactoryCustomizer.class, false, false)
三个参数:(type, includeNonSingletons, allowEagerInit)。
includeNonSingletons = false------ 只要单例allowEagerInit = false------ 不触发 FactoryBean 的早期初始化
allowEagerInit=false 很重要:如果设为 true,会强制初始化所有 FactoryBean, 可能导致"过早初始化"问题(第 15 章提过 SearchStrategy 与 eager init 的关系)。
同样的注释 "Look up does not include the parent context" 在第 5.2 节见过------ Boot 在 Web 相关的地方反复强调"不查父上下文",因为 Actuator 子上下文的存在。
13.3 为什么用 ImportBeanDefinitionRegistrar 注册 BPP
java
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(ServerProperties.class)
@Import(ServletWebServerConfiguration.BeanPostProcessorsRegistrar.class)
public class ServletWebServerConfiguration { ... }
注释写明:"Registered via ImportBeanDefinitionRegistrar for early registration"。
原因:@Bean 定义的 BeanPostProcessor 有个时序问题------@Configuration 类本身要先被实例化, 而 BPP 必须在其他 Bean 创建之前 就位。用 ImportBeanDefinitionRegistrar 可以在配置类解析阶段 就注册 BPP 的 BeanDefinition,比 @Bean 更早。
registerSyntheticBeanIfMissing 还会 beanDefinition.setSynthetic(true)------ 标记为合成 Bean 定义(第 16 章讲过,基础设施 Bean 的惯例), 且带 "IfMissing" 守卫,避免与用户自定义的同类型 BPP 冲突。
13.4 定制器链的实际顺序
以 Tomcat servlet 应用为例,典型顺序是: ServletWebServerFactoryCustomizer(order=0,应用 server.*) → TomcatServletWebServerFactoryCustomizer(server.tomcat.*) → TomcatVirtualThreadsWebServerFactoryCustomizer → WebSocketTomcatWebServerFactoryCustomizer → (Actuator)ManagementWebServerFactoryCustomizer。
通用配置先于专属配置,这样专属配置能覆盖通用配置。
⚠️ 未核实 :除
ServletWebServerFactoryCustomizer.getOrder() == 0外, 其余定制器的@Order值我未逐一读取。
14. 排障手册
| 症状 | 原因 | 验证 | 修复 |
|---|---|---|---|
| "Port 8080 was already in use" | 端口被占 | `netstat -ano | findstr 8080(Win)/ lsof -i :8080`(Unix) |
| "Unable to start ServletWebServerApplicationContext due to missing ServletWebServerFactory bean" | 上下文是 SERVLET 但没有 starter-web | 看 ConditionEvaluationReport 里 TomcatServletWebServerAutoConfiguration 是否 matched |
加 spring-boot-starter-web;或 spring.main.web-application-type=none |
| "due to multiple ServletWebServerFactory beans : ..." | 两个 starter 同时引入 | mvn dependency:tree 找冲突 |
排除多余 starter |
| "Unable to start ReactiveWebApplicationContext due to missing HttpHandler bean" | WebFlux 自动配置未生效 | 看 HttpHandlerAutoConfiguration 的匹配情况 |
检查是否被 exclude / 条件不满足 |
| 端口监听了但请求 404 | context-path 配置 | 日志里 "with context path '/xxx'" | 检查 server.servlet.context-path |
| 关闭时请求被截断 | 优雅停机超时 | 日志 "Graceful shutdown aborted with one or more requests still active" | 调大 spring.lifecycle.timeout-per-shutdown-phase |
| Jetty 下优雅停机不生效 | 没有 StatisticsHandler |
调试 findStatisticsHandler |
确保 server.shutdown=graceful 且 handler 树未被覆盖 |
| 应用启动后进程立即退出 | 没有非守护线程托底 | 看是否有 container-N 线程 |
Tomcat 有 startNonDaemonAwaitThread;若被禁则检查 server.port=-1 |
local.server.port 拿不到 |
不是 WebServerApplicationContext,或事件未触发 |
检查上下文类型 | reactive/servlet 都会发 WebServerInitializedEvent,确认 initializer 已注册 |
Netty 下 PortInUseException 没出现,只有 "Unable to start Netty" |
非 Linux 平台的 EADDRINUSE 错误码不匹配,或不是 BindException |
看 cause 链最底层 | 这是 判定局限性,只能靠原始异常诊断 |
@Value("${local.management.port}") 解析失败 |
管理端口未启用,或 namespace 不对 | 看 server.ports PropertySource |
需要 Actuator + management.server.port |
15. 方法论:本章沉淀的三条定律
15.1 第十六定律:契约写在 Javadoc 里,实现用"奇怪代码"兑现
当你看到一段"绕远路"的代码(先摘后装、延迟绑定、代理转发), 先去找对应接口/方法的 Javadoc------那里往往有它要满足的契约。
本章的三个实证:
| 奇怪代码 | 它要满足的契约 | 契约出处 |
|---|---|---|
| Tomcat 先摘 connector 再 start 再装回 | "fully configured but paused" | ServletWebServerFactory.getWebServer() 的 Javadoc |
Jetty server.setConnectors(null) |
同上 | 同上 |
DelayedInitializationHttpHandler 代理 |
"not to initialize the handler too early" | WebServerManager 的类注释 |
WebServerGracefulShutdownLifecycle.stop() 抛异常 |
必须用异步 stop(Runnable) |
SmartLifecycle 的方法契约 |
反向应用 :当你自己要实现一个 Boot 的 SPI 时, 第一件事是读接口 Javadoc,而不是读参考实现。 因为参考实现里的奇怪代码是为了满足某个契约------如果你不知道契约,就会写出"能跑但违规"的实现。
这与第四定律("顺序语义常藏在 Javadoc 里")是同一家族:Boot 的契约在 Javadoc,不在代码。
15.2 第十七定律:"阻止 JVM 退出"的线程是诊断信号
看到
setDaemon(false)+ 阻塞调用 + 名字类似await/container的线程, 那是"让 JVM 不退出"的托底线程,不是业务逻辑。
Tomcat 的 startNonDaemonAwaitThread():
java
// Unlike Jetty, all Tomcat threads are daemon threads. We create a
// blocking non-daemon to stop immediate shutdown
为什么这条定律有用? 因为这类代码看起来毫无意义(一个阻塞在 await() 上、永远不会返回的线程), 初学者会以为是"等待关闭命令"的业务逻辑,然后试图"优化"它。
判定三要素:
setDaemon(false)------ 明确指定非守护- 阻塞调用(
await()、join()、take()) - 线程名是
container-N/xxx-await这类基础设施名字
对比 :Jetty 不需要(线程非 daemon);Netty 有 startDaemonAwaitThread(名字带 Daemon,作用待核实)。
15.3 第十八定律:多实现的差异点 = 底层 API 能力差
Boot 对同一契约的多个实现,结构差异几乎总是由底层库的能力差决定的,而非 Boot 的设计偏好。
| 契约 | Tomcat | Jetty | Netty | 差异根源 |
|---|---|---|---|---|
| 构造时不监听 | listener 里摘 connector | setConnectors(null) |
天然不绑定 | connector 归属/API 形态不同 |
| 阻止 JVM 退出 | 需要额外线程 | 不需要 | 需要(待核实) | 线程是否 daemon |
| 优雅停机等待 | 50ms 轮询 | 100ms 轮询 | 原生 disposeNow |
有无原生 API |
| 活跃请求判定 | 两个 Tomcat API | StatisticsHandler |
内部 | 是否需额外 Handler |
这也解释了为什么 spring-boot-web-server 抽象层不能把优雅停机做成模板方法: 底层能力差异太大,无法共用代码。
16. 本章未逐行核实 / 待你实测的项
- Tomcat 覆写
destroy()是推测(文中已声明)。 - Tomcat / Jetty / Netty 三套实现的全部配置项未逐一对比。
WebServer生命周期在 AOT 下的差异未验证。- 管理端口的
ManagementWebServerFactoryCustomizer本章只提到(第 25 章)。 - 优雅停机(graceful shutdown)的完整流程未逐行读。
下一章 :第 19 章进入 Spring MVC 自动配置------DispatcherServlet、消息转换与 Jackson 3。
17. 本章小结
事实清单
WebServer有 5 个方法:start/stop/getPort/shutDownGracefully(default)/destroy(default,@since 3.2.0)。getPort()返回-1表示无端口;server.port=-1时autoStart=false。- Tomcat 的
initialize()通过 Context START_EVENT 的 listener 摘 connector,另有bindOnInit=false双保险。 - Tomcat 有
startNonDaemonAwaitThread()因为 "all Tomcat threads are daemon threads";Jetty 不需要。 containerCounter是static AtomicInteger,从 -1 开始,第一个实例不改 engine 名。WebServerApplicationContext.GRACEFUL_SHUTDOWN_PHASE = MAX-1024,START_STOP_LIFECYCLE_PHASE = MAX-2048。WebServerGracefulShutdownLifecycle.stop()抛UnsupportedOperationException。GracefulShutdownResult在 Lifecycle 层被丢弃(只用于触发 callback)。- Jetty 的优雅停机依赖
StatisticsHandler存在 ,而非Shutdown.GRACEFUL配置。 PortInUseException.ifPortBindingException靠消息含 "in use" 区分"端口占用"与"地址无法分配"。WebServerFactoryCustomizerBeanPostProcessor用LambdaSafe跳过泛型不匹配的定制器。ServerPortInfoApplicationContextInitializer的属性名是local.{namespace}.port,用addFirst且向父上下文传播。
未核实清单
NettyWebServer.stop() 是否把 disposableServer 置回 null;startDaemonAwaitThread 的方法体与作用 (名字带 Daemon,与 Tomcat 的 non-daemon 不同);ConnectorStartFailedException 的定义; TomcatWebServer.shutDownGracefully / destroy 的实现;abort() 的调用方; MissingWebServerFactoryBeanFailureAnalyzer 的诊断文案;Mono.fromSupplier 是否缓存; Jetty autoStart=false 时状态机的意图;findStatisticsHandler 不处理 Handler.Collection 是否刻意; 各 WebServerFactoryCustomizer 的 @Order 值;lifecycleTimeout 的配置项; 以及第 17 章遗留的 WAR 部署下 WebApplicationContextInitializer 两次调用问题。
下一章预告
第 19 章:Spring MVC 自动配置 ------DispatcherServlet 是怎么被注册进去的, 消息转换器(HttpMessageConverter)的装配顺序,以及 4.x 的 Jackson 3 迁移。
我们会从这一章结尾的地方继续: performDeferredLoadOnStartup() 触发的 DispatcherServlet.init(), 以及 DispatcherServletRegistrationBean 的 loadOnStartup 为什么是 -1。
(本章完)