Spring Boot 是怎么判断该不该启动 Web 服务器的?WebApplicationType 是什么?
简化版
**Spring Boot 启动时会「自动推断」这个应用是什么类型,从而决定要不要启动 Web 服务器、启动哪种——这就是 WebApplicationType。**它有三种值:① SERVLET(传统的 Servlet Web 应用)——启动内嵌 Tomcat/Jetty/Undertow,创建 AnnotationConfigServletWebServerApplicationContext;② REACTIVE(响应式 Web 应用,WebFlux)——启动内嵌 Netty,创建响应式的 context;③ NONE(非 Web 应用)——不启动任何 Web 服务器,就是个普通程序(如定时任务、消息消费者、批处理),用普通的 context,跑完就退出(或靠非 web 的方式常驻)。怎么推断:Spring Boot 看 classpath 上有哪些类——如果有 WebFlux 相关类且没有 Spring MVC 的核心类,就是 REACTIVE;如果有 Servlet/Spring MVC 相关类,就是 SERVLET;两者都没有,就是 NONE。可以手动指定:new SpringApplication(App.class).setWebApplicationType(WebApplicationType.NONE) 或配置 spring.main.web-application-type=none,强制覆盖自动推断(比如引入了 web 依赖但就想跑个不带 web 的任务)。
详细版
三种 WebApplicationType:
| 类型 | 含义 | 服务器 | ApplicationContext | 判断依据 |
|---|---|---|---|---|
| SERVLET | 传统 Servlet Web | Tomcat/Jetty/Undertow | ServletWebServer…Context | classpath 有 Servlet/Spring MVC |
| REACTIVE | 响应式 Web(WebFlux) | Netty | ReactiveWebServer…Context | classpath 有 WebFlux、无 MVC |
| NONE | 非 Web 应用 | 不启动 | 普通 AnnotationConfig…Context | 两者都没有 |
推断逻辑(简化):
① classpath 有 WebFlux 的 DispatcherHandler
且 没有 Spring MVC 的 DispatcherServlet
且 没有 Jersey → REACTIVE
② classpath 有 Servlet 类 和 Spring MVC 的 ConfigurableWebApplicationContext
→ SERVLET
③ 都不满足 → NONE
// 手动指定(覆盖自动推断)
public static void main(String[] args) {
SpringApplication app = new SpringApplication(MyApp.class);
app.setWebApplicationType(WebApplicationType.NONE); // 强制非 web
app.run(args);
}
// 或用 SpringApplicationBuilder
new SpringApplicationBuilder(MyApp.class)
.web(WebApplicationType.NONE)
.run(args);
// 或配置文件
// spring.main.web-application-type=none
⚠️ 一个经典场景:项目引入了 web 依赖(如
spring-boot-starter-web),但你其实想跑一个「不启动 Web 服务器」的任务(比如一个只做数据迁移、跑完就退出的程序,或一个纯消息消费者)。默认推断会因为 classpath 有 web 依赖而判定为SERVLET、启动 Tomcat 占着端口——这时应显式spring.main.web-application-type=none或setWebApplicationType(NONE)覆盖推断。反过来,同一个应用如果同时引入了spring-boot-starter-web(MVC)和spring-boot-starter-webflux,推断会优先判定为 SERVLET(因为存在 MVC 的 DispatcherServlet),想用 WebFlux 就得显式指定REACTIVE。所以「引了什么依赖」和「最终跑成什么类型」不总是一一对应,推断有优先级、也可被覆盖。
完整版教学
一、为什么需要推断应用类型
先理解「同样是 Spring Boot 应用,为什么要分类型」:
Spring Boot 应用其实有几种形态:
① 传统 Web 应用:接 HTTP 请求,需要启动 Tomcat 等 Web 服务器
② 响应式 Web 应用:用 WebFlux,需要启动 Netty(非阻塞)
③ 非 Web 应用:根本不接 HTTP——
定时任务、消息消费者、批处理、命令行工具
不同形态,启动方式完全不同:
- Web 应用要"启动 Web 服务器 + 用支持 Web 的 ApplicationContext"
- 非 Web 应用"不启动服务器 + 用普通 ApplicationContext"
如果不区分:
非 Web 应用也启动 Tomcat → 白占端口、白耗资源
→ 需要在启动时判断"我到底是哪种",走对应的启动流程
推断应用类型是因为「Spring Boot 应用有几种形态、启动方式不同」——传统 Web(启动 Tomcat)、响应式 Web(启动 Netty)、非 Web(不启动服务器,如定时任务/消息消费者/批处理)。不区分的话,非 Web 应用也启动 Tomcat 就白占端口白耗资源。所以启动时要判断「我是哪种」,走对应流程。理解「Spring Boot 应用分传统 Web/响应式 Web/非 Web 三形态、启动方式不同、需要推断类型走对应流程」,就理解了为什么要推断。
二、三种 WebApplicationType
三种类型各对应一种应用形态:
① SERVLET(传统 Servlet Web 应用):
- 用 Spring MVC,基于 Servlet 规范(阻塞式)
- 启动内嵌 Tomcat/Jetty/Undertow
- context:ServletWebServerApplicationContext
- 最常见(spring-boot-starter-web 引入的就是它)
② REACTIVE(响应式 Web 应用):
- 用 Spring WebFlux,基于 Reactor(非阻塞、响应式)
- 启动内嵌 Netty(默认)
- context:ReactiveWebServerApplicationContext
- spring-boot-starter-webflux 引入
③ NONE(非 Web 应用):
- 不接 HTTP、不启动任何 Web 服务器
- context:普通的 AnnotationConfigApplicationContext
- 用于:定时任务、消息消费者、批处理、CLI 工具
- run() 后如果没有非守护线程常驻,应用会跑完就退出
三种类型:SERVLET(传统 Spring MVC、阻塞式、启动 Tomcat/Jetty/Undertow,最常见)、REACTIVE(Spring WebFlux、非阻塞响应式、启动 Netty)、NONE(不接 HTTP、不启动服务器,用于定时任务/消息消费者/批处理/CLI,run 后无常驻线程就退出)。每种用不同的 ApplicationContext。理解「三类型:SERVLET(MVC/Tomcat/最常见)/REACTIVE(WebFlux/Netty/响应式)/NONE(不启动服务器/任务类应用)、各用不同 context」,就掌握了三种类型。
三、推断逻辑:看 classpath 有什么类
Spring Boot 怎么自动判断类型?靠「检查 classpath 上有哪些关键类」:
推断的核心:类是否存在(ClassUtils.isPresent 检查)
逻辑(简化,按顺序判断):
① 是不是 REACTIVE?
classpath 有 WebFlux 的 DispatcherHandler(org.springframework.web.reactive.)
且 没有 Spring MVC 的 DispatcherServlet
且 没有 Jersey
→ 是 REACTIVE
② 是不是 SERVLET?
classpath 有 Servlet 相关类 和 Spring MVC 的 web 上下文类
→ 是 SERVLET
③ 都不是 → NONE
原理:引入不同的 starter,会带来不同的类
spring-boot-starter-web → 带来 Spring MVC 的 DispatcherServlet → 推断 SERVLET
spring-boot-starter-webflux → 带来 WebFlux 的类(且不带 MVC)→ 推断 REACTIVE
什么 web starter 都没引 → 两类关键类都没有 → 推断 NONE
所以"你引了什么 web 依赖",决定了推断结果
推断靠「检查 classpath 上有哪些关键类」(ClassUtils.isPresent):有 WebFlux 的 DispatcherHandler 且无 MVC 的 DispatcherServlet → REACTIVE;有 Servlet/MVC 的类 → SERVLET;都没有 → NONE。原理是「引入不同 starter 带来不同的类」——starter-web 带来 MVC 的 DispatcherServlet(推断 SERVLET)、starter-webflux 带来 WebFlux 类(推断 REACTIVE)、什么都没引则 NONE。理解「推断靠检查 classpath 关键类:有 WebFlux 无 MVC→REACTIVE、有 MVC→SERVLET、都没有→NONE、引什么 starter 决定推断结果」,就理解了自动推断的机制。
四、SERVLET 优先:MVC 和 WebFlux 都引入时
一个重要细节:同时引入 MVC 和 WebFlux 时的优先级:
如果 classpath 同时有 Spring MVC 和 Spring WebFlux:
(比如项目里既引了 starter-web 又引了 starter-webflux)
→ 推断结果是 SERVLET(不是 REACTIVE)!
原因看推断逻辑:
判断 REACTIVE 的条件之一是"没有 DispatcherServlet"
但同时引了 MVC 就有 DispatcherServlet → REACTIVE 条件不满足
→ 落到 SERVLET
后果:
你以为引了 WebFlux 就是响应式,结果因为也有 MVC,跑成了 SERVLET
WebFlux 的响应式特性没生效(或行为混乱)
解决:
① 想用 WebFlux:别引 starter-web,或显式 setWebApplicationType(REACTIVE)
② 一般不建议同时引两个 web starter(容易混乱)
记忆:MVC 和 WebFlux 共存 → 默认 SERVLET 赢
一个重要细节:同时引入 Spring MVC 和 WebFlux 时,推断结果是 SERVLET(不是 REACTIVE)——因为判断 REACTIVE 的条件包含「没有 DispatcherServlet」,而引了 MVC 就有 DispatcherServlet,条件不满足,落到 SERVLET。后果是「以为引了 WebFlux 就响应式,结果跑成 SERVLET」。解决:想用 WebFlux 就别引 starter-web,或显式指定 REACTIVE。一般不建议同时引两个 web starter。理解「MVC 和 WebFlux 共存时默认 SERVLET 赢(因 REACTIVE 要求没有 DispatcherServlet)、想用 WebFlux 要显式指定或别引 MVC」,就掌握了这个易踩的坑。
五、手动指定:覆盖自动推断
自动推断可以被「手动指定」覆盖,这在几个场景很有用:
覆盖推断的方式:
① 代码:
SpringApplication app = new SpringApplication(App.class);
app.setWebApplicationType(WebApplicationType.NONE);
② Builder:
new SpringApplicationBuilder(App.class).web(WebApplicationType.NONE).run(args);
③ 配置:spring.main.web-application-type=none
典型场景:
场景一:引了 web 依赖,但想跑非 web 任务
比如一个数据迁移工具,依赖里带了 starter-web(传递依赖或复用)
但你就想跑完退出、不启动 Tomcat
→ 显式 none,避免白启动 Web 服务器占端口
场景二:想强制用 REACTIVE
classpath 同时有 MVC 和 WebFlux,默认推断成 SERVLET
→ 显式 reactive
场景三:测试时不想启动 Web 服务器
→ none,让测试更快
所以:自动推断是"默认的便利",需要时可以精确控制
自动推断可被手动指定覆盖:代码 setWebApplicationType、Builder .web(...)、配置 spring.main.web-application-type。典型场景:① 引了 web 依赖但想跑非 web 任务(数据迁移工具带了 web 传递依赖,但想跑完退出不启动 Tomcat,显式 none);② 强制 REACTIVE(MVC+WebFlux 共存默认 SERVLET 时);③ 测试时不启动服务器(none 更快)。理解「手动指定覆盖推断(setWebApplicationType/spring.main.web-application-type)、场景:引了 web 依赖想跑非 web 任务用 none、强制 reactive、测试用 none」,就掌握了覆盖推断的用法。
六、SpringApplication 的其他定制
推断类型只是 SpringApplication 众多可定制项之一,顺带了解其他定制:
SpringApplication 启动前可定制(在 run 之前):
app.setWebApplicationType(...) 设应用类型
app.setBannerMode(Banner.Mode.OFF) 关闭启动横幅
app.setDefaultProperties(map) 设默认属性(最低优先级)
app.setAdditionalProfiles("dev") 追加激活的 profile
app.addListeners(...) 加事件监听器(监听早期事件)
app.addInitializers(...) 加 ApplicationContextInitializer
app.setLogStartupInfo(false) 不打印启动信息
app.setLazyInitialization(true) 全局懒加载(加快启动)
或用 SpringApplicationBuilder(链式,适合复杂/父子容器):
new SpringApplicationBuilder(App.class)
.web(WebApplicationType.NONE)
.bannerMode(Banner.Mode.OFF)
.profiles("dev")
.run(args);
这些定制都在"启动之前"设置,改变 SpringApplication.run() 的行为
setWebApplicationType 只是 SpringApplication 众多定制项之一——启动前(run 之前)还能:setBannerMode(关横幅)、setDefaultProperties(默认属性)、setAdditionalProfiles(追加 profile)、addListeners(监听早期事件,见启动事件题)、setLazyInitialization(全局懒加载加快启动)等,或用 SpringApplicationBuilder 链式定制(适合父子容器)。这些都在启动前设置以改变 run() 行为。理解「SpringApplication 启动前可定制:setWebApplicationType/setBannerMode/setAdditionalProfiles/addListeners/setLazyInitialization、或用 SpringApplicationBuilder 链式」,就把 WebApplicationType 放进了 SpringApplication 定制的整体图景。
记忆钩子:「Spring Boot 启动推断 WebApplicationType 决定要不要启动 Web 服务器:①SERVLET(传统 MVC/阻塞/启动 Tomcat,最常见)②REACTIVE(WebFlux/非阻塞/启动 Netty)③NONE(不启动服务器,定时任务/消息消费/批处理);推断靠检查 classpath 关键类:有 WebFlux 的 DispatcherHandler 且无 MVC 的 DispatcherServlet→REACTIVE、有 MVC→SERVLET、都没有→NONE;★MVC 和 WebFlux 共存默认 SERVLET 赢(REACTIVE 要求没 DispatcherServlet);可手动覆盖 setWebApplicationType/spring.main.web-application-type=none(引了 web 依赖但想跑非 web 任务时用)」。
七、常见误区与追问
- 误区:Spring Boot 应用一定会启动 Web 服务器。 不一定——WebApplicationType 为 NONE 时不启动任何 Web 服务器(用于定时任务、消息消费者、批处理、CLI 工具);是否启动取决于推断结果或手动指定。
- 误区:引入了 spring-boot-starter-web 就没法不启动 Tomcat。 能——显式设 spring.main.web-application-type=none 或 setWebApplicationType(NONE) 覆盖推断,即使 classpath 有 web 依赖也不启动 Web 服务器;适合「引了 web 依赖但想跑非 web 任务」的场景。
- 误区:同时引 MVC 和 WebFlux 会用 WebFlux(响应式)。 默认推断成 SERVLET——因为判断 REACTIVE 的条件包含「没有 DispatcherServlet」,引了 MVC 就有 DispatcherServlet、条件不满足、落到 SERVLET;想用 WebFlux 要显式指定 REACTIVE 或不引 MVC。
- 误区:WebApplicationType 是运行时动态变的。 是启动时(SpringApplication.run 早期)根据 classpath 一次性推断确定的,之后不变;它决定了创建哪种 ApplicationContext、启动哪种服务器。
- 追问:Spring Boot 怎么知道该启动 Tomcat 还是 Netty? 靠 WebApplicationType 推断——检查 classpath:有 Spring MVC(DispatcherServlet)就是 SERVLET,启动 Servlet 容器(Tomcat/Jetty/Undertow);有 WebFlux(DispatcherHandler)且无 MVC 就是 REACTIVE,启动 Netty;据此创建对应的 ApplicationContext 和 Web 服务器。
- 追问:一个纯消息消费者/定时任务应用,WebApplicationType 应该是什么? NONE——它不接 HTTP、不需要 Web 服务器;如果因为传递依赖带了 web starter 被推断成 SERVLET,会白启动 Tomcat 占端口,应显式设 spring.main.web-application-type=none;应用靠消息监听容器/调度线程等非守护线程常驻,不会跑完就退出。
- 追问:除了 WebApplicationType,SpringApplication 启动前还能定制什么? 很多——setBannerMode(横幅)、setAdditionalProfiles(追加 profile)、setDefaultProperties(默认属性)、addListeners(早期事件监听)、addInitializers、setLazyInitialization(全局懒加载加快启动)、setLogStartupInfo 等,都在 run() 前设置,或用 SpringApplicationBuilder 链式配置。
八、加强记忆
Spring Boot 启动时自动推断 WebApplicationType,决定要不要启动 Web 服务器、启动哪种。三种:① SERVLET(传统 Spring MVC、阻塞式、启动内嵌 Tomcat/Jetty/Undertow,用 ServletWebServerApplicationContext,最常见);② REACTIVE(Spring WebFlux、非阻塞响应式、启动内嵌 Netty,用 ReactiveWebServerApplicationContext);③ NONE(不接 HTTP、不启动 Web 服务器,用普通 context,用于定时任务/消息消费者/批处理/CLI)。推断靠检查 classpath 关键类:有 WebFlux 的 DispatcherHandler 且无 MVC 的 DispatcherServlet → REACTIVE;有 Servlet/MVC 类 → SERVLET;都没有 → NONE(引什么 web starter 决定推断结果)。关键细节:同时引 MVC 和 WebFlux 时默认 SERVLET 赢(因为 REACTIVE 要求「没有 DispatcherServlet」)。可手动覆盖:setWebApplicationType(NONE)、spring.main.web-application-type=none——典型场景是「引了 web 依赖但想跑不启动服务器的任务」(数据迁移、纯消息消费者)。WebApplicationType 是 SpringApplication 众多启动前定制项之一(还有 setBannerMode/setAdditionalProfiles/addListeners/setLazyInitialization 等)。一句话「WebApplicationType 决定要不要启动 Web 服务器:SERVLET(MVC/Tomcat/最常见)/REACTIVE(WebFlux/Netty)/NONE(不启动,任务类);靠 classpath 关键类推断(有 WebFlux 无 MVC→REACTIVE、有 MVC→SERVLET、都没有→NONE),MVC+WebFlux 共存默认 SERVLET;可 setWebApplicationType/spring.main.web-application-type=none 覆盖」。