SpringBoot 如何实现 main 方法启动 Web 容器的?
结论先行:不靠外部容器,靠内嵌容器 + 编程式启动。SpringApplication.run 根据 classpath 推断 WebApplicationType.SERVLET,创建 ServletWebServerApplicationContext;该上下文在 refresh() 的 onRefresh() 阶段调用 createWebServer(),由 ServletWebServerFactory(默认 TomcatServletWebServerFactory)new 出 Tomcat、注册 DispatcherServlet、启动 Connector,于是 main 方法一跑,端口就监听了——整个过程没有 web.xml,也没有 server.xml。
一、调用链
main()
└─ SpringApplication.run()
├─ WebApplicationType.deduceFromClasspath() ← 有 DispatcherServlet 且无 WebFlux → SERVLET
├─ createApplicationContext() → AnnotationConfigServletWebServerApplicationContext
└─ refresh()
└─ ServletWebServerApplicationContext#onRefresh()
└─ createWebServer()
├─ getWebServerFactory() ← 从容器中找唯一 ServletWebServerFactory
├─ factory.getWebServer(factory -> {
│ factory.addContextCustomizers(...) // 注册 DispatcherServlet、Filter
│ })
└─ TomcatServletWebServerFactory#getWebServer()
├─ new Tomcat()、设置 baseDir
├─ addWebapp(path, docBase) → TomcatEmbeddedContext
└─ tomcat.start()
└─ finishRefresh() → WebServerStartStopLifecycle 完成端口绑定、Connector 启动
二、核心组件分工
| 组件 | 职责 | 默认实现 |
|---|---|---|
WebApplicationType | 判定应用类型(SERVLET/REACTIVE/NONE) | 依据 classpath 推断 |
ServletWebServerApplicationContext | 持有并启动 Web 容器 | Boot 内置 |
ServletWebServerFactory | 创建具体容器实例 | TomcatServletWebServerFactory |
ServletContextInitializer | 往容器注册 Servlet/Filter | DispatcherServletRegistrationBean |
WebServer | 容器抽象,负责 start/stop | TomcatWebServer |
WebServerFactoryCustomizer | 定制端口、线程数、连接数 | ServerProperties 驱动 |
三、代码:从零手写等同效果(理解用)
// 不用 Spring Boot,也能用 main 启动内嵌 Tomcat——这正是 Boot 内部做的事
public class ManualTomcat {
public static void main(String[] args) throws Exception {
Tomcat tomcat = new Tomcat();
tomcat.setPort(8080);
tomcat.getConnector(); // 必须调用,才会创建 Connector
Context ctx = tomcat.addContext("", new File(".").getAbsolutePath());
Tomcat.addServlet(ctx, "dispatcher", new DispatcherServlet());
ctx.addServletMappingDecoded("/*", "dispatcher");
tomcat.start();
System.out.println("已监听 8080");
tomcat.getServer().await();
}
}
// Boot 场景下更常见的定制方式:改端口/线程数,或替换容器
@Component
public class TomcatCustomizer implements WebServerFactoryCustomizer<TomcatServletWebServerFactory> {
@Override
public void customize(TomcatServletWebServerFactory factory) {
factory.addConnectorCustomizers(connector -> {
connector.setProperty("maxThreads", "400");
connector.setProperty("acceptCount", "200");
});
}
}
server:
port: 8080
shutdown: graceful # 优雅停机,配合 spring.lifecycle.timeout-per-shutdown-phase
tomcat:
threads:
max: 400
min-spare: 20
accept-count: 200
max-connections: 8192
spring:
lifecycle:
timeout-per-shutdown-phase: 30s
四、为什么不需要 web.xml
Servlet 3.0 起支持 ServletContainerInitializer(SPI 机制)。Boot 通过 TomcatStarter(实现 ServletContainerInitializer)把容器里所有 ServletContextInitializer 回调出来,Spring MVC 的 DispatcherServlet、CharacterEncodingFilter 等都被注册为这类初始化器。于是"配置"从 XML 变成了代码,容器启动时由 SPI 触发,等价于传统 web.xml 的声明。
五、常见追问
- 问:怎么换成 Jetty 或 Undertow? 排除
spring-boot-starter-tomcat,加入spring-boot-starter-jetty/spring-boot-starter-undertow,Boot 会按 classpath 选对应ServletWebServerFactory。 - 问:端口为 0 是什么意思? 随机端口,启动后用
ServletWebServerApplicationContext#getWebServer().getPort()取实际端口,测试常用。 - 问:
server.port什么时候生效? 在getWebServer()时通过ServerProperties读取,因此改端口不需要重新编译,但必须在容器启动前确定。 - 问:优雅停机怎么实现的?
server.shutdown=graceful注册WebServerGracefulShutdownLifecycle,先停止接收新请求、等待在途请求完成(超时受timeout-per-shutdown-phase限制)再关闭容器。 - 问:打成 war 包还能启动吗? 能,继承
SpringBootServletInitializer并重写configure,由外部容器调用,此时内嵌容器不会启动。
小结
主线只有一句:main → SpringApplication → ServletWebServerApplicationContext.onRefresh → ServletWebServerFactory 创建并 start 容器。抓住"内嵌容器 + Servlet 3.0 SPI + onRefresh 时机"三个关键词,再补优雅停机与容器替换,答案就完整了。