Spring Boot 4.1.1 深度源码解析-第 18 章 内嵌 Web 服务器:`WebServer` 抽象与 Tomcat/Jetty/Netty

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 时提过)。 它的核心特性:

  1. 源为 null 时不调用 setter ------ 避免用 null 覆盖掉 factory 的默认值
  2. 源抛异常时跳过 (PropertyMapper.get() 返回的是默认实例,默认忽略异常)
  3. 支持 .when(...) / .whenNot(...) 条件
  4. 支持 .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 0
  • TomcatServletWebServerFactoryCustomizer(Tomcat 专属 server.tomcat.*)------ 无 @Order(默认 LOWEST_PRECEDENCE?)
  • TomcatVirtualThreadsWebServerFactoryCustomizer
  • WebSocketTomcatWebServerFactoryCustomizer
  • ManagementWebServerFactoryCustomizer(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");
        }
    }
}

两件事:

  1. 重新抛出被延迟的异常 :TomcatEmbeddedContext 收集了 ServletContainerInitializer 执行期间的异常, 在这里一次性抛出。
  2. 检查每个 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 把它推迟到端口绑定之后。

为什么?因为:

  1. DispatcherServlet 的初始化需要 Spring 容器的 Bean(如 HandlerMapping)
  2. 在 onRefresh() 阶段(Tomcat 构造时),这些 Bean 可能还没创建完
  3. 推迟到 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)。

这有什么问题?

  1. 破坏 lazy-init / 循环依赖处理:提前实例化会绕过 Spring 的 Bean 创建顺序管理
  2. 破坏 AOT / 延迟语义
  3. 如果 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,623
  • START_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(已建连接可继续)
}

要点:

  1. CountDownLatch 的双向用法 :shutDownGracefully 阻塞到"连接器已关闭"为止(但不等活跃请求); finally 里再 countDown() 一次,保证异常时主线程不被永久阻塞。
  2. 活跃判定两个维度 :StandardContext.getInProgressAsyncCount() > 0(异步请求) 或任一 StandardWrapper.getCountAllocated() > 0(servlet 实例被占用)。
  3. 50ms 轮询,无超时上限 ------只要没 abort() 就一直等。
  4. 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()------强制关闭,活跃请求被打断。

完整流程:

  1. 停 phase MAX-1024 → webServer.shutDownGracefully(...) → 停止接收新请求、等待活跃请求
  2. 超时或 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";
}

要点:

  1. 属性名是动态的 :local.server.port(默认)/local.management.port(Actuator 子上下文,namespace="management")。
  2. sources.addFirst(source) :server.ports 放在最前,优先级最高------真实端口必须覆盖任何配置。
  3. 向父上下文传播 :if (context.getParent() != null) setPortProperty(context.getParent(), ...), 所以主应用里能 @Value("${local.management.port}")。
  4. 双身份: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();
    }
}

三层结构:

  1. ifCausedBy ------ 遍历 cause 链找指定类型(泛型 + isInstance)
  2. ifPortBindingException ------ 找到 BindException 后,检查消息是否含 "in use"
  3. 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() 上、永远不会返回的线程), 初学者会以为是"等待关闭命令"的业务逻辑,然后试图"优化"它。

判定三要素:

  1. setDaemon(false) ------ 明确指定非守护
  2. 阻塞调用(await()、join()、take())
  3. 线程名是 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. 本章未逐行核实 / 待你实测的项

  1. Tomcat 覆写 destroy() 是推测(文中已声明)。
  2. Tomcat / Jetty / Netty 三套实现的全部配置项未逐一对比。
  3. WebServer 生命周期在 AOT 下的差异未验证。
  4. 管理端口的 ManagementWebServerFactoryCustomizer 本章只提到(第 25 章)。
  5. 优雅停机(graceful shutdown)的完整流程未逐行读。

下一章 :第 19 章进入 Spring MVC 自动配置------DispatcherServlet、消息转换与 Jackson 3。


17. 本章小结

事实清单

  1. WebServer 有 5 个方法:start / stop / getPort / shutDownGracefully(default)/ destroy(default, @since 3.2.0)。
  2. getPort() 返回 -1 表示无端口;server.port=-1 时 autoStart=false。
  3. Tomcat 的 initialize() 通过 Context START_EVENT 的 listener 摘 connector,另有 bindOnInit=false 双保险。
  4. Tomcat 有 startNonDaemonAwaitThread() 因为 "all Tomcat threads are daemon threads";Jetty 不需要。
  5. containerCounter 是 static AtomicInteger,从 -1 开始,第一个实例不改 engine 名。
  6. WebServerApplicationContext.GRACEFUL_SHUTDOWN_PHASE = MAX-1024,START_STOP_LIFECYCLE_PHASE = MAX-2048。
  7. WebServerGracefulShutdownLifecycle.stop() 抛 UnsupportedOperationException。
  8. GracefulShutdownResult 在 Lifecycle 层被丢弃(只用于触发 callback)。
  9. Jetty 的优雅停机依赖 StatisticsHandler 存在 ,而非 Shutdown.GRACEFUL 配置。
  10. PortInUseException.ifPortBindingException 靠消息含 "in use" 区分"端口占用"与"地址无法分配"。
  11. WebServerFactoryCustomizerBeanPostProcessor 用 LambdaSafe 跳过泛型不匹配的定制器。
  12. 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。


(本章完)

相关推荐
鶴哥只手遮天1 小时前
Spring Boot 4.1.1 深度源码解析-第 13 章 · Bean 定义加载:`BeanDefinitionLoader`、组件扫描与 Bean 命名
java·后端·开源
泡茶喝茶写代码1 小时前
A股量化策略回测实战:从行情数据到绩效评估(第 1 篇):回测引擎从零搭建
java·python·股票数据api·股票数据api接口·股票api数据接口·股票量化数据api·股票量化数据接口
鶴哥只手遮天1 小时前
Spring Boot 4.1.1 深度源码解析-第 10 章 · Profile 与多文档:`spring.config.activate`
java·后端·开源
鶴哥只手遮天1 小时前
Spring Boot 4.1.1 深度源码解析-第 06 章 · Environment 构建:PropertySource 次序
java·后端·开源
鶴哥只手遮天1 小时前
Spring Boot 4.1.1 深度源码解析-第 09 章 · `@ConfigurationProperties` 全生命周期:注册、绑定、校验与 AOT
java·后端·开源
Wang's Blog1 小时前
Java 项目实战: 交友项目 - 百度人脸识别:抽取自动装配组件
java·百度·交友
鶴哥只手遮天1 小时前
Spring Boot 4.1.1 深度源码解析-第 08 章 · Binder 绑定引擎:从字符串到 Java 对象的完整路径
java·后端·开源
希洛兔1 小时前
开闭原则详解:对扩展开放,对修改关闭(含图形编辑器反例 + 抽象正例 + JDK/Spring 实战)
java·开闭原则
鶴哥只手遮天1 小时前
Spring Boot 4.1.1 深度源码解析-第 03 章 · 模块化的自动配置:`*.imports`机制、classic 兼容层与 Starter 生成
java·后端·开源