嵌入式 WebServer:端口、Context 与应用生命周期如何接合
可执行 jar 让 Web 容器变成应用的一部分,但没有消除容器边界:端口仍可能冲突,Connector 仍有线程与队列,ServletContext 仍要初始化,应用 ready 与 socket listen 仍不是同一时刻,停机仍要先拒绝新流量再等待在途请求。把嵌入式理解成“无需关心 Tomcat”,只会让这些状态藏进启动日志。
Spring Boot 4.1 的 Servlet 基线为 6.1,官方嵌入式主线包括 Tomcat 11 与 Jetty 12.1,具体组合以 System Requirements 为准。服务器定制入口见 Embedded Web Servers,Servlet 请求分派细节仍属于前面的 Servlet 家族与后续 MVC 家族。
WebApplicationType 决定是否创建 WebServer 上下文
SpringApplication 根据 classpath 和显式配置选择 SERVLET、REACTIVE 或 NONE。Servlet 模式创建 ServletWebServerApplicationContext,refresh 期间寻找一个 ServletWebServerFactory,用它创建 WebServer,再把 Servlet、Filter、Listener 与 context 关联。普通 ApplicationContext 没有这条服务器创建链。
多个 WebServerFactory 候选会产生歧义;没有候选则无法启动 Servlet 应用。排除 starter、同时引入多个容器或用户声明自定义 factory 都会改变选择。启动证据应记录 factory bean name、server implementation、绑定地址/端口、协议与实际 connector 数量。
ServletContextInitializer 是嵌入式模式下编程注册 Web 组件的统一入口,Boot 会收集、排序并在服务器启动时调用。ServletRegistrationBean、FilterRegistrationBean 等封装映射、顺序和 dispatcher types。初始化器里执行远程 I/O 会阻塞整个端口启动;注册同名组件或冲突映射应在启动期失败。
外置容器部署使用 Servlet 容器创建应用,启动入口与 classloader 所有权不同。传统 WAR 需要正确的 initializer 和 provided 容器依赖。不要同时假设“应用拥有服务器”和“服务器拥有应用”;线程、临时目录、JNDI 与关闭责任会因此不同。
端口绑定是启动中的资源获取动作
server.port、server.address、SSL、HTTP/2 和 connector 定制最终成为底层 server 配置。端口 0 可让操作系统选择随机空闲端口,适合集成测试;启动后通过 WebServerInitializedEvent 或 local server port 获取实际值。先自行找空闲端口再启动存在 TOCTOU 竞争,另一个进程可能在间隙占用。
WebServerLifecycleDemo.java 展示绑定、ready、drain 与 stop 的顺序:
javac --release 17 -Xlint:all -Werror examples/backend-development/spring-boot/embedded-webserver/WebServerLifecycleDemo.java examples/backend-development/spring-boot/embedded-webserver/PortBindingFailureDemo.java
java -cp examples/backend-development/spring-boot/embedded-webserver WebServerLifecycleDemostates=[factory-selected, server-created, port-bound:8080, context-ready, accepting-traffic, draining, stopped] boundBeforeReady=true stoppedAfterDrain=true端口监听早于 ready 并不代表请求一定会正常处理。平台必须以 readiness 控制流量,而不是只做 TCP connect。相反,管理端口与业务端口分离时,管理端口健康不能证明主 connector 可接收请求;关键探针最好在主服务器路径也有入口。
绑定失败通常包装为 WebServerException/PortInUseException,最深 cause 可能是 BindException、权限或地址不可用。反向实验固定“失败不能进入 ready”:
java -cp examples/backend-development/spring-boot/embedded-webserver PortBindingFailureDemorequestedPort=8080 bindRejected=true contextReady=false低端口权限、IPv4/IPv6 地址、容器 hostPort、同 Pod sidecar 和测试并发都可能造成不同冲突。排查要同时查看进程监听、编排端口映射和实际 server.address,不要只改成另一个固定端口掩盖所有权错误。
Factory Customizer 应保持组合性
WebServerFactoryCustomizer<T> 在 factory 创建 server 前修改配置,适合组织级 connector、压缩、线程、访问日志与错误页默认。多个 customizer 会按顺序叠加;直接替换 factory 可能绕过 Boot 绑定与其他 customizer。每个定制应说明覆盖哪些属性、用户如何退出、顺序冲突如何处理。
容器专属能力需要绑定专属 factory 类型。若公共 Starter 无条件依赖 Tomcat API,切换 Jetty 时类加载就失败。通用配置留在 ServerProperties/标准 abstraction,容器优化放在有 OnClass 条件的独立自动配置,并分别测试。
线程、accept backlog、最大连接、keep-alive、header/body 限制和临时目录都影响容量与安全。属性名字在容器之间不一,数值也不能机械对齐。容量预算应从到达率、服务时间和资源上限推导,再映射到底层实现;“max threads=200”不代表系统就能安全承载 200 个慢请求。
SSL bundle、证书轮换和多 connector 会扩大生命周期。证书加载失败应阻止 server ready;热更新必须证明新连接使用新材料、旧连接如何结束。管理端口使用另一地址时要配置网络与鉴权,不能因“内部端口”就暴露 env、heapdump 等敏感端点。
Context refresh 与 WebServer 启动相互约束
ServletWebServerApplicationContext 在 refresh 中创建 server,但普通 singleton、生命周期 Bean 和事件仍沿 Spring Context 运行。WebServerInitializedEvent 表示 server 已初始化并可取得端口,不表示 Runner 完成或 readiness accepting。Context refresh 失败时必须停止已经创建的 server,释放端口与线程。
server 初始化器可能需要容器 Bean,Bean 又可能等待 server 端口,形成启动环。获取随机端口的业务客户端应在 WebServerInitializedEvent 之后配置,或由测试框架注入;不要在普通 Bean 构造器中读取尚未确定的 local port。
父子 context 或独立 management context 可能创建多个 server。事件监听器要检查 ApplicationContext identity,避免把管理端口覆盖成业务端口。指标和日志应为 connector 增加 server/port/protocol 标签,但控制基数,不能把动态地址完整展开成无限 tag。
优雅停机是流量状态与请求终止的协议
收到关闭请求时,应用先把 readiness 切到 REFUSING_TRAFFIC,服务器拒绝或不再接收新请求,并在 grace period 内等待活动请求完成。超时后强制终止。不同服务器对 keep-alive、新连接和请求的处理实现不同,必须在目标容器上做真实测试。
长轮询、SSE、WebSocket、异步 Servlet 和大文件传输可能超过 grace period。应用要传播取消,客户端要有重连与幂等;不能用无限停机等待保护单个连接。线程池、数据库连接与消息消费者的停止顺序也要协调,避免 Web 请求仍在执行而下游资源已经关闭。
编排平台的 termination grace period 必须大于应用 shutdown timeout 加摘流量传播余量。preStop 若只 sleep 会消耗预算且无法证明流量已摘除;以 readiness 状态、入口连接数和在途请求数作为证据更可靠。强杀路径仍可能发生,持久一致性不能依赖 destroy 回调。
服务器验收必须穿透到 socket 与在途请求
集成测试应使用随机端口,等待 ready 后发真实 HTTP 请求,验证绑定地址、Header/Body 限制和错误路径;再触发 context close,验证 readiness 先拒绝、旧请求在预算内完成、新请求失败、端口释放、容器线程退出。仅使用 MockMvc 不会创建真实 WebServer,不能覆盖这些属性。
生产指标至少包括 connector 当前/最大连接、accept queue、工作线程、请求 active、长请求年龄、拒绝、错误、graceful shutdown remaining 和实际停止时间。把服务器指标与应用线程池、数据库池和延迟分位关联,才能分辨入口饱和与下游等待。
嵌入式服务器把部署单元简化成一个进程,却把服务器版本、配置和生命周期纳入应用责任。明确谁选择 factory、何时绑定端口、何时才 ready、关闭时如何拒绝和排空,才算真正拥有这个“内嵌”容器。
