应用类型推断与 WebApplicationType 探测
概述
Spring Boot 在启动时第一件事就是推断当前应用的类型——是 Servlet Web(传统 Tomcat)、Reactive Web(WebFlux + Netty),还是非 Web 应用。这一推断结果直接影响后续创建什么类型的 ApplicationContext、加载什么自动配置。
本文逐层拆解 SpringApplication.deduceWebApplicationType() 的完整实现,覆盖 classpath 探测、枚举优先级、版本差异等细小实现细节。
本文基于 Spring Boot 3.x 源码分析,关键部分对比 Spring Boot 2.x 差异。
1. WebApplicationType 枚举定义
// org.springframework.boot.WebApplicationType
public enum WebApplicationType {
/** 非 Web 应用——不需要内嵌 Web 服务器 */
NONE,
/** Servlet Web 应用——需要 ServletWebServerFactory(Tomcat / Jetty / Undertow) */
SERVLET,
/** Reactive Web 应用——需要 ReactiveWebServerFactory(Netty / Jetty / Undertow) */
REACTIVE
}三个枚举值对应三种完全不同的容器启动路径:
| 类型 | ApplicationContext 实现 | 内嵌服务器工厂 | 典型场景 |
|---|---|---|---|
NONE | AnnotationConfigApplicationContext | 无 | 纯 Job / 批处理 / 测试 |
SERVLET | AnnotationConfigServletWebServerApplicationContext | TomcatServletWebServerFactory / JettyServletWebServerFactory / UndertowServletWebServerFactory | 传统 MVC 应用 |
REACTIVE | AnnotationConfigReactiveWebServerApplicationContext | NettyReactiveWebServerFactory / JettyReactiveWebServerFactory / UndertowReactiveWebServerFactory | WebFlux 应用 |
2. deduceWebApplicationType() 核心实现
// org.springframework.boot.WebApplicationType
static WebApplicationType deduceFromClasspath() {
// 第一步:探测 Reactive 类
if (ClassUtils.isPresent("org.springframework.web.reactive.DispatcherHandler", null)
&& !ClassUtils.isPresent("org.springframework.web.servlet.DispatcherServlet", null)
&& !ClassUtils.isPresent("org.glassfish.jersey.servlet.ServletContainer", null)) {
return WebApplicationType.REACTIVE;
}
// 第二步:探测 Servlet 类
for (String className : SERVLET_INDICATOR_CLASSES) {
if (ClassUtils.isPresent(className, null)) {
return WebApplicationType.SERVLET;
}
}
// 第三步:都不是 → NONE
return WebApplicationType.NONE;
}2.1 执行流程分解
deduceFromClasspath()
│
├─ REACTIVE 判定(三条件同时满足)
│ ├─ ClassUtils.isPresent("DispatcherHandler") == true ← Reactive 核心处理器
│ ├─ ClassUtils.isPresent("DispatcherServlet") == false ← 没有传统 Servlet
│ └─ ClassUtils.isPresent("ServletContainer") == false ← 没有 Jersey Servlet
│ │
│ └─ 满足 → REACTIVE
│
├─ SERVLET 判定
│ └─ 遍历 SERVLET_INDICATOR_CLASSES 数组,任一存在 → SERVLET
│
└─ 都不满足 → NONE2.2 SERVLET_INDICATOR_CLASSES 数组
// org.springframework.boot.WebApplicationType
private static final String[] SERVLET_INDICATOR_CLASSES = {
"jakarta.servlet.Servlet",
"org.springframework.web.servlet.DispatcherServlet" // 候补
};注意:Spring Boot 3.x 以上使用
jakarta.servlet.Servlet(Jakarta EE 命名空间),而 Spring Boot 2.x 使用的是javax.servlet.Servlet。
3. 判定优先级 —— 为什么 REACTIVE 优先于 SERVLET?
从源码可以清楚看到:REACTIVE 的判定条件最为严格,必须在同时满足"存在 DispatcherHandler + 不存在 DispatcherServlet + 不存在 ServletContainer"时才返回 REACTIVE。
这种优先级设计的原因:
避免误判:Spring MVC 和 Spring WebFlux 可以共存于同一个项目中。如果两者都在 classpath 上,按照现有逻辑会走 SERVLET 分支——因为
DispatcherServlet.class存在,REACTIVE 的第 2 个条件不满足。Reactive 对 classpath 要求更纯粹:只有明确只有 WebFlux 依赖(spring-boot-starter-webflux)而没有 spring-boot-starter-web 时,才会被判定为 REACTIVE。
回退安全:默认回退 SERVLET 而非 NONE,保证大部分 Web 应用有合理的默认行为。
4. ClassUtils.isPresent() 探测机制
deduceFromClasspath() 的核心底层是 ClassUtils.isPresent(),这是一个细粒度的类存在性检测工具。
// org.springframework.util.ClassUtils
public static boolean isPresent(String className, @Nullable ClassLoader classLoader) {
try {
// 尝试加载指定类
forName(className, classLoader);
return true;
} catch (IllegalAccessError err) {
throw new IllegalStateException("...", err);
} catch (Throwable ex) {
// ClassNotFoundException / NoClassDefFoundError → false
return false;
}
}4.1 内部执行细节
ClassUtils.isPresent("jakarta.servlet.Servlet", null)
│
├─ 1. 获取 ClassLoader
│ └─ ArgumentClassLoader → ClassUtils.getDefaultClassLoader()
│ ├─ Thread.currentThread().getContextClassLoader() // 优先线程上下文
│ └─ ClassUtils.class.getClassLoader() // 兜底
│
├─ 2. Class.forName(className, false, classLoader)
│ ├─ 成功 → return true
│ └─ 失败 → catch Throwable
│
└─ 3. catch 逻辑
├─ IllegalAccessError → 抛出 IllegalStateException(不可恢复)
├─ ClassNotFoundException → return false
├─ NoClassDefFoundError → return false
└─ LinkageError → return false4.2 为什么用 Class.forName() 而不是 ClassLoader.loadClass()?
Class.forName(className, false, classLoader) 的第二个参数 false 表示不执行静态初始化块,只做链接(linking)。这比 ClassLoader.loadClass() 更安全:
loadClass()只加载不链接,缺少LinkageError的检测;forName(..., false, ...)在链接阶段可以检测到NoClassDefFoundError等错误。
5. Spring Boot 2.x vs 3.x 的 classpath 探测差异
| 差异点 | Spring Boot 2.x | Spring Boot 3.x |
|---|---|---|
| Servlet API 包名 | javax.servlet.Servlet | jakarta.servlet.Servlet |
SERVLET_INDICATOR_CLASSES | ["javax.servlet.Servlet", "org.springframework.web.servlet.DispatcherServlet"] | ["jakarta.servlet.Servlet", "org.springframework.web.servlet.DispatcherServlet"] |
| Spring Framework 基线 | 5.x | 6.x |
ClassUtils.isPresent() 实现 | 相同逻辑,同在 spring-core | 相同逻辑,同在 spring-core |
deduceFromClasspath() 实现 | 相同三阶段判定 | 相同三阶段判定 |
迁移影响:从 Spring Boot 2 升级到 3 时,如果项目中依赖了旧的 javax.servlet API 而缺少 jakarta.servlet,Servlet 探测会失败,导致应用被误判为 NONE 类型而无法启动内嵌 Web 容器。
6. 执行时机与调用链路
deduceWebApplicationType() 在 SpringApplication 构造函数中调用:
// SpringApplication.java
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
...
this.webApplicationType = WebApplicationType.deduceFromClasspath(); // ← 第 1 步
...
}// SpringApplication.java
public ConfigurableApplicationContext run(String... args) {
...
// 将推断结果绑定到 BootstrapContext
bootstrapContext.register(WebApplicationType.class, ...);
...
// 根据 webApplicationType 创建不同 ApplicationContext
context = createApplicationContext(); // ← 使用 this.webApplicationType
...
}6.1 推断结果对后续流程的影响
webApplicationType
│
├─ SERVLET
│ └─ createApplicationContext()
│ → AnnotationConfigServletWebServerApplicationContext
│ → 触发 Tomcat / Jetty / Undertow 自动配置
│
├─ REACTIVE
│ └─ createApplicationContext()
│ → AnnotationConfigReactiveWebServerApplicationContext
│ → 触发 Netty / Reactor Netty 自动配置
│
└─ NONE
└─ createApplicationContext()
→ AnnotationConfigApplicationContext
→ 不加载 Web 相关自动配置7. 测试与自定义覆盖机制
7.1 单元测试中手动设置
@SpringBootTest
class MyJobTest {
@Test
void testJob() {
SpringApplication app = new SpringApplication(MyJob.class);
app.setWebApplicationType(WebApplicationType.NONE); // 手动覆盖
app.run();
}
}7.2 SpringApplicationBuilder 的 web() 方法
new SpringApplicationBuilder(MyApp.class)
.web(WebApplicationType.NONE) // 通过 builder 设置
.run(args);内部调用链:
// SpringApplicationBuilder.java
public SpringApplicationBuilder web(WebApplicationType webApplicationType) {
this.application.setWebApplicationType(webApplicationType); // ← 直接设置
return this;
}7.3 ReactiveWebServerFactory 与 ServletWebServerFactory 共存时的处理
当 classpath 同时存在 spring-boot-starter-web 和 spring-boot-starter-webflux 时:
deduceFromClasspath() 判定结果:SERVLET
│
└─ 创建 AnnotationConfigServletWebServerApplicationContext
│
├─ ServletWebServerFactory 自动配置生效
│ └─ TomcatServletWebServerFactory 创建 Tomcat
│
└─ ReactiveWebServerFactory 被 @ConditionalOnMissingBean 抑制
└─ 不创建 NettyReactiveWebServerFactory这是因为 ServletWebServerFactoryAutoConfiguration 和 ReactiveWebServerFactoryAutoConfiguration 各自有 @ConditionalOnClass 条件,且 ServletWebServerFactoryConfiguration 内部使用 @ConditionalOnMissingBean 来保证只有一个生效。
8. 推断失败的回退策略
如果 deduceFromClasspath() 流程中发生异常(例如 ClassUtils.isPresent() 抛出 IllegalStateException),整个 SpringApplication 的构造会失败,并不会静默回退。
但在正常路径中,如果 classpath 上没有任何 Web 相关类(既没有 jakarta.servlet.Servlet,也没有 DispatcherHandler),则回退到 WebApplicationType.NONE,应用以非 Web 模式启动,不会尝试启动内嵌容器。
默认兜底逻辑
// SpringApplication.java(构造方法中)
this.webApplicationType = WebApplicationType.deduceFromClasspath();
// 如果没有主动设置过,则使用推断结果
// 可以通过 setWebApplicationType() 主动覆盖整个设计原则是:推断是默认行为,但允许显式覆盖,保证了灵活性与可控性的平衡。
总结
| 细节点 | 核心要点 |
|---|---|
① deduceWebApplicationType() 实现 | ClassUtils.isPresent() 三阶段探测 |
| ② 判定优先级 | REACTIVE → SERVLET → NONE,REACTIVE 条件最严格 |
| ③ 2.x vs 3.x 差异 | javax.servlet → jakarta.servlet 包名迁移 |
| ④ 共存时处理 | ServletWebServerFactory 优先于 Reactive |
| ⑤ 测试中 NONE 的触发 | setWebApplicationType(NONE) 手动覆盖 |
| ⑥ Builder 调用路径 | SpringApplicationBuilder.web(NONE) → setWebApplicationType() |
| ⑦ 回退策略 | 无 Web 类时回退 NONE,非 Web 模式启动 |