SpringBoot 如何实现 main 方法启动 Web 容器的?
面试考察点
- 内嵌容器原理:面试官想知道你是否理解 Spring Boot 为什么不需要外部 Tomcat,而是能把 Web 容器 "嵌入" 到应用进程中,以及这个嵌入过程是怎么发生的。
- 源码追踪能力:能否从
main()→run()→refresh()→onRefresh()一路跟到 Tomcat 启动的那行代码,说出关键的类和方法。 - Servlet 规范理解:是否知道传统的 Servlet 容器和内嵌容器的区别,Spring Boot 是怎么把 DispatcherServlet 注册到内嵌容器中的。
核心答案
先说结论:Spring Boot 通过 内嵌 Web 容器(Embedded Servlet Container)实现从 main() 方法启动 Web 服务。核心原理就三步:
- 引入内嵌容器依赖:
spring-boot-starter-web默认引入了spring-boot-starter-tomcat,里面是嵌入版的 Tomcat(不是那个独立的 Tomcat 服务器) - 在
refresh()的onRefresh()阶段创建并启动容器:ServletWebServerApplicationContext重写了onRefresh(),在这里创建 Tomcat 并调用start() - 将
DispatcherServlet注册到内嵌容器:Spring Boot 通过ServletRegistrationBean把DispatcherServlet注册到内嵌 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);
}
TomcatWebServer 的 start() 方法就是调用 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 | 生态最成熟,默认选择 |
| Jetty | JettyServletWebServerFactory | 轻量级,适合长连接(WebSocket) |
| Undertow | UndertowServletWebServerFactory | 高性能,IO 模型好,内存占用小 |
面试高频追问
-
追问一:Spring Boot 能不能用外部 Tomcat 部署?
- 可以。把打包方式改成
war,让启动类继承SpringBootServletInitializer并重写configure()方法,然后去掉内嵌 Tomcat 依赖就行。不过现在基本没人这么干了,java -jar不香吗?
- 可以。把打包方式改成
-
追问二:内嵌 Tomcat 和独立 Tomcat 性能有区别吗?
- 性能几乎没区别。内嵌 Tomcat 就是同一个 Tomcat 的核心代码,只是以 jar 包形式嵌入到应用进程中,省去了进程间通信的开销。从某种角度来说,内嵌方式反而少了一层反向代理的开销。
-
追问三: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 包才能做到的事。
