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


面试考察点

  1. 内嵌容器原理:面试官想知道你是否理解 Spring Boot 为什么不需要外部 Tomcat,而是能把 Web 容器 "嵌入" 到应用进程中,以及这个嵌入过程是怎么发生的。
  2. 源码追踪能力:能否从 main()run()refresh()onRefresh() 一路跟到 Tomcat 启动的那行代码,说出关键的类和方法。
  3. Servlet 规范理解:是否知道传统的 Servlet 容器和内嵌容器的区别,Spring Boot 是怎么把 DispatcherServlet 注册到内嵌容器中的。

核心答案

先说结论:Spring Boot 通过 内嵌 Web 容器(Embedded Servlet Container)实现从 main() 方法启动 Web 服务。核心原理就三步:

  1. 引入内嵌容器依赖spring-boot-starter-web 默认引入了 spring-boot-starter-tomcat,里面是嵌入版的 Tomcat(不是那个独立的 Tomcat 服务器)
  2. refresh()onRefresh() 阶段创建并启动容器ServletWebServerApplicationContext 重写了 onRefresh(),在这里创建 Tomcat 并调用 start()
  3. DispatcherServlet 注册到内嵌容器:Spring Boot 通过 ServletRegistrationBeanDispatcherServlet 注册到内嵌 Tomcat 中,后续的请求处理和传统 Spring MVC 完全一致

深度解析

一、传统方式 vs 内嵌方式

先搞清楚传统部署和 Spring Boot 内嵌部署的本质区别:

上图对比了两种部署方式的核心差异:

  • 传统方式:Tomcat 是独立进程,Spring 应用被打成 war 包部署到 Tomcat 中。Tomcat 负责管理 Servlet 生命周期,Spring 是 "被管理" 的。启动顺序是 Tomcat → Spring。
  • Spring Boot 方式:整个过程反过来了。Java 进程是主体,Spring IoC 容器先启动,然后在容器内部创建并启动内嵌的 Tomcat。启动顺序是 main() → Spring → Tomcat。

这就是为什么 Spring Boot 不需要装 Tomcat、不需要打 war 包、不需要写 web.xml,直接 java -jar 就能跑起来。

二、从 main() 到 Tomcat 启动的源码链路

这是面试官最想听的部分。从 main() 到 Tomcat 启动,调用链如下:

main()
 └── SpringApplication.run()
      └── new SpringApplication()
           └── run(context)                         // 核心启动方法
                ├── createApplicationContext()       // 创建上下文
                │    └── 创建 ServletWebServerApplicationContext
                │
                ├── prepareContext()                 // 准备上下文
                │
                └── refreshContext()                 // 刷新容器(核心!)
                     └── ServletWebServerApplicationContext.refresh()
                          └── onRefresh()            // 重写了这个方法!
                               └── createWebServer() // 创建 Web 服务器
                                    │
                                    ├── 从容器中获取 WebServerFactory
                                    │   (默认是 TomcatServletWebServerFactory)
                                    │
                                    ├── factory.getWebServer()
                                    │   └── new Tomcat()
                                    │   └── tomcat.start()
                                    │
                                    └── 将 DispatcherServlet 注册到 Tomcat

关键就在于 ServletWebServerApplicationContext 这个类。它是 AnnotationConfigServletWebServerApplicationContext 的父类,专门为内嵌 Web 容器设计的 ApplicationContext

三、onRefresh() 里到底做了什么?

// ServletWebServerApplicationContext.java
@Override
protected void onRefresh() {
    super.onRefresh();
    try {
        createWebServer();  // 核心:创建内嵌 Web 服务器
    } 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) {
        // 1. 从 Spring 容器中获取 WebServerFactory
        //    默认是 TomcatServletWebServerFactory
        ServletWebServerFactory factory = getWebServerFactory();

        // 2. 获取所有 Servlet、Filter、Listener
        //    包括 DispatcherServlet
        // 3. 用工厂创建 WebServer(内嵌 Tomcat)
        this.webServer = factory.getWebServer(
            getSelfInitializer());
    }
    // ...
}

TomcatServletWebServerFactory.getWebServer() 做的事:

// TomcatServletWebServerFactory.java(简化版)
@Override
public WebServer getWebServer(
        ServletContextInitializer... initializers) {

    // 1. 创建 Tomcat 实例(不是独立服务器的 Tomcat,是嵌入版的)
    Tomcat tomcat = new Tomcat();

    // 2. 设置连接器(Connector,监听端口)
    Connector connector = new Connector(
        this.protocol);
    connector.setPort(this.port);  // 默认 8080
    tomcat.getService().addConnector(connector);

    // 3. 配置 Host 和 Context
    Host host = tomcat.getHost();
    Context context = tomcat.addContext("", "");

    // 4. 注册 DispatcherServlet
    //    通过 ServletContainerInitializer 的方式
    //    把 DispatcherServlet 加到 Tomcat 的 Context 中

    // 5. 返回 TomcatWebServer 包装对象
    return getTomcatWebServer(tomcat);
}

TomcatWebServerstart() 方法就是调用 tomcat.start(),至此内嵌 Tomcat 就跑起来了,开始监听端口。

四、DispatcherServlet 是怎么注册进去的?

传统 Spring MVC 需要在 web.xml 里配 DispatcherServlet,Spring Boot 是自动完成的:

// DispatcherServletAutoConfiguration.java(简化版)
@AutoConfiguration
@ConditionalOnWebApplication
public class DispatcherServletAutoConfiguration {

    @Bean
    public DispatcherServlet dispatcherServlet() {
        DispatcherServlet servlet = new DispatcherServlet();
        // 自动配置好各种属性
        return servlet;
    }

    // 关键:把 DispatcherServlet 注册到内嵌容器
    @Bean
    public DispatcherServletRegistrationBean
            dispatcherServletRegistration(
                DispatcherServlet dispatcherServlet) {

        DispatcherServletRegistrationBean registration =
            new DispatcherServletRegistrationBean(
                dispatcherServlet, "/*");  // 拦截所有请求
        registration.setName("dispatcherServlet");
        return registration;
    }
}

DispatcherServletRegistrationBean 实现了 ServletContextInitializer 接口,createWebServer() 时会被回调,从而把 DispatcherServlet 注册到内嵌 Tomcat 中。等价于传统 web.xml 中的:

<servlet>
    <servlet-name>dispatcherServlet</servlet-name>
    <servlet-class>
        org.springframework.web.servlet.DispatcherServlet
    </servlet-class>
</servlet>
<servlet-mapping>
    <servlet-name>dispatcherServlet</servlet-name>
    <url-pattern>/*</url-pattern>
</servlet-mapping>

只不过 Spring Boot 用自动配置 + Java 代码替代了 XML。

五、如何切换 Web 容器?

Spring Boot 默认用 Tomcat,但切换成 Jetty 或 Undertow 非常简单——换依赖就行:

<!-- 默认:Tomcat -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- 切换为 Undertow:排除 Tomcat,引入 Undertow -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <exclusions>
        <exclusion>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-undertow</artifactId>
</dependency>

为什么换依赖就能切换?因为 Tomcat、Jetty、Undertow 都实现了统一的 WebServerFactory 接口,createWebServer() 里用的是接口,具体实现类通过自动配置注入。经典的多态 + 工厂模式。

容器工厂类特点
Tomcat(默认)TomcatServletWebServerFactory生态最成熟,默认选择
JettyJettyServletWebServerFactory轻量级,适合长连接(WebSocket)
UndertowUndertowServletWebServerFactory高性能,IO 模型好,内存占用小

面试高频追问

  1. 追问一:Spring Boot 能不能用外部 Tomcat 部署?

    • 可以。把打包方式改成 war,让启动类继承 SpringBootServletInitializer 并重写 configure() 方法,然后去掉内嵌 Tomcat 依赖就行。不过现在基本没人这么干了,java -jar 不香吗?
  2. 追问二:内嵌 Tomcat 和独立 Tomcat 性能有区别吗?

    • 性能几乎没区别。内嵌 Tomcat 就是同一个 Tomcat 的核心代码,只是以 jar 包形式嵌入到应用进程中,省去了进程间通信的开销。从某种角度来说,内嵌方式反而少了一层反向代理的开销。
  3. 追问三:Tomcat 是在所有 Bean 创建之前还是之后启动的?

    • 之前onRefresh()refresh() 的第 9 步执行,而 Bean 的实例化在第 11 步 finishBeanFactoryInitialization()。所以 Tomcat 启动时,业务 Bean 还没创建。不过 DispatcherServlet 本身是在 onRefresh() 之前就已经注册到容器中的(通过自动配置),所以不影响请求处理。

常见面试变体

  • "Spring Boot 内嵌 Tomcat 的原理是什么?"
  • "为什么 Spring Boot 不需要外部 Tomcat?"
  • "Spring Boot 如何切换 Web 容器?"
  • "ServletWebServerApplicationContext 的作用是什么?"

总结

一句话:Spring Boot 通过 ServletWebServerApplicationContext 重写 onRefresh() 方法,在 IoC 容器刷新过程中创建并启动内嵌 Web 容器(默认 Tomcat),再通过自动配置把 DispatcherServlet 注册进去。整个过程对开发者完全透明,一个 main() 方法就搞定了传统方式需要 Tomcat + web.xml + war 包才能做到的事。