SpringBoot 如何实现 main 方法启动 Web 容器的?

#Java#面试八股

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/FilterDispatcherServletRegistrationBean
WebServer容器抽象,负责 start/stopTomcatWebServer
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 时机"三个关键词,再补优雅停机与容器替换,答案就完整了。

笔记加载中…