嵌入式Tomcat启动全流程:从main()到第一个HTTP请求的完整路径
嵌入式Tomcat启动全流程:从main()到第一个HTTP请求的完整路径
适读人群:Spring Boot后端开发者,希望理解Web服务器启动机制的Java工程师 | 阅读时长:约18分钟
开篇故事
很多人用Spring Boot,都觉得嵌入式Tomcat是理所当然的事情。引入spring-boot-starter-web,写个main方法,服务就起来了,HTTP请求就能接收了。
但有一次我需要做一个需求:在服务启动时,等所有Bean初始化完毕、缓存预热完成之后,才开始接收外部HTTP请求。否则缓存还没预热,大量请求打进来,可能把数据库打崩。
我去翻源码,才弄明白Tomcat是在refresh()的哪个阶段启动的,以及有什么钩子可以让我延迟开放端口。这个排查过程让我把嵌入式Tomcat的启动流程完整地梳理了一遍。
一、嵌入式Tomcat的核心设计
传统的Tomcat是一个独立进程,Spring应用部署为WAR包运行其中。嵌入式Tomcat的思路是反过来的:Tomcat被作为一个Java对象嵌入到Spring应用进程里,作为一个Bean来管理。
核心类:
TomcatServletWebServerFactory:创建Tomcat Web服务器的工厂TomcatWebServer:Tomcat实例的包装器,管理启动/停止WebServerStartStopLifecycle:把Tomcat的生命周期接入Spring的Lifecycle体系
二、源码核心路径解析
2.1 从main()到Tomcat启动的完整时序
2.2 createWebServer的源码
// ServletWebServerApplicationContext.java 第155行(简化)
@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) {
StartupStep createWebServer = getApplicationStartup()
.start("spring.boot.webserver.create");
// 从BeanFactory获取WebServerFactory(自动装配的TomcatServletWebServerFactory)
ServletWebServerFactory factory = getWebServerFactory();
createWebServer.tag("factory", factory.getClass().toString());
// 创建Web服务器(这里Tomcat已经start,但还没接受请求)
this.webServer = factory.getWebServer(getSelfInitializer());
createWebServer.end();
// 注册关闭钩子(优雅停机)
getBeanFactory().registerSingleton("webServerGracefulShutdown",
new WebServerGracefulShutdownLifecycle(this.webServer));
getBeanFactory().registerSingleton("webServerStartStop",
new WebServerStartStopLifecycle(this, this.webServer));
}
// ...
}2.3 TomcatServletWebServerFactory.getWebServer
// TomcatServletWebServerFactory.java 第118行(简化)
@Override
public WebServer getWebServer(ServletContextInitializer... initializers) {
if (this.disableMBeanRegistry) {
Registry.disableRegistry();
}
Tomcat tomcat = new Tomcat();
// 配置基础目录(临时目录)
File baseDir = (this.baseDirectory != null ? this.baseDirectory :
createTempDir("tomcat"));
tomcat.setBaseDir(baseDir.getAbsolutePath());
// 配置Connector(HTTP/1.1,绑定端口)
for (LifecycleListener listener : this.serverLifecycleListeners) {
tomcat.getServer().addLifecycleListener(listener);
}
Connector connector = new Connector(this.protocol);
connector.setThrowOnFailure(true);
tomcat.getService().addConnector(connector);
customizeConnector(connector);
tomcat.setConnector(connector);
// 配置Host和Context
tomcat.getHost().setAutoDeploy(false);
configureEngine(tomcat.getEngine());
for (Connector additionalConnector : this.additionalTomcatConnectors) {
tomcat.getService().addConnector(additionalConnector);
}
prepareContext(tomcat.getHost(), initializers);
// 创建TomcatWebServer包装器,内部调用tomcat.start()
return getTomcatWebServer(tomcat);
}2.4 getSelfInitializer:注册DispatcherServlet
getSelfInitializer()返回一个ServletContextInitializer,它在Tomcat的Context初始化时执行,负责把DispatcherServlet注册到Tomcat:
// ServletWebServerApplicationContext.java
private org.springframework.boot.web.servlet.ServletContextInitializer getSelfInitializer() {
return this::selfInitialize;
}
private void selfInitialize(ServletContext servletContext) throws ServletException {
prepareWebApplicationContext(servletContext);
registerApplicationScope(servletContext);
WebApplicationContextUtils.registerEnvironmentBeans(getBeanFactory(), servletContext);
// 从BeanFactory获取所有ServletContextInitializer(包括DispatcherServletRegistrationBean)
for (ServletContextInitializer beans : getServletContextInitializerBeans()) {
beans.onStartup(servletContext); // 注册Servlet/Filter/Listener
}
}DispatcherServletRegistrationBean.onStartup()会调用servletContext.addServlet("dispatcherServlet", this.servlet),把DispatcherServlet注册到Tomcat的Context中。
2.5 两阶段启动:create vs start
这是我最初要解决那个问题的关键:
重要:Tomcat的端口绑定和Connector启动发生在onRefresh()(Step 8的早期),但真正开始处理HTTP请求是在finishRefresh()通过WebServerStartStopLifecycle.start()触发的。
在此之间,缓存预热(如果放在@PostConstruct或SmartInitializingSingleton.afterSingletonsInstantiated()里)会在Tomcat开始接受请求之前完成。
三、完整代码示例
3.1 自定义嵌入式Tomcat配置
@Configuration(proxyBeanMethods = false)
public class TomcatCustomizerConfig {
// 方式一:通过WebServerFactoryCustomizer自定义
@Bean
public WebServerFactoryCustomizer<TomcatServletWebServerFactory> tomcatCustomizer() {
return factory -> {
// 连接超时
factory.addConnectorCustomizers(connector -> {
connector.setProperty("connectionTimeout", "20000");
connector.setProperty("maxKeepAliveRequests", "100");
});
// 调整线程池
factory.addConnectorCustomizers(connector -> {
ProtocolHandler handler = connector.getProtocolHandler();
if (handler instanceof AbstractProtocol<?> protocol) {
protocol.setMaxThreads(200);
protocol.setMinSpareThreads(10);
protocol.setAcceptCount(100);
}
});
// 配置访问日志
AccessLogValve accessLog = new AccessLogValve();
accessLog.setDirectory("logs");
accessLog.setPattern("%h %l %u %t \"%r\" %s %b %D");
accessLog.setEnabled(true);
factory.addContextValves(accessLog);
};
}
// 方式二:直接提供TomcatServletWebServerFactory Bean
@Bean
@ConditionalOnMissingBean(value = TomcatServletWebServerFactory.class,
search = SearchStrategy.CURRENT)
public TomcatServletWebServerFactory customTomcatFactory() {
TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory();
factory.setPort(8080);
factory.setContextPath("/api");
// 配置SSL
Ssl ssl = new Ssl();
ssl.setEnabled(false);
factory.setSsl(ssl);
return factory;
}
}3.2 缓存预热后再开放请求的方案
// 方案:利用SmartLifecycle控制Tomcat开始接受请求的时机
@Component
public class CacheWarmupLifecycle implements SmartLifecycle {
private static final Logger log = LoggerFactory.getLogger(CacheWarmupLifecycle.class);
@Autowired
private CacheService cacheService;
@Autowired
private WebServer webServer; // 注入TomcatWebServer
private volatile boolean running = false;
@Override
public void start() {
log.info("Starting cache warmup before accepting HTTP requests...");
try {
cacheService.warmUpAll();
log.info("Cache warmup completed, starting to accept requests");
} catch (Exception e) {
log.error("Cache warmup failed, but starting server anyway", e);
}
running = true;
}
@Override
public void stop() {
running = false;
}
@Override
public boolean isRunning() {
return running;
}
@Override
public int getPhase() {
// WebServerStartStopLifecycle的phase是Integer.MAX_VALUE - 1
// 我们要在Tomcat开始接受请求之前执行,phase要更小
return Integer.MAX_VALUE - 2;
}
@Override
public boolean isAutoStartup() {
return true;
}
}3.3 优雅停机实现
// Spring Boot 2.3+ 内置优雅停机,只需配置:
# application.yml
server:
shutdown: graceful # 优雅停机(等待进行中的请求完成)
spring:
lifecycle:
timeout-per-shutdown-phase: 30s # 最大等待时间
# 收到SIGTERM信号后:
# 1. Tomcat停止接受新请求
# 2. 等待已有请求处理完毕(最多30s)
# 3. Spring Context关闭
# 4. 进程退出
// 如果需要更细粒度控制,监听ContextClosedEvent
@Component
public class GracefulShutdownHandler implements ApplicationListener<ContextClosedEvent> {
@Autowired
private ActiveRequestTracker requestTracker;
@Override
public void onApplicationEvent(ContextClosedEvent event) {
log.info("Application shutting down, waiting for {} active requests...",
requestTracker.getActiveCount());
int attempts = 0;
while (requestTracker.getActiveCount() > 0 && attempts < 60) {
try {
Thread.sleep(500);
attempts++;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
log.info("Shutdown complete");
}
}四、踩坑实录
坑1:Tomcat端口已绑定但请求返回503
现象:应用启动时端口就绑定了,用curl打过去返回503 Service Unavailable。
根因:onRefresh()阶段Tomcat就绑定端口了,但DispatcherServlet还没有初始化完成(等finishRefresh()之后才完全就绪)。如果在这个窗口期发请求,Tomcat能接收连接,但Servlet容器还没准备好,所以返回503。
通常应用启动时间很短,这个窗口期几乎不可感知。但如果你的Bean初始化很慢(比如预加载大量数据),这个窗口期可能长达几秒甚至几十秒,就会出现"应用在启动中但已经有请求进来"的问题。
解决方案:使用Spring Boot内置的spring.lifecycle.timeout-per-shutdown-phase或者Readiness探针来控制流量切入时机。
坑2:自定义Connector导致HTTPS不工作
现象:配置了server.ssl.enabled=true,但HTTPS请求还是报连接拒绝。
根因:如果你自定义了TomcatServletWebServerFactory Bean并直接new Connector(),会绕过Spring Boot的SSL自动配置逻辑(TomcatServletWebServerFactoryCustomizer)。
正确做法:通过WebServerFactoryCustomizer来调整,而不是完全替换Factory。
坑3:嵌入式Tomcat的临时目录权限
现象:Docker容器里启动失败,报Unable to create Tomcat temp directory。
根因:嵌入式Tomcat默认在系统临时目录(java.io.tmpdir)下创建工作目录。某些容器环境下权限受限。
解决:
server:
tomcat:
basedir: /app/tomcat # 指定工作目录或者:
java -Djava.io.tmpdir=/app/tmp -jar app.jar坑4:多Connector支持HTTP和HTTPS同时开放
很多项目需要同时支持HTTP和HTTPS:
@Bean
public WebServerFactoryCustomizer<TomcatServletWebServerFactory> httpsCustomizer() {
return factory -> {
// 主Connector是HTTPS(配置在application.yml的server.ssl里)
// 额外添加一个HTTP Connector用于健康检查和内部调用
Connector httpConnector = new Connector(TomcatServletWebServerFactory.DEFAULT_PROTOCOL);
httpConnector.setPort(8090); // 不同端口
httpConnector.setSecure(false);
httpConnector.setScheme("http");
factory.addAdditionalTomcatConnectors(httpConnector);
};
}五、总结与延伸
嵌入式Tomcat的核心设计:
TomcatServletWebServerFactory(工厂)
↓ 创建
TomcatWebServer(包装器)
↓ 包含
Tomcat(Apache Catalina实例)
↓ 包含
Connector(端口监听)+ Context(Web应用)
↓ 通过
DispatcherServletRegistrationBean 注册了 DispatcherServlet两阶段启动保证了:端口绑定(onRefresh)→ 所有Bean初始化 → 开始接受请求(finishRefresh),这中间有一个完整的初始化窗口。
下一篇聊@Async异步方法的6个常见坑,这是很多生产事故的来源。
