Spring Boot 4.1.1 深度源码解析-第 17 章 应用类型推断与上下文工厂:`WebApplicationType`

10 阅读56分钟

Spring Boot 4.1.1 深度源码解析-第 17 章 应用类型推断与上下文工厂:WebApplicationType 与 ApplicationContextFactory

卷五 · Web 栈 · 第一章

从这一章开始,我们离开"通用启动引擎"的地带,进入 Spring Boot 最有存在感的一部分:Web 栈。 但在讲 DispatcherServlet、讲 Tomcat 连接器之前,必须先回答一个更靠前的问题: Spring Boot 是怎么决定"我是一个 Web 应用"的?以及,它凭什么决定该造出什么样的 ApplicationContext?

这两个问题的答案,分别落在 WebApplicationType 与 ApplicationContextFactory 上。 它们都很短——WebApplicationType 全文不到 120 行,ApplicationContextFactory 不到 100 行—— 但它们决定了后面几十章所有内容的运行环境。


0. 引子:三个真实报错,以及它们共同的根

在进入源码之前,先看三个在生产与 Stack Overflow 上反复出现的报错。它们看起来毫不相关,实际上根都在本章要讲的两个类上。

报错 A

org.springframework.context.ApplicationContextException: Unable to start ServletWebServerApplicationContext
due to multiple ServletWebServerFactory beans : tomcatServletWebServerFactory, jettyServletWebServerFactory

现象:项目里同时引入了 spring-boot-starter-web 和 spring-boot-starter-jetty(或者有人手贱引入了两个 starter-tomcat 版本)。 根:ServletWebServerApplicationContext.getWebServerFactory() 要求恰好一个 ServletWebServerFactory bean。它拿到 0 个或者 ≥2 个都会抛异常——但这两个分支抛的是不同类型的异常。

报错 B

org.springframework.context.ApplicationContextException: Unable to start ReactiveWebApplicationContext
due to missing HttpHandler bean

现象:classpath 上有 spring-boot-starter-webflux,上下文被判定成了 REACTIVE, 但 @EnableWebFlux/WebFluxAutoConfiguration 没生效(比如被某个条件排掉了),于是 HttpHandler 缺失。 根:ReactiveWebServerApplicationContext.getHttpHandler()。而"上下文被判定成 REACTIVE"这一步,就是本章的 WebApplicationType 干的。

报错 C

java.lang.IllegalStateException: Unable create a default ApplicationContext instance,
you may need a custom ApplicationContextFactory

现象:自定义了 setApplicationContextFactory(...),但这个工厂在给定 webApplicationType 下返回了 null, 同时 SPI 链上也没有任何工厂能接住。 根:DefaultApplicationContextFactory.create(...) 的兜底分支——注意它抛的是 IllegalStateException,不是 ApplicationContextException。

这三个报错的共同根,都可以归到一句话:

SpringApplication 在构造期推断出一个 WebApplicationType,然后拿着这个值去问一组 ApplicationContextFactory:谁能给我环境?谁能给我上下文?

谁先回答,就用谁的。本章就是把"推断"和"问"这两个动作,拆成源码级别的步骤。


1. 全景:类型决策的三个落点

WebApplicationType 这个值一旦确定,会在三个不同的时刻被消费。理解这一点,是理解整章的关键。

1.1 三个落点

#落点触发位置消费方式结果
1Environment 类型prepareEnvironment()deduceEnvironmentClass() → EnvironmentConverter决定 Environment 是 ApplicationEnvironment/ApplicationServletEnvironment/ApplicationReactiveWebEnvironment
2Environment 实例getOrCreateEnvironment()applicationContextFactory.createEnvironment(type)在落点 1 之前先造一个实例
3ApplicationContextcreateApplicationContext()applicationContextFactory.create(type)决定 AnnotationConfigApplicationContext/AnnotationConfigServletWebServerApplicationContext/AnnotationConfigReactiveWebServerApplicationContext

注意落点 1 和落点 2 是两件不同的事,很容易混淆:

  • 落点 2 是"先造一个环境出来"——因为 prepareEnvironment() 一开始就需要一个 Environment 对象去跑 ConfigData、去跑 listeners.environmentPrepared(...)。
  • 落点 1 是"造完之后,如果发现类型不对,再转换一次"——因为落点 2 造出来的环境可能不是最终想要的类型。

为什么要分两步?因为 SpringApplication 允许你提前注入一个 Environment:

private ConfigurableEnvironment getOrCreateEnvironment() {
    if (this.environment != null) {
        return this.environment;      // ← 用户通过 setEnvironment() 提前注入
    }
    WebApplicationType webApplicationType = this.properties.getWebApplicationType();
    ConfigurableEnvironment environment = this.applicationContextFactory.createEnvironment(webApplicationType);
    if (environment == null && this.applicationContextFactory != ApplicationContextFactory.DEFAULT) {
        environment = ApplicationContextFactory.DEFAULT.createEnvironment(webApplicationType);
    }
    return (environment != null) ? environment : new ApplicationEnvironment();
}

如果你调了 application.setEnvironment(myEnv),那第一步直接返回你给的对象, 此时这个对象的类型可能跟 webApplicationType 完全不匹配。所以第二步必须存在:用 EnvironmentConverter 把它转成正确的类型。

1.2 时序

SpringApplication 构造
  └─ this.properties.setWebApplicationType(WebApplicationType.deduce())   ← 【推断】
        │
        ├─ SpringFactoriesLoader.load(Deducer.class)  → MVC(10) / WebFlux(20)
        └─ 兜底 isServletApplication() → SERVLET / NONE
  │
run(args)
  │
  ├─ prepareEnvironment(...)
  │    ├─ getOrCreateEnvironment()                  ← 【落点 2】createEnvironment(type)
  │    │     └─ applicationContextFactory.createEnvironment(webApplicationType)
  │    │           └─ DefaultApplicationContextFactory → SPI 链
  │    ├─ configureEnvironment(env, args)
  │    ├─ ConfigurationPropertySources.attach(env)
  │    ├─ listeners.environmentPrepared(...)        ← ConfigData 在这一步把 application.yaml 读进来
  │    ├─ bindToSpringApplication(env)              ← ★ spring.main.web-application-type 在这里覆盖!
  │    └─ if (!isCustomEnvironment)
  │          env = environmentConverter.convertEnvironmentIfNecessary(env, deduceEnvironmentClass())
  │                                                  ← 【落点 1】getEnvironmentType(type)
  │
  ├─ printBanner(env)
  ├─ createApplicationContext()                     ← 【落点 3】create(type)
  │     └─ Assert.state(context != null, "ApplicationContextFactory created null context")
  ├─ prepareContext(...)
  └─ refreshContext(context)

1.3 一个必须记住的时序事实

看上面这张图里打了 ★ 的那一行:

bindToSpringApplication(environment);
if (!this.isCustomEnvironment) {
    EnvironmentConverter environmentConverter = new EnvironmentConverter(getClassLoader());
    environment = environmentConverter.convertEnvironmentIfNecessary(environment, deduceEnvironmentClass());
}

bindToSpringApplication() 出现在 deduceEnvironmentClass() 之前。 而 createApplicationContext() 在它们之后。

这意味着:

spring.main.web-application-type 可以写在 application.yaml 里,并且会真的生效—— 它不仅能改变 Environment 的类型,还能改变 ApplicationContext 的类型。

这不是显而易见的设计。很多人的直觉是"classpath 推断发生在构造期,配置文件的读取发生在启动期,所以配置文件改不了类型"。 这个直觉是错的。真正的原因是:

  1. 构造期推断出来的值只是初始值,被放进 ApplicationProperties.webApplicationType;
  2. prepareEnvironment() 里 bindToSpringApplication() 用 Binder 把 spring.main.* 重新绑定一遍,覆盖了初始值;
  3. 落点 1、落点 3 都在绑定之后才读这个值。

源码级证据:

// SpringApplication.java:1113 附近(非 Web 应用也能通过属性覆盖)
@Test
void nonWebApplicationConfiguredViaAPropertyHasTheCorrectTypeOfContextAndEnvironment() {
    ConfigurableApplicationContext context = new SpringApplication(ExampleConfig.class)
        .run("--spring.main.web-application-type=none");
    assertThat(context).isNotInstanceOfAny(WebApplicationContext.class, ReactiveWebApplicationContext.class);
    assertThat(context.getEnvironment()).isNotInstanceOfAny(ConfigurableWebEnvironment.class);
}

这个测试同时断言了上下文类型和环境类型两个维度都被属性改变了。这是"落点 1 与落点 3 共享同一个值"的直接证据。

再看 smoke test:

# smoke-test/spring-boot-smoke-test-web-application-type/src/main/resources/application.properties
spring.main.web-application-type=reactive

一个 classpath 上是 servlet 栈的项目(这个 smoke test 模块叫 spring-boot-smoke-test-web-application-type), 通过一行配置被强制改成 REACTIVE。后面还有一个测试叫 WebEnvironmentNoneOverridesWebApplicationTypeTests, 用 @SpringBootTest(webEnvironment = WebEnvironment.NONE) 再把它覆盖回非 Web——这是第五条覆盖路径,稍后第 5 节会展开。

1.4 为什么"推断"和"创建"要分成两个 SPI

一个自然的疑问:为什么不让 WebApplicationType 自己决定上下文类型,而要绕一圈 ApplicationContextFactory?

答案藏在 ApplicationContextFactory 的类注释里:

/**
 * Strategy interface for creating the {@link ConfigurableApplicationContext} used by a
 * {@link SpringApplication}. Created contexts should be returned in their default form,
 * with the {@code SpringApplication} responsible for configuring and refreshing the
 * context.
 */

"Created contexts should be returned in their default form"——工厂只负责造,不负责配。 这个约束带来的好处是:

  • 新增一种 Web 栈(比如 4.x 新增的 gRPC),不需要改 WebApplicationType 枚举,也不需要改 SpringApplication;
  • 只要往 META-INF/spring.factories 里加一个 ApplicationContextFactory 实现即可;
  • 而"我是不是 Web 应用"这个语义问题,则由 WebApplicationType.Deducer 这个独立 SPI 回答。

两个 SPI 的切分依据是:"是什么"和"怎么造"是两件事。 第 4.x 版把原本写死在 WebApplicationType.deduceFromClasspath() 里的逻辑外置成 Deducer,时间点就在 @since 4.0.1—— 这正是模块化改造(第 03 章讲的 *.imports 与模块拆分)的同一波浪潮。


2. WebApplicationType 枚举逐行

文件坐标:core/spring-boot/src/main/java/org/springframework/boot/WebApplicationType.java 作者:Andy Wilkinson、Brian Clozel、Phillip Webb @since:2.0.0

2.1 三个常量

public enum WebApplicationType {

	/**
	 * The application should not run as a web application and should not start an
	 * embedded web server.
	 */
	NONE,

	/**
	 * The application should run as a servlet-based web application and should start an
	 * embedded servlet web server.
	 */
	SERVLET,

	/**
	 * The application should run as a reactive web application and should start an
	 * embedded reactive web server.
	 */
	REACTIVE;

三个常量,没有额外字段,没有构造器参数。它是纯粹的标记枚举。

Javadoc 里有两个值得抠的词:

  • NONE:"should not start an embedded web server"——注意是"不启动内嵌服务器",不是"不能处理 HTTP"。 一个 NONE 应用照样可以有 RestClient、可以有 @HttpExchange 客户端,只是不监听端口。
  • SERVLET / REACTIVE:"should start an embedded servlet/reactive web server"——强调 embedded。 这排除了 WAR 部署到外部容器的场景吗?不排除。WAR 部署有自己的工厂(第 12 节会讲),但 WebApplicationType 仍然是 SERVLET。 也就是说:SERVLET 的语义是"servlet 栈",不是"必然内嵌"。

2.2 SERVLET_INDICATOR_CLASSES:为什么是这两个类

private static final String[] SERVLET_INDICATOR_CLASSES = { "jakarta.servlet.Servlet",
		"org.springframework.web.context.ConfigurableWebApplicationContext" };

判断"是不是 servlet 应用",用的是两个类同时存在:

  1. jakarta.servlet.Servlet——Servlet API 本身;
  2. org.springframework.web.context.ConfigurableWebApplicationContext——Spring 的 Web 上下文抽象。

为什么需要第二个?

设想一个场景:你的项目里通过某个传递依赖带进来了 jakarta.servlet-api(比如某个老版本的 javax.servlet 桥接包、 或者某个 SDK 自带 servlet API)。此时只有 jakarta.servlet.Servlet 存在, 而 spring-web 不在 classpath 上 → ConfigurableWebApplicationContext 不存在。

如果只看第一个类,Spring Boot 会误判成 SERVLET,然后尝试创建 AnnotationConfigServletWebServerApplicationContext, 接着在 onRefresh() 里 getWebServerFactory() 找不到 ServletWebServerFactory bean,抛出报错 A 的"missing"分支。 两个类一起判断,把这个误判窗口缩到了最小。

第二个类来自 spring-web(包名 org.springframework.web.context), 而 spring-web 是 spring-webmvc 的硬依赖。所以"两个类都在"实际上等价于"spring-webmvc(或至少 spring-web + servlet-api)在 classpath 上"。

注意这里有个不对称:jakarta.servlet.Servlet 来自 servlet-api,ConfigurableWebApplicationContext 来自 spring-web。 只引入 spring-web 而不引入 servlet-api 的情况几乎不存在(因为 spring-web 对 servlet-api 是 provided/optional), 但只引入 servlet-api 不引入 spring-web 的情况很常见。这个双类设计就是防后者的。

2.3 deduce()

/**
 * Deduce the {@link WebApplicationType} from the current classpath.
 * @return the deduced web application
 * @since 4.0.1
 */
public static WebApplicationType deduce() {
    for (Deducer deducer : SpringFactoriesLoader.forDefaultResourceLocation().load(Deducer.class)) {
        WebApplicationType deduced = deducer.deduceWebApplicationType();
        if (deduced != null) {
            return deduced;
        }
    }
    return isServletApplication() ? WebApplicationType.SERVLET : WebApplicationType.NONE;
}

全文只有 10 行,但每一行都有讲究:

(1) SpringFactoriesLoader.forDefaultResourceLocation() 这是 Spring Framework 7 的新工厂方法(旧写法是 SpringFactoriesLoader.loadFactories(...))。 forDefaultResourceLocation() 返回一个绑定到 META-INF/spring.factories 的 loader 实例。 它是实例方法链,可以 .load(...)、.load(..., argumentResolver)。

(2) .load(Deducer.class) 返回的是 List<Deducer>,且已按 @Ordered/@Order 排序 这一点极其重要。SpringFactoriesLoader 内部用 AnnotationAwareOrderComparator 排序(框架行为,未在本仓库逐行核实, 但 Boot 侧大量依赖这个语义,例如 ServletWebServerFactoryCustomizerBeanPostProcessor 就自己又排了一次序)。 因此 WebMvcWebApplicationTypeDeducer 的 @Order(10) 会排在 WebFluxWebApplicationTypeDeducer 的 @Order(20) 之前。

(3) if (deduced != null) return deduced; Deducer 的返回类型是 @Nullable WebApplicationType。返回 null 表示"我不知道,问下一个"。 这是典型的责任链(Chain of Responsibility)。

(4) 兜底:isServletApplication() ? SERVLET : NONE

★ 核心结论:deduce() 的兜底分支永远不可能返回 REACTIVE。 在 Spring Boot 4.1.1 中,REACTIVE 只能由 WebApplicationType.Deducer SPI 返回。 核心模块(spring-boot)本身对"响应式"一无所知——它连 reactor.core.publisher.Mono 都不认识。

这是模块化改造的直接结果,也是 3.x 与 4.x 的一个行为差异: 3.x 时代 WebApplicationType.deduceFromClasspath() 是写死在枚举里的:

// 3.x 的写法(示意,本仓库不含此代码)
static WebApplicationType deduceFromClasspath() {
    if (ClassUtils.isPresent(WEBFLUX_INDICATOR_CLASS, null)
            && !ClassUtils.isPresent(WEBMVC_INDICATOR_CLASS, null)
            && !ClassUtils.isPresent(JERSEY_INDICATOR_CLASS, null)) {
        return WebApplicationType.REACTIVE;
    }
    for (String className : SERVLET_INDICATOR_CLASSES) {
        if (!ClassUtils.isPresent(className, null)) {
            return WebApplicationType.NONE;
        }
    }
    return WebApplicationType.SERVLET;
}

3.x 里 REACTIVE 的判定条件里显式带了 !ClassUtils.isPresent(WEBMVC_INDICATOR_CLASS, null)—— "有 WebFlux 且没有 WebMVC 才是 REACTIVE"。 4.x 把这个判断挪到了 Deducer 的 @Order 上:MVC 的 Deducer 先跑,它命中就返回 SERVLET,WebFlux 的 Deducer 根本没机会跑。 语义等价,但机制完全不同。

⚠️ 未逐行核实:3.x 的源码不在本仓库中,上面的 3.x 代码片段来自我对历史版本的记忆, 属于"推测/待核实"。可验证的本仓库事实是:4.1.1 的 deduce() 兜底只有 SERVLET/NONE 两值, REACTIVE 必然来自 Deducer。

2.4 isServletApplication():注意是"全部满足"不是"任一满足"

private static boolean isServletApplication() {
    for (String servletIndicatorClass : SERVLET_INDICATOR_CLASSES) {
        if (!ClassUtils.isPresent(servletIndicatorClass, null)) {
            return false;
        }
    }
    return true;
}

循环体里是 if (!present) return false——短路与语义。 两个类必须全部存在才返回 true。 这与第 15 章讲的 @ConditionalOnClass({"A","B"}) 语义一致("和"而非"或")。

同样地,两个 Deducer 里的 INDICATOR_CLASSES 循环也是同样的写法:

// WebMvcWebApplicationTypeDeducer
for (String indicatorClass : INDICATOR_CLASSES) {
    if (!ClassUtils.isPresent(indicatorClass, null)) {
        return null;
    }
}
return WebApplicationType.SERVLET;

同一个模式出现了三次(核心兜底 + 两个 Deducer)。这是 Boot 的惯用法,值得记住。

2.5 ClassUtils.isPresent(name, null) 的 null ClassLoader

三个地方都传了 null 作为第二个参数。ClassUtils.isPresent(String className, @Nullable ClassLoader classLoader) 的实现是:

// spring-core(不在本仓库)
public static boolean isPresent(String className, @Nullable ClassLoader classLoader) {
    try {
        forName(className, classLoader);
        return true;
    } catch (IllegalAccessError | Throwable ex) {
        return false;
    }
}

传 null 表示"用默认的 ClassUtils.getDefaultClassLoader()",即: 先取当前线程的 context ClassLoader,取不到再取 ClassUtils 自己的 ClassLoader,再取不到用 ClassLoader.getSystemClassLoader()。

这个细节很重要:这意味着推断结果依赖于线程上下文 ClassLoader。 在某些隔离类加载器环境(OSGi、某些插件容器、spring-boot-loader 的 LaunchedURLClassLoader 嵌套场景, 或者测试里手动切换 TCCL)下,推断可能得到与"肉眼看到的 classpath"不同的结果。

顺带一提,catch (... | Throwable ex) 这个写法捕捉的是 Throwable—— 包括 ClassNotFoundException、NoClassDefFoundError(类存在但依赖缺失)、LinkageError。 "类文件在,但它引用的某个类不在"这种情况,也会被算作"不存在"。这是正确的语义(这个类确实不可用),但排查时容易困惑。

2.6 WebApplicationTypeRuntimeHints

static class WebApplicationTypeRuntimeHints implements RuntimeHintsRegistrar {

    @Override
    public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) {
        for (String servletIndicatorClass : SERVLET_INDICATOR_CLASSES) {
            registerTypeIfPresent(servletIndicatorClass, classLoader, hints);
        }
    }

    private void registerTypeIfPresent(String typeName, @Nullable ClassLoader classLoader, RuntimeHints hints) {
        if (ClassUtils.isPresent(typeName, classLoader)) {
            hints.reflection().registerType(TypeReference.of(typeName));
        }
    }

}

这个类是包私有静态内部类,说明它是在别处(大概率是某个 @ImportRuntimeHints 或 spring.factories 的 RuntimeHintsRegistrar 条目)被注册的。

⚠️ 未能核实:我在本仓库中没有定位到 WebApplicationTypeRuntimeHints 的注册点。 用 grep -rn "WebApplicationTypeRuntimeHints" 只能命中定义处本身。 可能它未被注册(遗留代码),也可能通过某种我没有检索到的机制注册。 这一条是本次阅读的未完成项,有兴趣的读者可以自行在 core/spring-boot 的 spring.factories 与 *.imports 中搜索。

它的作用本身很清楚:为 SERVLET_INDICATOR_CLASSES 里确实存在的类注册反射提示。 注意 registerTypeIfPresent 的守卫——只为存在的类注册,避免为不存在的类生成反射条目导致 native image 构建失败。

这个"先探测再注册"的模式,与第 16 章讲的 NoSuchBeanDefinitionFailureAnalyzer 的"提前抓取"是同一种防御性思维。


3. Deducer SPI:推断逻辑的外置

3.1 接口定义

/**
 * Strategy that may be implemented by a module that can deduce the
 * {@link WebApplicationType}.
 *
 * @since 4.0.1
 */
@FunctionalInterface
public interface Deducer {

    /**
     * Deduce the web application type.
     * @return the deduced web application type or {@code null}
     */
    @Nullable WebApplicationType deduceWebApplicationType();

}

@FunctionalInterface + 单一无参方法 + @Nullable 返回。 可以用 lambda 实现:() -> WebApplicationType.REACTIVE。

注意它的 @since 4.0.1——与 deduce() 的 @since 4.0.1 一致,说明二者是同一次提交引入的。

3.2 为什么要有这个 SPI

如果你读过第 03 章(模块化的自动配置),会发现这是同一套思路:

核心模块(spring-boot)不应该知道任何具体技术栈的存在。 它只定义"我这里有个决策点,谁关心谁来注册"。

在 4.x 里,spring-boot 核心模块不依赖 spring-webmvc,也不依赖 spring-webflux。 所以核心模块里的 deduce() 不可能去检查 DispatcherServlet 或 DispatcherHandler 是否存在—— 它连这些类的名字都不该知道。

于是:

  • spring-boot-webmvc 模块注册 WebMvcWebApplicationTypeDeducer
  • spring-boot-webflux 模块注册 WebFluxWebApplicationTypeDeducer
  • 核心模块只负责"按顺序问一遍,都没人认领就走兜底"

这就是依赖倒置:决策逻辑被下沉到知道具体技术栈的模块里。

3.3 WebMvcWebApplicationTypeDeducer

文件坐标:module/spring-boot-webmvc/src/main/java/org/springframework/boot/webmvc/WebMvcWebApplicationTypeDeducer.java 作者:Phillip Webb 类可见性:class(包私有)

/**
 * {@link WebApplicationType} deducer to ensure Spring MVC applications use
 * {@link WebApplicationType#SERVLET}.
 *
 * @author Phillip Webb
 */
@Order(10) // Ordered before WebFlux
class WebMvcWebApplicationTypeDeducer implements WebApplicationType.Deducer {

	private static final String[] INDICATOR_CLASSES = { "jakarta.servlet.Servlet",
			"org.springframework.web.servlet.DispatcherServlet",
			"org.springframework.web.context.ConfigurableWebApplicationContext" };

	@Override
	public @Nullable WebApplicationType deduceWebApplicationType() {
		// Guard in case the classic module is being used and dependencies are excluded
		for (String indicatorClass : INDICATOR_CLASSES) {
			if (!ClassUtils.isPresent(indicatorClass, null)) {
				return null;
			}
		}
		return WebApplicationType.SERVLET;
	}

	// ... RuntimeHints 略
}

三个关键事实:

(1) @Order(10) // Ordered before WebFlux 注释直白地说明了排序意图。10 < 20,所以 MVC 先跑。

(2) 三个指示类 相比核心的 SERVLET_INDICATOR_CLASSES(2 个),这里多了一个 org.springframework.web.servlet.DispatcherServlet。 DispatcherServlet 来自 spring-webmvc。所以这个 Deducer 实际判定的是"完整的 MVC 栈", 而核心兜底判定的是"至少有 servlet 能力"。

(3) 那句注释:// Guard in case the classic module is being used and dependencies are excluded

这句注释非常值得解释。它涉及 4.x 的"classic 模块"概念。

Spring Boot 4.x 把原来的 spring-boot-starter-web 拆成了模块化结构, 同时保留了一个"classic"兼容层(第 03 章讲过 classic 兼容层)。 在 classic 模块里,依赖是"一大包"的——比如 classic 的 web starter 可能同时带上 MVC 和 WebFlux 的类。

但用户可以用 Gradle/Maven 的 exclude 把其中一部分排掉。此时:

  • spring-boot-webmvc 这个模块 jar 还在 classpath 上 → 所以 WebMvcWebApplicationTypeDeducer 会被 SpringFactoriesLoader 加载;
  • 但 spring-webmvc 这个库 jar 被 exclude 了 → 所以 DispatcherServlet 类不存在。

如果没有这个 guard,Deducer 会直接返回 SERVLET(因为它就是写死的), 然后 ServletWebServerApplicationContext 会在启动时因为找不到 DispatcherServlet 而炸掉—— 但那已经是 refresh() 阶段了,报错会非常难懂。

有了这个 guard:类不在就返回 null,责任链继续往下走(下一个是 WebFlux 的 Deducer), 最终可能落到 NONE。在早期就优雅降级,而不是在晚期爆炸。

方法论提炼(第十三定律的雏形):凡是 SPI 实现类里的"存在性检查", 十有八九是在防"模块 jar 在、实现 jar 不在"的依赖裁剪场景。 看到这类注释,立刻去想:这个模块是不是有 classic 版本?有没有可能被 exclude?

3.4 WebFluxWebApplicationTypeDeducer

文件坐标:module/spring-boot-webflux/src/main/java/org/springframework/boot/webflux/WebFluxWebApplicationTypeDeducer.java

@Order(20) // Ordered after MVC
class WebFluxWebApplicationTypeDeducer implements WebApplicationType.Deducer {

	private static final String[] INDICATOR_CLASSES = { "reactor.core.publisher.Mono",
			"org.springframework.web.reactive.DispatcherHandler" };

	@Override
	public @Nullable WebApplicationType deduceWebApplicationType() {
		// Guard in case the classic module is being used and dependencies are excluded
		for (String indicatorClass : INDICATOR_CLASSES) {
			if (!ClassUtils.isPresent(indicatorClass, null)) {
				return null;
			}
		}
		return WebApplicationType.REACTIVE;
	}

	// ... RuntimeHints 略
}

与 MVC 版本结构完全一致,区别只有:

MVC DeducerWebFlux Deducer
@Order1020
指示类数量32
指示类jakarta.servlet.Servlet
DispatcherServlet
ConfigurableWebApplicationContext
reactor.core.publisher.Mono
org.springframework.web.reactive.DispatcherHandler
返回SERVLETREACTIVE
所在模块spring-boot-webmvcspring-boot-webflux

WebFlux 只有两个指示类,因为不需要 servlet API。注意里面没有 ConfigurableWebApplicationContext—— reactive 栈用的是 org.springframework.boot.web.context.reactive.ReactiveWebApplicationContext(Boot 自己的), 不是 Spring 的 ConfigurableWebApplicationContext。

3.5 spring.factories 注册

# module/spring-boot-webmvc/src/main/resources/META-INF/spring.factories
# WebApplicationType Deducers
org.springframework.boot.WebApplicationType$Deducer=\
org.springframework.boot.webmvc.WebMvcWebApplicationTypeDeducer
# module/spring-boot-webflux/src/main/resources/META-INF/spring.factories
# WebApplicationType Deducers
org.springframework.boot.WebApplicationType$Deducer=\
org.springframework.boot.webflux.WebFluxWebApplicationTypeDeducer

注意 key 是 org.springframework.boot.WebApplicationType$Deducer——嵌套接口用 $ 分隔,这是 spring.factories 的约定。

对比:同目录下的自动配置注册用的是 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports, 而 Deducer 用的是传统的 spring.factories。 这说明 SpringFactoriesLoader 机制在 4.x 并没有被废弃,.imports 只服务于"自动配置类"这一种场景(第 03 章讲过)。 凡是 key 不是 ...AutoConfiguration 的 SPI,依旧走 spring.factories。

3.6 优先级推导:MVC 与 WebFlux 同时存在 → SERVLET

把上面的信息拼起来:

classpath 上同时有 spring-webmvc + spring-webflux
  │
  ├─ Deducer 链(按 @Order 排序后):[MvcDeducer(10), WebFluxDeducer(20)]
  │
  ├─ MvcDeducer.deduceWebApplicationType()
  │     三个类都在 → return SERVLET
  │
  └─ deduce() 的 if (deduced != null) 命中 → 直接 return SERVLET
     WebFluxDeducer 从未被调用

结论:classpath 上同时有 MVC 和 WebFlux 时,Spring Boot 4.1.1 判定为 SERVLET。

这与 Spring Boot 官方文档的说法一致("If both are present, Spring MVC wins")。 但源码层面的实现机制在 4.x 变了:不是靠 !isPresent(WEBMVC) 的显式排除,而是靠 @Order 的短路。

⚠️ 一个容易踩的坑:这个"MVC 赢"的结论依赖于 SpringFactoriesLoader.load() 确实按 @Order 排序。 这个排序行为由 Spring Framework 的 SpringFactoriesLoader 提供,不在本仓库,我未能逐行核实。 但从 Boot 侧的 @Order(10) // Ordered before WebFlux 注释来看,作者显然依赖了这个行为。 如果你在自己的环境中发现两者同时存在时被判成了 REACTIVE,第一件事就是去查排序。

3.7 完整的决策表

把 deduce() 的所有可能路径列成表。记:

  • S = jakarta.servlet.Servlet
  • C = org.springframework.web.context.ConfigurableWebApplicationContext
  • D = org.springframework.web.servlet.DispatcherServlet
  • M = reactor.core.publisher.Mono
  • H = org.springframework.web.reactive.DispatcherHandler
#SCDMHMVC DeducerWebFlux Deducer兜底结果
1✗✗✗✗✗nullnullNONENONE
2✓✓✓✗✗SERVLET——SERVLET
3✗✗✗✓✓nullREACTIVE—REACTIVE
4✓✓✓✓✓SERVLET(不执行)—SERVLET
5✓✓✗✓✓nullREACTIVE—REACTIVE
6✓✓✗✓✗nullnullSERVLETSERVLET
7✓✗✗✗✗nullnullNONENONE
8✗✓✗✗✗nullnullNONENONE
9✓✓✓✓✗SERVLET(不执行)—SERVLET

逐条解释几个反直觉的:

第 5 行(S✓ C✓ D✗ M✓ H✓ → REACTIVE) 有 servlet-api、有 spring-web,但没有 spring-webmvc,同时有完整的 WebFlux。 MVC Deducer 因为 D 不存在返回 null;WebFlux Deducer 命中 → REACTIVE。 这正是 spring-boot-starter-webflux 单独使用、但传递依赖里混进了 servlet-api 的情况。 注意:这是 3.x 与 4.x 语义保持一致的关键场景。

第 6 行(S✓ C✓ D✗ M✓ H✗ → SERVLET) 有 servlet-api、有 spring-web、有 reactor 的 Mono,但没有 DispatcherHandler(即没有 spring-webflux)。 这在实际中很常见——很多库(如 reactor-core 被某个 SDK 带入)会带来 Mono,但并没有 WebFlux。 两个 Deducer 都返回 null,走兜底 isServletApplication()(S✓ C✓)→ SERVLET。 这就是为什么"项目里出现了 Mono 却依然是 SERVLET"。

第 7 行(只有 servlet-api → NONE) 这是 2.2 节讲过的误判防护场景。只有 servlet-api,没有 spring-web → C 不存在 → 兜底返回 NONE。 这是正确的:一个只有 servlet-api 的项目确实不应该是 Spring Boot 的 Web 应用。

第 8 行(只有 spring-web → NONE) 同理,S 不存在 → NONE。

3.8 两个 RuntimeHints 的重复代码

MVC 和 WebFlux 的 Deducer 各带一个几乎一模一样的 RuntimeHintsRegistrar 内部类:

static class WebMvcWebApplicationTypeDeducerRuntimeHints implements RuntimeHintsRegistrar {
    @Override
    public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) {
        for (String servletIndicatorClass : INDICATOR_CLASSES) {
            registerTypeIfPresent(servletIndicatorClass, classLoader, hints);
        }
    }
    private void registerTypeIfPresent(String typeName, @Nullable ClassLoader classLoader, RuntimeHints hints) {
        if (ClassUtils.isPresent(typeName, classLoader)) {
            hints.reflection().registerType(TypeReference.of(typeName));
        }
    }
}

WebFlux 版本唯一的区别是变量名还是叫 servletIndicatorClass(复制粘贴的痕迹), 以及 INDICATOR_CLASSES 的内容不同。

三份(WebApplicationType + MVC + WebFlux)几乎相同的 registerTypeIfPresent 实现。 这是一个可以抽取的重复,但 Boot 选择了复制—— 我推测原因是:这些 hints 类的注册点分散在不同模块,抽成公共类会引入 spring-boot → 各模块的反向依赖方向问题, 或者单纯因为每个 hints 类必须能被独立注册(推测,未核实)。


4. 显式覆盖:五条路径与它们的优先级

deduce() 的结果只是初始值。有五条路径可以覆盖它。

4.1 路径一:SpringApplication.setWebApplicationType(...)

public void setWebApplicationType(WebApplicationType webApplicationType) {
    Assert.notNull(webApplicationType, "'webApplicationType' must not be null");
    this.properties.setWebApplicationType(webApplicationType);
}

最简单的路径。注意 Assert.notNull——不能传 null 来"取消设置"。 如果想恢复推断,只能重新 new SpringApplication(...)。

4.2 路径二:SpringApplicationBuilder.web(...)

public SpringApplicationBuilder web(WebApplicationType webApplicationType) {
    this.application.setWebApplicationType(webApplicationType);
    return this;
}

只是路径一的流式封装。

SpringApplicationBuilder 内部有两处强制 NONE 的地方,值得注意:

// child(...) 方法内
// It's not possible if embedded web server are enabled to support web contexts as
// parents because the servlets cannot be initialized at the right point in lifecycle.
web(WebApplicationType.NONE);
// parent(...) 方法内
if (this.parent == null) {
    this.parent = new SpringApplicationBuilder(sources).web(WebApplicationType.NONE)
        .properties(this.defaultProperties)
        .environment(this.environment);
}

父/子上下文一律是 NONE。注释给的理由是:"如果启用了内嵌 web server,就不可能让 web 上下文当父上下文, 因为 servlet 无法在生命周期的正确时间点被初始化。"

这句话背后的机制是:servlet 的初始化发生在 onRefresh() → createWebServer() → getSelfInitializer().onStartup(servletContext), 而父子上下文的关联发生在更早的阶段。如果父上下文也要起一个 web server,时序会打架。

4.3 路径三:spring.main.web-application-type(配置)

这是最容易被忽略、也最有用的一条。

绑定目标:ApplicationProperties.webApplicationType

// ApplicationProperties.java:86
/**
 * Flag to explicitly request a specific type of web application. If not set,
 * auto-detected based on the classpath.
 */
private @Nullable WebApplicationType webApplicationType;

@Nullable WebApplicationType getWebApplicationType() {
    return this.webApplicationType;
}

void setWebApplicationType(@Nullable WebApplicationType webApplicationType) {
    this.webApplicationType = webApplicationType;
}

注意 setter/getter 都是包私有(没有 public)。这不影响 Binder—— Spring Boot 的 JavaBeanBinder 会通过 ReflectionUtils.makeAccessible 调用非 public 的 setter。 (框架行为,未在本仓库逐行核实,但 SpringApplicationTests 的测试用例提供了行为证据。)

配置元数据(core/spring-boot/src/main/resources/META-INF/additional-spring-configuration-metadata.json):

{
  "name": "spring.main.web-application-type",
  "type": "org.springframework.boot.WebApplicationType",
  "sourceType": "org.springframework.boot.SpringApplication",
  "description": "Flag to explicitly request a specific type of web application. If not set, auto-detected based on the classpath."
}

注意这份元数据是手写的(additional- 前缀),不是注解处理器生成的。 因为 ApplicationProperties 不是 @ConfigurationProperties 类,处理器不会扫它。

同一个文件里还有一个废弃项:

{
  "name": "spring.main.web-environment",
  "type": "java.lang.Boolean",
  "sourceType": "org.springframework.boot.SpringApplication",
  "description": "Run the application in a web environment (auto-detected by default).",
  "deprecation": {
    "replacement": "spring.main.web-application-type",
    "since": "2.0.0"
  }
}

spring.main.web-environment 自 2.0.0 起废弃,替代品就是 spring.main.web-application-type。 但注意:这个废弃项的元数据还在,实际绑定大概率已经不存在了(ApplicationProperties 里没有 webEnvironment 字段)。 如果你在项目里写了 spring.main.web-environment=false,它不会报错,也不会生效—— 这是一个"静默失效"的坑。(未核实是否仍有兼容层做映射,按字段缺失推断为不生效。)

生效时机(关键):

private ConfigurableEnvironment prepareEnvironment(...) {
    ConfigurableEnvironment environment = getOrCreateEnvironment();   // 用的是【推断值】
    configureEnvironment(environment, applicationArguments.getSourceArgs());
    ConfigurationPropertySources.attach(environment);
    listeners.environmentPrepared(bootstrapContext, environment);     // 配置文件在这里被读取
    ApplicationInfoPropertySource.moveToEnd(environment);
    DefaultPropertiesPropertySource.moveToEnd(environment);
    Assert.state(!environment.containsProperty("spring.main.environment-prefix"),
            "Environment prefix cannot be set via properties.");
    bindToSpringApplication(environment);                             // ★ 覆盖 webApplicationType
    if (!this.isCustomEnvironment) {
        EnvironmentConverter environmentConverter = new EnvironmentConverter(getClassLoader());
        environment = environmentConverter.convertEnvironmentIfNecessary(environment, deduceEnvironmentClass());
    }                                                                 // ★ 用覆盖后的值决定环境类型
    ConfigurationPropertySources.attach(environment);
    return environment;
}

一句话:配置文件的覆盖发生在 createApplicationContext() 之前,所以上下文类型也会被改。

4.4 路径四:@SpringBootTest(webEnvironment = ...)

测试场景下,SpringBootTestContextBootstrapper 会主动读取这个属性来决定 WebEnvironment:

// SpringBootTestContextBootstrapper.java:163
return binder.bind("spring.main.web-application-type", Bindable.of(WebApplicationType.class)) ...

(本行上下文未完整读取,仅按行内容说明:bootstrapper 用 Binder 直接读这个属性。)

而 WebEnvironment.NONE 会强制注入一个属性:

// SpringBootTestContextBootstrapper.java:291
propertySourceProperties.add("spring.main.web-application-type=none");

这就是前面 smoke test 里 WebEnvironmentNoneOverridesWebApplicationTypeTests 的机制: application.properties 里写了 reactive,但 @SpringBootTest(webEnvironment = WebEnvironment.NONE) 在属性源里加了一条 spring.main.web-application-type=none,把它覆盖掉。

SpringBootContextLoader 里还有一句注释说明了为什么需要这样处理:

// SpringBootContextLoader.java:445
* was registered. This can occur when spring.main.web-application-type is set to ...

(原注释未完整读取,仅引用方向。)

4.5 路径五:SpringBootServletInitializer(WAR 部署)

WAR 部署时用 SpringBootServletInitializer,它内部有一个自己的 ApplicationContextFactory(第 12 节详述)。 它不会改 WebApplicationType,但会绕过 ApplicationContextFactory 的默认 SPI 链。

4.6 五条路径的优先级

从"最早"到"最晚"(后者覆盖前者):

1. WebApplicationType.deduce()          (构造期,初始值)
       ↓
2. setWebApplicationType / builder.web() (构造后 run 前,编程式)
       ↓
3. spring.main.web-application-type      (prepareEnvironment 期,配置式)
       ↓
4. @SpringBootTest webEnvironment        (测试期,注入属性,等价于 3)
       ↓
5. SpringBootServletInitializer 的自定义工厂(WAR 期,绕过而非覆盖)

注意第 2 条和第 3 条的关系:如果你在代码里 setWebApplicationType(SERVLET), 又在 application.yaml 里写 spring.main.web-application-type: none,最终是 none——配置赢。 因为 bindToSpringApplication 是无条件执行的。

这是一个很反直觉的行为:编程式设置会被配置文件覆盖。 如果你希望编程式设置不可被覆盖,唯一的办法是自定义 ApplicationContextFactory(第 13 节)。


5. ApplicationContextFactory 接口逐行

文件坐标:core/spring-boot/src/main/java/org/springframework/boot/ApplicationContextFactory.java 作者:Andy Wilkinson、Phillip Webb @since:2.4.0

5.1 类注释里的关键约束

/**
 * Strategy interface for creating the {@link ConfigurableApplicationContext} used by a
 * {@link SpringApplication}. Created contexts should be returned in their default form,
 * with the {@code SpringApplication} responsible for configuring and refreshing the
 * context.
 *
 * @author Andy Wilkinson
 * @author Phillip Webb
 * @since 2.4.0
 */
@FunctionalInterface
public interface ApplicationContextFactory {

"Created contexts should be returned in their default form"—— 工厂不能做这些事:

  • ❌ 不能设置 Environment(SpringApplication.prepareContext() 里会 context.setEnvironment(environment))
  • ❌ 不能注册 Bean 定义(那是 load(context, sources) 的事)
  • ❌ 不能 refresh()(那是 refreshContext(context) 的事)
  • ❌ 不能添加 ApplicationContextInitializer

工厂唯一职责:new 出一个空的、默认配置的上下文对象。

这个约束是必要的,因为 SpringApplication 在拿到上下文后还要做一堆事 (prepareContext 里设置环境、应用 initializer、注册 springApplicationArguments 单例、加 LazyInitializationBeanFactoryPostProcessor 等)。 如果工厂自己 refresh() 了,这些就全乱了。

5.2 四个方法,只有一个是抽象的

// ① 常量
ApplicationContextFactory DEFAULT = new DefaultApplicationContextFactory();

// ② default:环境类型
default @Nullable Class<? extends ConfigurableEnvironment> getEnvironmentType(
        @Nullable WebApplicationType webApplicationType) {
    return null;
}

// ③ default:环境实例
default @Nullable ConfigurableEnvironment createEnvironment(@Nullable WebApplicationType webApplicationType) {
    return null;
}

// ④ 抽象:上下文
@Nullable ConfigurableApplicationContext create(@Nullable WebApplicationType webApplicationType);

@FunctionalInterface 允许多个 default 方法,只要恰好一个抽象方法。这里就是 create(...)。

所以你可以这样写:

application.setApplicationContextFactory((type) -> new MyCustomContext());

三个方法的职责分工:

方法@since返回 null 的含义谁调用
getEnvironmentType2.6.14"我不关心环境类型"deduceEnvironmentClass()
createEnvironment2.6.14"我不造环境"getOrCreateEnvironment()
create2.4.0"我造不了这种类型的上下文"createApplicationContext()

注意 ② 和 ③ 是 2.6.14 才加的——这是一个小版本里加的(2.6.x 的补丁版本), 说明它是一个缺陷修复而非新特性。修复的是什么?

在 2.6.14 之前,SpringApplication 只能通过 ApplicationContextFactory 拿到上下文, 而 Environment 的类型是独立推断的(老版本有一套 webEnvironment 布尔量 + ConfigurableWebEnvironment 判断)。 这导致一个不一致:上下文是 SERVLET 的,但环境不是 StandardServletEnvironment。

2.6.14 把"环境类型"也纳入同一个 SPI,保证二者同源。

5.3 getEnvironmentType 的 Javadoc 里藏着转换语义

/**
 * Return the {@link Environment} type expected to be set on the
 * {@link #create(WebApplicationType) created} application context. The result of this
 * method can be used to convert an existing environment instance to the correct type.
 * @param webApplicationType the web application type or {@code null}
 * @return the expected application context type or {@code null} to use the default
 * @since 2.6.14
 */

"The result of this method can be used to convert an existing environment instance to the correct type"—— 这直接说明了为什么需要它:不是为了"造",而是为了"转"。

EnvironmentConverter 就是干这个的(第 8 节详述)。

5.4 createEnvironment 的强约束

/**
 * Create a new {@link Environment} to be set on the
 * {@link #create(WebApplicationType) created} application context. The result of this
 * method must match the type returned by
 * {@link #getEnvironmentType(WebApplicationType)}.
 * ...
 */

"must match the type returned by getEnvironmentType(...)"—— 这是 Javadoc 层面的契约,没有运行时校验。

如果你自定义工厂时返回了不匹配的类型(比如 getEnvironmentType 返回 ApplicationServletEnvironment.class 但 createEnvironment 返回 new ApplicationEnvironment()),不会报错, 但 EnvironmentConverter.convertEnvironmentIfNecessary 会做 type.equals(environment.getClass()) 判断, 发现不匹配就会再转换一次——相当于你的 createEnvironment 白干了,但结果还是对的。

这是 Boot 里常见的"契约靠 Javadoc、行为靠兜底"的设计: 违反契约不会崩,只是多走一步。

5.5 两个静态工厂方法

/**
 * Creates an {@code ApplicationContextFactory} that will create contexts by
 * instantiating the given {@code contextClass} through its primary constructor.
 * @param contextClass the context class
 * @return the factory that will instantiate the context class
 * @see BeanUtils#instantiateClass(Class)
 */
static ApplicationContextFactory ofContextClass(Class<? extends ConfigurableApplicationContext> contextClass) {
    return of(() -> BeanUtils.instantiateClass(contextClass));
}

/**
 * Creates an {@code ApplicationContextFactory} that will create contexts by calling
 * the given {@link Supplier}.
 * @param supplier the context supplier, for example
 * {@code AnnotationConfigApplicationContext::new}
 * @return the factory that will instantiate the context class
 */
static ApplicationContextFactory of(Supplier<ConfigurableApplicationContext> supplier) {
    return (webApplicationType) -> supplier.get();
}

of(...) 的实现极其简单——忽略 webApplicationType 参数,无条件调 supplier.get()。

这意味着:

用 ApplicationContextFactory.of(...) 创建的工厂,对所有 WebApplicationType 返回同一种上下文。

这是一个很危险的默认行为。举例:

application.setApplicationContextFactory(
    ApplicationContextFactory.ofContextClass(AnnotationConfigApplicationContext.class));

这样设置后,即使 WebApplicationType 是 SERVLET,上下文也是纯 AnnotationConfigApplicationContext—— 没有 servlet 能力,ServletWebServerFactory bean 会被创建但没人用它, 最终你会得到一个"配了 web 但没起服务器"的诡异状态(不报错,端口没监听)。

注意 ofContextClass 用的是 BeanUtils.instantiateClass(Class),它调用的是"primary constructor"。 在 Boot 4.x 里这通常就是无参构造器;如果类有多个构造器,instantiateClass 会尝试用唯一的构造器 (Kotlin 主构造器也支持)。如果类没有无参构造器,会抛 BeanInstantiationException。


6. DefaultApplicationContextFactory 逐行

文件坐标:core/spring-boot/src/main/java/org/springframework/boot/DefaultApplicationContextFactory.java 可见性:class(包私有)——你不能直接 new 它,只能通过 ApplicationContextFactory.DEFAULT 拿到。

6.1 全文

class DefaultApplicationContextFactory implements ApplicationContextFactory {

	// Method reference is not detected with correct nullability
	@SuppressWarnings("NullAway")
	@Override
	public @Nullable Class<? extends ConfigurableEnvironment> getEnvironmentType(
			@Nullable WebApplicationType webApplicationType) {
		return getFromSpringFactories(webApplicationType, ApplicationContextFactory::getEnvironmentType, null);
	}

	// Method reference is not detected with correct nullability
	@SuppressWarnings("NullAway")
	@Override
	public @Nullable ConfigurableEnvironment createEnvironment(@Nullable WebApplicationType webApplicationType) {
		return getFromSpringFactories(webApplicationType, ApplicationContextFactory::createEnvironment, null);
	}

	// Method reference is not detected with correct nullability
	@SuppressWarnings("NullAway")
	@Override
	public ConfigurableApplicationContext create(@Nullable WebApplicationType webApplicationType) {
		try {
			return getFromSpringFactories(webApplicationType, ApplicationContextFactory::create,
					this::createDefaultApplicationContext);
		}
		catch (Exception ex) {
			throw new IllegalStateException("Unable create a default ApplicationContext instance, "
					+ "you may need a custom ApplicationContextFactory", ex);
		}
	}
    // ...
}

6.2 那个泛型魔法:getFromSpringFactories

@Contract("_, _, !null -> !null")
private <T> @Nullable T getFromSpringFactories(@Nullable WebApplicationType webApplicationType,
        BiFunction<ApplicationContextFactory, @Nullable WebApplicationType, @Nullable T> action,
        @Nullable Supplier<T> defaultResult) {
    for (ApplicationContextFactory candidate : SpringFactoriesLoader.loadFactories(ApplicationContextFactory.class,
            getClass().getClassLoader())) {
        T result = action.apply(candidate, webApplicationType);
        if (result != null) {
            return result;
        }
    }
    return (defaultResult != null) ? defaultResult.get() : null;
}

这是全文最精妙的一段。它把"遍历 SPI 链 + 执行某个动作 + 取第一个非空结果 + 兜底" 抽成了一个泛型方法,三个 public 方法共用它,只是传入的 action 不同。

参数含义:

参数类型含义
webApplicationType@Nullable WebApplicationType传给每个候选的参数
actionBiFunction<Factory, Type, T>对每个候选执行什么动作
defaultResult@Nullable Supplier<T>全部候选都返回 null 时的兜底

三次调用:

getFromSpringFactories(type, ApplicationContextFactory::getEnvironmentType, null);
//                                                                          ↑ 无兜底 → 可能返回 null

getFromSpringFactories(type, ApplicationContextFactory::createEnvironment, null);
//                                                                          ↑ 无兜底 → 可能返回 null

getFromSpringFactories(type, ApplicationContextFactory::create, this::createDefaultApplicationContext);
//                                                              ↑ 有兜底 → 永不返回 null(除非抛异常)

为什么前两个没有兜底,第三个有?

  • 环境类型/环境实例:SpringApplication 自己有兜底(ApplicationEnvironment.class / new ApplicationEnvironment()), 见 1.1 节的代码。所以这里返回 null 是合法的、被期待的。
  • 上下文:SpringApplication.createApplicationContext() 对 null 的态度是 Assert.state(context != null, ...) 直接炸。 所以 DefaultApplicationContextFactory 必须自己保证不返回 null → 必须有兜底。

这个分工体现了"兜底责任归谁"的清晰边界: 调用方能兜底的,SPI 就不用兜;调用方不能兜底的,SPI 必须兜。

6.3 @Contract("_, _, !null -> !null")

@Contract("_, _, !null -> !null")

这是 IntelliJ IDEA 的 @Contract 注解(来自 org.jetbrains.annotations,此处 import 是 org.springframework.lang.Contract—— Spring 自己的版本,语义相同)。

含义:"如果第三个参数不是 null,则返回值不是 null。"

有了这个契约,IDE 就能推断: create() 里传的是 this::createDefaultApplicationContext(非 null),所以返回值非 null, 于是 create() 方法声明返回 ConfigurableApplicationContext(不带 @Nullable)就不会被静态分析工具报警。

对比 getEnvironmentType() 和 createEnvironment():它们传的 defaultResult 是 null, 所以 @Contract 不适用,返回类型标注为 @Nullable。

这是一个用注解让 nullability 静态可验证的经典手法。

6.4 AOT 双分支:为什么有两个上下文类

private ConfigurableApplicationContext createDefaultApplicationContext() {
    if (!AotDetector.useGeneratedArtifacts()) {
        return new AnnotationConfigApplicationContext();
    }
    return new GenericApplicationContext();
}

AotDetector.useGeneratedArtifacts() 来自 Spring Framework(org.springframework.aot.AotDetector), 它检测当前是否运行在 AOT 生成的产物中(即 native-image 或者 JVM 上跑 AOT 生成的初始化代码)。

为什么 AOT 下要用 GenericApplicationContext 而不是 AnnotationConfigApplicationContext?

AnnotationConfigApplicationContext 的额外能力是:

  • 构造时创建 AnnotatedBeanDefinitionReader
  • 构造时创建 ClassPathBeanDefinitionScanner
  • 支持 register(Class...)、scan(String...) 这类注解驱动的 Bean 定义注册

而 AOT 模式下,所有的 Bean 定义已经在编译期生成好了, 由 AOT 生成的 xxx__ApplicationContextInitializer 在运行时直接注册 BeanDefinition, 不再需要运行时扫描和注解解析。

所以 AOT 下用更朴素的 GenericApplicationContext,可以:

  1. 避免初始化 ClassPathBeanDefinitionScanner(AOT 下无用,且是 native image 的体积负担)
  2. 避免 AnnotatedBeanDefinitionReader 加载一堆条件注解处理器

同样的模式出现在两个 web 工厂里(第 7 节会看到):

// ServletWebServerApplicationContextFactory
private ConfigurableApplicationContext createContext() {
    if (!AotDetector.useGeneratedArtifacts()) {
        return new AnnotationConfigServletWebServerApplicationContext();
    }
    return new ServletWebServerApplicationContext();
}

// ReactiveWebServerApplicationContextFactory
private ConfigurableApplicationContext createContext() {
    if (!AotDetector.useGeneratedArtifacts()) {
        return new AnnotationConfigReactiveWebServerApplicationContext();
    }
    return new ReactiveWebServerApplicationContext();
}

三处完全相同的模式。命名规律也一致:AnnotationConfigXxx(非 AOT)vs Xxx(AOT)。

6.5 异常包装

catch (Exception ex) {
    throw new IllegalStateException("Unable create a default ApplicationContext instance, "
            + "you may need a custom ApplicationContextFactory", ex);
}

注意异常类型:IllegalStateException(JDK 标准异常),不是 Boot 的 ApplicationContextException。

为什么?我推测的原因是(推测,未核实): 这是在 createApplicationContext() 的早期,此刻还没有 ApplicationContext 可以用于 FailureAnalyzer 的上下文, 且这个错误的性质是"配置错误"而非"应用启动失败",用一个通用的 JDK 异常表达更合适。

另外注意错误信息本身:"you may need a custom ApplicationContextFactory"—— 这是一条行动建议,符合第 16 章讲的 FailureAnalysis 的 "description + action" 结构, 只不过这里是硬编码在异常消息里,而不是通过 FailureAnalyzer 生成。

注意这里没有对应的 FailureAnalyzer。所以这个错误不会走 handleRunFailure 的分析链路, 只会打出原始堆栈。这是 Boot 诊断体系里一个"未被 FailureAnalyzer 覆盖"的角落。 原因可能是它太罕见了(只有自定义工厂且工厂返回 null 才会触发)。

6.6 三次 @SuppressWarnings("NullAway")

// Method reference is not detected with correct nullability
@SuppressWarnings("NullAway")

三个方法上都有,注释说 "Method reference is not detected with correct nullability"。

NullAway 是 Uber 开源的 nullability 静态检查工具(基于 Error Prone), Boot 在构建中启用了它(第 02 章讲过构建体系)。

问题在于: ApplicationContextFactory::getEnvironmentType 这个方法引用的签名是 @Nullable Class<...> getEnvironmentType(@Nullable WebApplicationType), 但 NullAway 在推断 BiFunction<ApplicationContextFactory, @Nullable WebApplicationType, @Nullable T> 的匹配时, 无法正确传播 @Nullable——它可能认为方法引用返回的是非 null,从而误报。

这条注释是工具局限性的诚实记录,也是 Boot 代码里"注释解释为什么 suppress"的范例。 对比第 15、16 章见过的其他 suppress,Boot 的惯例是:每个 @SuppressWarnings 都要有注释说明原因。


7. spring.factories 里的两个 Web 上下文工厂

7.1 注册文件全文

文件坐标:module/spring-boot-web-server/src/main/resources/META-INF/spring.factories

# Application Context Factories
org.springframework.boot.ApplicationContextFactory=\
org.springframework.boot.web.server.reactive.context.ReactiveWebServerApplicationContextFactory,\
org.springframework.boot.web.server.servlet.context.ServletWebServerApplicationContextFactory

# Application Context Initializers
org.springframework.context.ApplicationContextInitializer=\
org.springframework.boot.web.server.context.ServerPortInfoApplicationContextInitializer

# Failure Analyzers
org.springframework.boot.diagnostics.FailureAnalyzer=\
org.springframework.boot.web.server.PortInUseFailureAnalyzer,\
org.springframework.boot.web.server.context.MissingWebServerFactoryBeanFailureAnalyzer

# HTTP Server LocalTestWebServer Providers
org.springframework.boot.test.http.server.LocalTestWebServer$Provider=\
org.springframework.boot.web.server.reactive.context.ReactiveWebServerApplicationContextLocalTestWebServerProvider,\
org.springframework.boot.web.server.servlet.context.ServletWebServerApplicationContextLocalTestWebServerProvider

# Spring Test Context Customizer Factories
org.springframework.test.context.ContextCustomizerFactory=\
org.springframework.boot.web.server.context.SpringBootTestRandomPortContextCustomizerFactory

一个文件里注册了 5 类 SPI,全部与 web server 相关。这个模块(spring-boot-web-server)是 4.x 新增的抽象层—— 它不依赖任何具体服务器(Tomcat/Jetty/Netty 都不依赖),只定义抽象 + 通用设施。

这是 4.x 模块化改造的又一个证据:把"web 服务器抽象"从 spring-boot 核心里剥离出来, 成为一个独立模块,具体服务器实现再各自依赖它。

模块拓扑(本仓库实测):

  • module/spring-boot-web-server:92 个主源码文件
  • module/spring-boot-tomcat:46 个
  • module/spring-boot-jetty:45 个
  • module/spring-boot-reactor-netty:18 个
  • module/spring-boot-webmvc:48 个
  • module/spring-boot-webflux:51 个

7.2 ReactiveWebServerApplicationContextFactory

文件坐标:module/spring-boot-web-server/src/main/java/.../reactive/context/ReactiveWebServerApplicationContextFactory.java 可见性:class(包私有)

class ReactiveWebServerApplicationContextFactory implements ApplicationContextFactory {

	@Override
	public @Nullable Class<? extends ConfigurableEnvironment> getEnvironmentType(
			@Nullable WebApplicationType webApplicationType) {
		return (webApplicationType != WebApplicationType.REACTIVE) ? null : ApplicationReactiveWebEnvironment.class;
	}

	@Override
	public @Nullable ConfigurableEnvironment createEnvironment(@Nullable WebApplicationType webApplicationType) {
		return (webApplicationType != WebApplicationType.REACTIVE) ? null : new ApplicationReactiveWebEnvironment();
	}

	@Override
	public @Nullable ConfigurableApplicationContext create(@Nullable WebApplicationType webApplicationType) {
		return (webApplicationType != WebApplicationType.REACTIVE) ? null : createContext();
	}

	private ConfigurableApplicationContext createContext() {
		if (!AotDetector.useGeneratedArtifacts()) {
			return new AnnotationConfigReactiveWebServerApplicationContext();
		}
		return new ReactiveWebServerApplicationContext();
	}

}

7.3 ServletWebServerApplicationContextFactory

class ServletWebServerApplicationContextFactory implements ApplicationContextFactory {

	@Override
	public @Nullable Class<? extends ConfigurableEnvironment> getEnvironmentType(
			@Nullable WebApplicationType webApplicationType) {
		return (webApplicationType != WebApplicationType.SERVLET) ? null : ApplicationServletEnvironment.class;
	}

	@Override
	public @Nullable ConfigurableEnvironment createEnvironment(@Nullable WebApplicationType webApplicationType) {
		return (webApplicationType != WebApplicationType.SERVLET) ? null : new ApplicationServletEnvironment();
	}

	@Override
	public @Nullable ConfigurableApplicationContext create(@Nullable WebApplicationType webApplicationType) {
		return (webApplicationType != WebApplicationType.SERVLET) ? null : createContext();
	}

	private ConfigurableApplicationContext createContext() {
		if (!AotDetector.useGeneratedArtifacts()) {
			return new AnnotationConfigServletWebServerApplicationContext();
		}
		return new ServletWebServerApplicationContext();
	}

}

7.4 ★ 一个极易读错的写法:!= 而不是 ==

注意这两个工厂的所有判断都写成:

return (webApplicationType != WebApplicationType.REACTIVE) ? null : ...;

而不是更自然的:

return (webApplicationType == WebApplicationType.REACTIVE) ? ... : null;

这两种写法在 webApplicationType == null 时行为完全不同:

写法type == null 时type == REACTIVE 时type == SERVLET 时
!= REACTIVE ? null : Xnull("我不处理")Xnull
== REACTIVE ? X : nullnullXnull

等等——看起来是一样的?让我仔细算:

  • != REACTIVE ? null : X

    • type = null:null != REACTIVE → true → 返回 null ✓
    • type = REACTIVE:false → 返回 X ✓
    • type = SERVLET:true → null ✓
  • == REACTIVE ? X : null

    • type = null:false → null ✓
    • type = REACTIVE:true → X ✓
    • type = SERVLET:false → null ✓

两种情况结果相同。因为 Java 的 == 对枚举引用是安全的引用比较,null == X 恒为 false。

那么为什么 Boot 要用 != 的写法?我一开始以为这里有 null 相关的语义差异,这个推测是错的。

真正的理由(我能确认的部分):这是一种"守卫式"(guard clause)写法, 把"不符合条件"的情况放在前面立即返回,符合 Boot 一贯的 early-return 风格。 它在语义上与 == 版本完全等价。

⚠️ 纠错记录:我在第一次阅读这段代码时,认为 != 写法是为了处理 webApplicationType == null 的特殊情况 (因为 ApplicationContextFactory.create 的参数标注了 @Nullable)。 经过逐值推演后确认:两者在包括 null 在内的所有输入下行为一致。 我保留这段纠错过程,因为它说明了一个常见陷阱—— 看到 != null 之外的 != 常量比较,不要自动脑补 null 语义。 判断依据必须是真值表,不是直觉。 (本仓库中 @Nullable WebApplicationType 参数确实可以是 null,但两个工厂对 null 的处理结果与 == 写法一致:返回 null。)

不过,@Nullable 参数这个事实本身很重要:SpringApplication 在 webApplicationType 为 null 时会走到哪?

ApplicationProperties.getWebApplicationType() 返回 @Nullable WebApplicationType。 构造期 deduce() 必然返回非 null,所以正常流程下不会是 null。 但如果有人通过反射或 Binder 把它设成 null(比如 spring.main.web-application-type= 空值), 那么 create(null) → 两个 web 工厂都返回 null → createDefaultApplicationContext() → AnnotationConfigApplicationContext。

即:webApplicationType = null 等价于 NONE(都得到非 web 上下文)。 这是一个未被文档化但可以从代码推出的事实。

7.5 注册顺序为什么是 Reactive 先、Servlet 后

org.springframework.boot.ApplicationContextFactory=\
org.springframework.boot.web.server.reactive.context.ReactiveWebServerApplicationContextFactory,\
org.springframework.boot.web.server.servlet.context.ServletWebServerApplicationContextFactory

Reactive 在前,Servlet 在后。顺序有影响吗?

没有。因为两个工厂的 create() 是互斥的:

  • type == REACTIVE → Reactive 工厂返回上下文,Servlet 工厂根本不会被问到(短路)
  • type == SERVLET → Reactive 工厂返回 null(不问了),Servlet 工厂返回上下文
  • type == NONE → 都返回 null → 兜底

这与 Deducer 的情况不同:Deducer 的 @Order 是有意义的(MVC 与 WebFlux 可以同时命中), 而 ApplicationContextFactory 的注册顺序无意义(不可能同时命中)。

方法论:判断一个 SPI 列表的顺序是否重要,看它的多个实现是否可能"同时命中"。 Deducer 可能同时命中(都有 web 依赖)→ 顺序重要,靠 @Order。 ApplicationContextFactory 不可能同时命中(type 唯一)→ 顺序不重要,靠声明顺序就够。 同理,第 16 章讲过的 FailureAnalyzer 列表是按 getOrder() 排的——那是因为多个 analyzer 都能处理同一个异常的父类型。

7.6 两个上下文类的继承体系

                    GenericApplicationContext
                              │
        ┌─────────────────────┼──────────────────────┐
        │                     │                      │
AnnotationConfig       GenericWebApplicationContext  GenericReactiveWebApplicationContext
ApplicationContext      (org.springframework.web     (org.springframework.boot.web
        │                .context.support)           .context.reactive)
        │                     │                      │
        │          ServletWebServerApplication-  ReactiveWebServerApplicationContext
        │          Context                           │
        │          (spring-boot-web-server)     AnnotationConfigReactiveWebServer
        │                     ▲                 ApplicationContext
        │                     │                      ▲
        │          AnnotationConfigServletWeb-       │
        │          ServerApplicationContext          │
        │                     ▲                      │
        └─────────────────────┴──────────────────────┘

(非 AOT 路径:AnnotationConfigServletWebServerApplicationContext / AnnotationConfigReactiveWebServerApplicationContext; AOT 路径:父类型 ServletWebServerApplicationContext / ReactiveWebServerApplicationContext。)

关键接口:

public interface WebServerApplicationContext extends ApplicationContext {
	int GRACEFUL_SHUTDOWN_PHASE = SmartLifecycle.DEFAULT_PHASE - 1024;
	int START_STOP_LIFECYCLE_PHASE = GRACEFUL_SHUTDOWN_PHASE - 1024;

	@Nullable WebServer getWebServer();
	@Nullable String getServerNamespace();
	// ...
}

两个 phase 常量:

  • SmartLifecycle.DEFAULT_PHASE = Integer.MAX_VALUE(即 2147483647)
  • GRACEFUL_SHUTDOWN_PHASE = Integer.MAX_VALUE - 1024
  • START_STOP_LIFECYCLE_PHASE = Integer.MAX_VALUE - 2048

启动时 phase 小的先启动,停止时 phase 大的先停止。

所以停止顺序是:webServerGracefulShutdown(MAX-1024)先停 → webServerStartStop(MAX-2048)后停。 这正是"先优雅停机(停止接收新请求、等活跃请求完成),再真正停服务器"的顺序。 (第 18 章会详细展开这两个 Lifecycle。)


8. Environment 的三条创建路径与 EnvironmentConverter

8.1 路径总览

                  ┌─────────────────────────────────────────┐
                  │ getOrCreateEnvironment()                │
                  ├─────────────────────────────────────────┤
   ① environment != null ?  ──yes──▶ 直接返回(用户注入)
                  │ no
                  ▼
   ② applicationContextFactory.createEnvironment(type)
                  │
        ┌─────────┴──────────┐
    非 null                null
        │                    │
        ▼                    ▼
     返回          factory != DEFAULT ?
                       │yes        │no
                       ▼           ▼
        DEFAULT.createEnvironment(type)   ③ new ApplicationEnvironment()
                       │
                   非 null → 返回 / null → ③

然后:

              ┌──────────────────────────────────────────┐
              │ if (!isCustomEnvironment)                │
              │   environment = converter.               │
              │     convertEnvironmentIfNecessary(        │
              │       environment, deduceEnvironmentClass())│
              └──────────────────────────────────────────┘

8.2 deduceEnvironmentClass()

private Class<? extends ConfigurableEnvironment> deduceEnvironmentClass() {
    WebApplicationType webApplicationType = this.properties.getWebApplicationType();
    Class<? extends ConfigurableEnvironment> environmentType = this.applicationContextFactory
        .getEnvironmentType(webApplicationType);
    if (environmentType == null && this.applicationContextFactory != ApplicationContextFactory.DEFAULT) {
        environmentType = ApplicationContextFactory.DEFAULT.getEnvironmentType(webApplicationType);
    }
    return (environmentType != null) ? environmentType : ApplicationEnvironment.class;
}

注意这条兜底链:

  1. 先问用户自定义的 factory
  2. 如果自定义 factory 返回 null,且它不是 DEFAULT → 再问 DEFAULT
  3. 如果还是 null → ApplicationEnvironment.class

第二步的 != ApplicationContextFactory.DEFAULT 判断很关键。 如果用户的 factory 就是 DEFAULT,那第一步和第二步是同一个对象,没必要问两次(虽然问两次也没害处)。

为什么要有第二步? 因为用户自定义 factory 往往是"只处理我关心的类型",其他类型它返回 null。 此时如果不回退到 DEFAULT,那么非 web 场景就会退化成 ApplicationEnvironment—— 这恰好是对的,但 SERVLET/REACTIVE 场景就会错(用户 factory 只处理 REACTIVE,SERVLET 时返回 null, 没有第二步就退化成 ApplicationEnvironment,尽管上下文是 servlet 的)。 所以第二步是**让自定义工厂能"部分覆盖"而不"全盘接管"**的关键设计。

8.3 EnvironmentConverter:转换而不是重建

文件坐标:core/spring-boot/src/main/java/org/springframework/boot/EnvironmentConverter.java 可见性:final class(包私有 final)

final class EnvironmentConverter {

	private static final String CONFIGURABLE_WEB_ENVIRONMENT_CLASS = "org.springframework.web.context.ConfigurableWebEnvironment";

	private static final Set<String> SERVLET_ENVIRONMENT_SOURCE_NAMES;

	static {
		Set<String> names = new HashSet<>();
		names.add(StandardServletEnvironment.SERVLET_CONTEXT_PROPERTY_SOURCE_NAME);
		names.add(StandardServletEnvironment.SERVLET_CONFIG_PROPERTY_SOURCE_NAME);
		names.add(StandardServletEnvironment.JNDI_PROPERTY_SOURCE_NAME);
		SERVLET_ENVIRONMENT_SOURCE_NAMES = Collections.unmodifiableSet(names);
	}

	private final ClassLoader classLoader;

	EnvironmentConverter(ClassLoader classLoader) {
		this.classLoader = classLoader;
	}

	ConfigurableEnvironment convertEnvironmentIfNecessary(ConfigurableEnvironment environment,
			Class<? extends ConfigurableEnvironment> type) {
		if (type.equals(environment.getClass())) {
			return environment;
		}
		return convertEnvironment(environment, type);
	}
    // ...
}

要点 1:type.equals(environment.getClass()) 是精确类型比较

不是 isAssignableFrom,是 equals。意味着: 如果目标是 ApplicationEnvironment.class 而当前是 ApplicationEnvironment,直接返回。 但如果当前是 ApplicationEnvironment 的子类,也会触发转换。

要点 2:SERVLET_ENVIRONMENT_SOURCE_NAMES 三个名字

StandardServletEnvironment.SERVLET_CONTEXT_PROPERTY_SOURCE_NAME   // "servletContextInitParams"
StandardServletEnvironment.SERVLET_CONFIG_PROPERTY_SOURCE_NAME     // "servletConfigInitParams"
StandardServletEnvironment.JNDI_PROPERTY_SOURCE_NAME               // "jndiProperties"

这三个是 StandardServletEnvironment 在构造时自动添加的 PropertySource。

8.4 ★ 反直觉的地方:目标环境是 servlet 时保留 servlet 源

private ConfigurableEnvironment convertEnvironment(ConfigurableEnvironment environment,
        Class<? extends ConfigurableEnvironment> type) {
    ConfigurableEnvironment result = createEnvironment(type);
    result.setActiveProfiles(environment.getActiveProfiles());
    result.setConversionService(environment.getConversionService());
    copyPropertySources(environment, result);
    return result;
}

private void copyPropertySources(ConfigurableEnvironment source, ConfigurableEnvironment target) {
    removePropertySources(target.getPropertySources(), isServletEnvironment(target.getClass(), this.classLoader));
    for (PropertySource<?> propertySource : source.getPropertySources()) {
        if (!SERVLET_ENVIRONMENT_SOURCE_NAMES.contains(propertySource.getName())) {
            target.getPropertySources().addLast(propertySource);
        }
    }
}

private void removePropertySources(MutablePropertySources propertySources, boolean isServletEnvironment) {
    Set<String> names = new HashSet<>();
    for (PropertySource<?> propertySource : propertySources) {
        names.add(propertySource.getName());
    }
    for (String name : names) {
        if (!isServletEnvironment || !SERVLET_ENVIRONMENT_SOURCE_NAMES.contains(name)) {
            propertySources.remove(name);
        }
    }
}

这段逻辑的语义要仔细读:

removePropertySources(target, isServletEnvironment):清空 target 的 PropertySource。 但如果 target 是 servlet 环境,则保留那三个 servlet 源。

为什么? 因为 ApplicationServletEnvironment 在构造时(通过 StandardServletEnvironment)会添加 servletContextInitParams、servletConfigInitParams、jndiProperties。 如果全部清空,这三个源就丢了。但它们可能已经持有真实的 ServletContext 参数(WAR 部署场景),不能丢。

然后 copyPropertySources 的循环:跳过源里名字属于这三个的 PropertySource。

为什么跳过? 因为 target 里已经有了(刚保留下来的)。如果不跳过,就会重复添加—— 而 MutablePropertySources 里同名 PropertySource 只能有一个(addLast 会先 remove 再 add,导致顺序变化)。

这一步组合起来实现一个精确语义:

目标环境的 servlet 源(真实 ServletContext 相关的)保留 target 自己的; 源环境里的其他 PropertySource(命令行、系统属性、application.yaml 等)全部复制过来; 源环境里如果也有同名 servlet 源,不复制(因为那是源环境自己的,可能为空/不同)。

为什么"源环境里如果也有同名 servlet 源就不复制"?

设想:用户在 setEnvironment() 里塞了一个 ApplicationServletEnvironment(此时没有真实 ServletContext, 三个源都是空的)。然后转换到另一个 ApplicationServletEnvironment。 源里的 servletContextInitParams 是空壳,target 里的是真身。 跳过源的那个,保留 target 的,就是正确行为。

方法论提炼:remove... + for(...) if(!contains) add... 这个组合, 是"目标优先、源补充"的合并模式。 看到 remove 里带一个布尔守卫、循环里又带一个反向守卫, 立刻意识到:这是一次有条件的合并,条件是"名字属于某集合"。

8.5 isServletEnvironment 的字符串类名判断

private boolean isServletEnvironment(Class<?> conversionType, ClassLoader classLoader) {
    try {
        Class<?> webEnvironmentClass = ClassUtils.forName(CONFIGURABLE_WEB_ENVIRONMENT_CLASS, classLoader);
        return webEnvironmentClass.isAssignableFrom(conversionType);
    }
    catch (Throwable ex) {
        return false;
    }
}

注意 CONFIGURABLE_WEB_ENVIRONMENT_CLASS 是字符串("org.springframework.web.context.ConfigurableWebEnvironment"), 不是 ConfigurableWebEnvironment.class。

为什么? 因为 EnvironmentConverter 在 spring-boot 核心模块里, 而 ConfigurableWebEnvironment 在 spring-web 里——核心模块不能直接引用它(否则会引入硬依赖)。

用字符串 + ClassUtils.forName + catch (Throwable) return false 是标准的软依赖处理手法。 这与第 15 章讲的 @ConditionalOnClass 用字符串类名是同一个道理。

8.6 createEnvironment 的降级

private ConfigurableEnvironment createEnvironment(Class<? extends ConfigurableEnvironment> type) {
    try {
        Constructor<? extends ConfigurableEnvironment> constructor = type.getDeclaredConstructor();
        ReflectionUtils.makeAccessible(constructor);
        return constructor.newInstance();
    }
    catch (Exception ex) {
        return new ApplicationEnvironment();
    }
}

getDeclaredConstructor()(无参)+ makeAccessible + newInstance。

如果失败(比如目标类没有无参构造器、构造器抛异常),降级到 new ApplicationEnvironment()。

注意:这里是静默降级,不打印任何日志。 如果你自定义了一个 ApplicationContextFactory,返回的 getEnvironmentType() 是一个没有无参构造器的类, 那么你会静默地得到一个 ApplicationEnvironment,而不是你期望的类型。 排查这种问题的方法:在 deduceEnvironmentClass() 和 EnvironmentConverter.createEnvironment 上打断点。

这条符合第十二定律的变体:"降级路径存在 = 这里不会崩,但可能静默偏离预期"。

8.7 isCustomEnvironment 的作用

if (!this.isCustomEnvironment) {
    EnvironmentConverter environmentConverter = new EnvironmentConverter(getClassLoader());
    environment = environmentConverter.convertEnvironmentIfNecessary(environment, deduceEnvironmentClass());
}

isCustomEnvironment 是 SpringApplication 的一个 boolean 字段, 在 setEnvironment(ConfigurableEnvironment environment) 里被置为 true。

语义:如果用户显式设置了 Environment,就不要转换它。

这看起来有点矛盾——不转换的话,类型可能不匹配啊?

原因:用户显式设置 Environment 通常是在测试或嵌入式场景下, 此时用户明确知道自己要什么样的环境,转换反而会破坏用户的意图 (比如用户塞了一个自定义的 MockEnvironment,转换会把它变成 ApplicationEnvironment,Mock 就没了)。

但这样就产生了一个不一致的可能:WebApplicationType 是 SERVLET,上下文是 servlet 的, 而环境是 ApplicationEnvironment(非 web)。

这会不会出问题?在大多数情况下不会——ApplicationEnvironment 与 ApplicationServletEnvironment 的差异主要是三个 servlet PropertySource 和一些 ConfigurableWebEnvironment 标记。 第 25 章讲 Actuator 管理端口时会再碰到这个话题。

这就是 Boot 里典型的"用户显式意图 > 框架推断一致性"的取舍。


9. 三个 Application*Environment

9.1 三个类,三个相同的方法覆写

类坐标父类可见性
ApplicationEnvironmentcore/spring-boot/.../ApplicationEnvironment.javaStandardEnvironmentclass(包私有)
ApplicationServletEnvironmentcore/spring-boot/.../web/context/servlet/ApplicationServletEnvironment.javaStandardServletEnvironmentpublic
ApplicationReactiveWebEnvironmentmodule/spring-boot-web-server/.../reactive/context/ApplicationReactiveWebEnvironment.javaStandardReactiveWebEnvironmentclass(包私有)

三个类的方法体完全一样:

@Override
protected @Nullable String doGetActiveProfilesProperty() {
    return null;
}

@Override
protected @Nullable String doGetDefaultProfilesProperty() {
    return null;
}

@Override
protected ConfigurablePropertyResolver createPropertyResolver(MutablePropertySources propertySources) {
    return ConfigurationPropertySources.createPropertyResolver(propertySources);
}

9.2 doGetActiveProfilesProperty() 返回 null 的含义

这三个方法是 AbstractEnvironment 的模板方法:

  • doGetActiveProfilesProperty() → 返回"用来读取 active profiles 的属性名"
  • doGetDefaultProfilesProperty() → 返回"用来读取 default profiles 的属性名"

返回 null 意味着"禁用通过属性设置 profile"。

这在第 10 章(Profile 语义)已经讲过: spring.profiles.active 的读取是被 ConfigDataEnvironment 特殊处理的, 不是通过 AbstractEnvironment 的标准机制。

如果这里返回 "spring.profiles.active",那么 AbstractEnvironment.getActiveProfiles() 会直接去 PropertySources 里找这个属性——但此时 ConfigData 的 profile 迭代还没跑完, 会导致鸡生蛋问题(要读 profile 才能决定加载哪些配置文件,但配置文件里有 profile 定义)。

Boot 的解法是:禁用标准机制(返回 null),让 ConfigDataEnvironmentPostProcessor 独占 profile 的解析权, 解析完成后通过 environment.setActiveProfiles(...) 显式设置。

三个 Environment 类都这么做,说明这个"禁用"是 Boot 的统一策略,不分 web 类型。

交叉验证:ApplicationProperties 里也有一个类似的机制—— spring.main.environment-prefix 的 Assert.state(!environment.containsProperty(...), "Environment prefix cannot be set via properties.")。 同样是"某些事情不能通过属性配置"的硬约束。

9.3 createPropertyResolver 与 ConfigurationPropertySources

@Override
protected ConfigurablePropertyResolver createPropertyResolver(MutablePropertySources propertySources) {
    return ConfigurationPropertySources.createPropertyResolver(propertySources);
}

这是 Boot 对 Framework 的又一个"偷偷替换"。 AbstractEnvironment 默认创建的是 PropertySourcesPropertyResolver, Boot 换成了 ConfigurationPropertySources.createPropertyResolver(...) 返回的类型。

目的:让 environment.getProperty("someKey") 支持宽松绑定(relaxed binding)。 比如 environment.getProperty("serverPort") 能找到 server.port。

第 06 章(Environment 构建)详细讲过 ConfigurationPropertySources.attach(),这里是配套的第二步: attach() 往 Environment 里加了一个名为 configurationProperties 的 PropertySource, 而 createPropertyResolver 保证 getProperty 会正确地使用宽松绑定规则。

两个动作必须配对:

  • 只有 attach() 没有 createPropertyResolver → 宽松绑定不生效
  • 只有 createPropertyResolver 没有 attach() → resolver 找不到源

这也是为什么 Boot 要自定义三个 Environment 子类——为了注入这两件事。

9.4 StandardReactiveWebEnvironment 与 ConfigurableReactiveWebEnvironment

public class StandardReactiveWebEnvironment extends StandardEnvironment implements ConfigurableReactiveWebEnvironment {

	public StandardReactiveWebEnvironment() {
		super();
	}

	protected StandardReactiveWebEnvironment(MutablePropertySources propertySources) {
		super(propertySources);
	}

}

注意:这个类几乎是空的。它只是 StandardEnvironment + 一个标记接口 ConfigurableReactiveWebEnvironment。

对比 servlet 侧的 StandardServletEnvironment(来自 spring-web):

  • StandardServletEnvironment 有实质内容:构造时添加三个 servlet PropertySource,覆写 customizePropertySources;
  • StandardReactiveWebEnvironment 什么都不加。

为什么?因为响应式栈没有 ServletContext/ServletConfig/JNDI 这类容器提供的属性源。 它的存在纯粹是为了提供类型标记——让 instanceof ConfigurableReactiveWebEnvironment 判断成为可能。

哪里用到了这个判断?比如 ReactiveWebServerApplicationContext、ReactiveWebApplicationContext 的判定, 以及 Actuator 的 ManagementPortType 判断(第 25 章)。

这是一个"标记接口"设计的范例: 没有方法、没有字段,只为了 instanceof。 判断依据:类体为空 + 继承 + 实现接口,三者齐备 → 标记用途。

9.5 一个不对称:ApplicationServletEnvironment 是 public

三个类里只有 ApplicationServletEnvironment 是 public:

public class ApplicationServletEnvironment extends StandardServletEnvironment {

而 ApplicationEnvironment 和 ApplicationReactiveWebEnvironment 都是包私有。

为什么?因为 SpringBootServletInitializer(在 org.springframework.boot.web.servlet.support 包) 需要引用它——WAR 部署场景下要构造这个环境。

看第 12 节的代码,确实如此:

@Override
public @Nullable Class<? extends ConfigurableEnvironment> getEnvironmentType(
        @Nullable WebApplicationType webApplicationType) {
    return (webApplicationType != WebApplicationType.SERVLET) ? null : ApplicationServletEnvironment.class;
}

同理,ApplicationServletEnvironment 位于 core/spring-boot 的 org.springframework.boot.web.context.servlet 包, 而 ApplicationReactiveWebEnvironment 位于 module/spring-boot-web-server—— 跨模块的包私有就用不了,但它在自己的模块内被 ReactiveWebServerApplicationContextFactory 用,所以包私有够用。

判断一个 Boot 类为什么是 public:很多时候不是"给用户用的",而是"给另一个包的 Boot 内部代码用的"。 在 4.x 模块化之后,跨模块引用比跨包引用更需要 public。


10. 上下文创建:从 createApplicationContext() 到 refresh()

10.1 入口

protected ConfigurableApplicationContext createApplicationContext() {
    ConfigurableApplicationContext context = this.applicationContextFactory
        .create(this.properties.getWebApplicationType());
    Assert.state(context != null, "ApplicationContextFactory created null context");
    return context;
}

两行代码,两件事:

  1. 调工厂
  2. Assert.state(context != null, ...) —— 对 null 硬失败

注意这里的错误消息:"ApplicationContextFactory created null context"。 它与 DefaultApplicationContextFactory 里那个 IllegalStateException 的消息 ("Unable create a default ApplicationContext instance, you may need a custom ApplicationContextFactory")不同。

所以:

  • 如果你用的是 ApplicationContextFactory.DEFAULT → 永远不会到这个 Assert(DEFAULT 保证非 null,否则抛 IllegalStateException)
  • 如果你用的是自定义工厂且它返回了 null → 命中这个 Assert,抛 IllegalStateException("ApplicationContextFactory created null context")

通过错误消息就能区分是哪条路径失败。这是一个有用的诊断技巧。

10.2 create() 返回之后,refresh() 之前

createApplicationContext() 拿到的上下文是"default form"——空的。 在 refresh() 之前,prepareContext() 会做一大堆事:

private void prepareContext(DefaultBootstrapContext bootstrapContext, ConfigurableApplicationContext context,
        ConfigurableEnvironment environment, SpringApplicationRunListeners listeners,
        ApplicationArguments applicationArguments, @Nullable Banner printedBanner) {
    context.setEnvironment(environment);                    // ① 环境注入
    postProcessApplicationContext(context);                 // ② BeanNameGenerator / ResourceLoader / ConversionService
    addAotGeneratedInitializerIfNecessary(this.initializers);
    ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
    if (beanFactory instanceof AbstractAutowireCapableBeanFactory autowireCapableBeanFactory) {
        autowireCapableBeanFactory.setAllowCircularReferences(this.properties.isAllowCircularReferences());
        if (beanFactory instanceof DefaultListableBeanFactory listableBeanFactory) {
            listableBeanFactory.setAllowBeanDefinitionOverriding(this.properties.isAllowBeanDefinitionOverriding());
        }
    }
    applyInitializers(context);                             // ③ ApplicationContextInitializer
    listeners.contextPrepared(context);
    bootstrapContext.close(context);                        // ④ Bootstrap 上下文关闭(第 12 章)
    if (this.properties.isLogStartupInfo()) {
        logStartupInfo(context);
        logStartupProfileInfo(context);
    }
    beanFactory.registerSingleton("springApplicationArguments", applicationArguments);
    if (printedBanner != null) {
        beanFactory.registerSingleton("springBootBanner", printedBanner);
    }
    if (this.properties.isLazyInitialization()) {
        context.addBeanFactoryPostProcessor(new LazyInitializationBeanFactoryPostProcessor());
    }
    if (this.properties.isKeepAlive()) {
        context.addApplicationListener(new KeepAlive());
    }
    context.addBeanFactoryPostProcessor(new PropertySourceOrderingBeanFactoryPostProcessor(context));
    if (!AotDetector.useGeneratedArtifacts()) {
        Set<Object> sources = getAllSources();
        Assert.state(!ObjectUtils.isEmpty(sources), "No sources defined");
        load(context, sources.toArray(new Object[0]));      // ⑤ 加载 Bean 定义(第 13 章)
    }
    listeners.contextLoaded(context);
}

注意最后那个 if (!AotDetector.useGeneratedArtifacts()) ——

AOT 模式下不加载 sources! 因为 Bean 定义已经由 AOT 生成, 由 addAotGeneratedInitializerIfNecessary(this.initializers) 加进来的 initializer 负责注册。

这再次印证 6.4 节的结论:AOT 路径与非 AOT 路径是两条几乎独立的轨道。

10.3 为什么 AOT 下不能用 AnnotationConfig*

把三处 AOT 分支放在一起看:

// 非 web
if (!AotDetector.useGeneratedArtifacts()) return new AnnotationConfigApplicationContext();
return new GenericApplicationContext();

// servlet
if (!AotDetector.useGeneratedArtifacts()) return new AnnotationConfigServletWebServerApplicationContext();
return new ServletWebServerApplicationContext();

// reactive
if (!AotDetector.useGeneratedArtifacts()) return new AnnotationConfigReactiveWebServerApplicationContext();
return new ReactiveWebServerApplicationContext();

AnnotationConfig* 系列相比 Generic*/Servlet*/Reactive* 系列多出的能力:

能力由谁提供AOT 下是否需要
AnnotatedBeanDefinitionReader —— 注册 @Configuration 类AnnotationConfigApplicationContext 构造时创建❌(AOT 已生成 BeanDefinition)
ClassPathBeanDefinitionScanner —— 扫描 @Component同上❌(AOT 已生成,且 native 下扫描代价高)
register(Class...) / scan(String...) 便捷方法同上❌

而 GenericApplicationContext 提供的是最基础的: registerBeanDefinition、getBeanFactory、refresh。AOT 只需要这些。

另外还有一个体积考量:ClassPathBeanDefinitionScanner 会拉进 ASM 字节码读取相关的类。 在 native image 里,多一个可达类就多一份编译产物。用 GenericApplicationContext 能显著减少镜像体积。

10.4 AotDetector.useGeneratedArtifacts() 的机制

// org.springframework.aot.AotDetector(不在本仓库)
public static boolean useGeneratedArtifacts() {
    return System.getProperty(AOT_SYSTEM_PROPERTY) != null;  // 示意,未核实
}

⚠️ 未核实:AotDetector 在 Spring Framework 中,本仓库不含其源码。 上面的实现是示意性的。可确认的本仓库事实是:Boot 在至少 4 处(DefaultApplicationContextFactory、 两个 web 工厂、SpringApplication.prepareContext)调用这个静态方法做分支。 实际机制(我推测是检测某个系统属性或类是否存在)请查阅 Framework 源码。

一个可自行验证的实验:

System.out.println(AotDetector.useGeneratedArtifacts());  // 普通 JVM 运行应为 false

11. 第四个实现:WAR 部署的 WarDeploymentApplicationContextFactory

11.1 装配点

文件坐标:core/spring-boot/src/main/java/org/springframework/boot/web/servlet/support/SpringBootServletInitializer.java 类:private static final class WarDeploymentApplicationContextFactory

它不是通过 spring.factories 注册的,而是在 createRootApplicationContext 里直接装配:

protected @Nullable WebApplicationContext createRootApplicationContext(ServletContext servletContext) {
    SpringApplicationBuilder builder = createSpringApplicationBuilder();
    builder.main(getClass());
    ApplicationContext parent = getExistingRootWebApplicationContext(servletContext);
    if (parent != null) {
        getLogger().info("Root context already created (using as parent).");
        servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, null);
        builder.initializers(new ParentContextApplicationContextInitializer(parent));
    }
    builder.initializers(new ServletContextApplicationContextInitializer(servletContext));
    builder.contextFactory(new WarDeploymentApplicationContextFactory(servletContext));   // ★
    builder = configure(builder);
    builder.listeners(new WebEnvironmentPropertySourceInitializer(servletContext));
    SpringApplication application = builder.build();
    // ...
    application.setRegisterShutdownHook(false);
    return run(application);
}

即:SpringApplicationBuilder.contextFactory(...) 覆盖了默认的 ApplicationContextFactory.DEFAULT。

11.2 实现

private static final class WarDeploymentApplicationContextFactory implements ApplicationContextFactory {

    private final ServletContext servletContext;

    private WarDeploymentApplicationContextFactory(ServletContext servletContext) {
        this.servletContext = servletContext;
    }

    @Override
    public @Nullable Class<? extends ConfigurableEnvironment> getEnvironmentType(
            @Nullable WebApplicationType webApplicationType) {
        return (webApplicationType != WebApplicationType.SERVLET) ? null : ApplicationServletEnvironment.class;
    }

    @Override
    public ConfigurableEnvironment createEnvironment(@Nullable WebApplicationType webApplicationType) {
        return new ApplicationServletEnvironment();            // ★ 注意:忽略了 webApplicationType
    }

    @Override
    public ConfigurableApplicationContext create(@Nullable WebApplicationType webApplicationType) {
        return new AnnotationConfigServletWebServerApplicationContext() {
            @Override
            protected void onRefresh() {
                super.onRefresh();
                try {
                    new WebApplicationContextInitializer(this)
                        .initialize(WarDeploymentApplicationContextFactory.this.servletContext);
                }
                catch (ServletException ex) {
                    throw new ApplicationContextException("Cannot initialize servlet context", ex);
                }
            }
        };
    }

}

11.3 三个值得注意的点

(1) createEnvironment 忽略了 webApplicationType

对比 create() 和 getEnvironmentType() 都做了 != SERVLET 的守卫, createEnvironment 没有守卫,无条件返回 new ApplicationServletEnvironment()。

这是不一致吗?从语义上说,WAR 部署必然是 servlet 应用,所以无条件返回 servlet 环境是合理的。 但如果 WebApplicationType 不是 SERVLET(比如有人手贱配了 spring.main.web-application-type=none), 就会产生"环境是 servlet 的、上下文是 AnnotationConfigServletWebServerApplicationContext 的"混合状态。

换句话说:WAR 部署场景下,spring.main.web-application-type 对上下文无效,但对环境有效。 这是一个边界情况,我没有找到对应的测试用例,属于未能核实的行为。

(2) create() 忽略 webApplicationType

同样,create() 无条件返回 AnnotationConfigServletWebServerApplicationContext 的匿名子类。

所以 WAR 部署下,WebApplicationType 的推断结果完全不影响上下文类型。 这是合理的——WAR 部署必须是 servlet 上下文。

(3) 匿名子类覆写 onRefresh()

@Override
protected void onRefresh() {
    super.onRefresh();
    try {
        new WebApplicationContextInitializer(this)
            .initialize(WarDeploymentApplicationContextFactory.this.servletContext);
    }
    catch (ServletException ex) {
        throw new ApplicationContextException("Cannot initialize servlet context", ex);
    }
}

这覆写了 ServletWebServerApplicationContext.onRefresh() 的行为。

回忆 ServletWebServerApplicationContext.createWebServer() 的逻辑:

private void createWebServer() {
    WebServer webServer = this.webServer;
    ServletContext servletContext = getServletContext();
    if (webServer == null && servletContext == null) {
        // 分支 A:既没 webServer 也没 servletContext → 创建内嵌服务器
        StartupStep createWebServer = getApplicationStartup().start("spring.boot.webserver.create");
        ServletWebServerFactory factory = getWebServerFactory();
        createWebServer.tag("factory", factory.getClass().toString());
        webServer = factory.getWebServer(getSelfInitializer());
        this.webServer = webServer;
        createWebServer.end();
        getBeanFactory().registerSingleton("webServerGracefulShutdown", ...);
        getBeanFactory().registerSingleton("webServerStartStop", ...);
    }
    else if (servletContext != null) {
        // 分支 B:已有 servletContext → 只做初始化,不创建服务器
        try {
            getSelfInitializer().onStartup(servletContext);
        }
        catch (ServletException ex) {
            throw new ApplicationContextException("Cannot initialize servlet context", ex);
        }
    }
    initPropertySources();
}

分支 B 就是 WAR 部署的路径。WAR 部署时 ServletContext 由外部容器提供(通过 ServletContextApplicationContextInitializer 注入), 所以 servletContext != null,走分支 B,不创建内嵌服务器。

而 WarDeploymentApplicationContextFactory 的匿名子类在 super.onRefresh()(即 createWebServer())之后 又调了一次 new WebApplicationContextInitializer(this).initialize(servletContext)。

这里有一个我未能完全理清的点:createWebServer() 的分支 B 已经调用了 getSelfInitializer().onStartup(servletContext), 而 getSelfInitializer() 返回的就是 new WebApplicationContextInitializer(this)::initialize。 那么匿名子类里的第二次调用是否重复了?

推测:这两次调用的目的可能不同(一次是触发 ServletContextInitializer,一次是完成 WebApplicationContext 的 initPropertySources 等收尾), 或者存在某种幂等保证。这一条我标记为"未核实",不做断言。

我对这里的诚实评价:我没有读到 WebApplicationContextInitializer 的源码就下结论是危险的。 但把这个问题暴露出来是有价值的——它说明 WAR 部署路径的 onRefresh 逻辑有一处"看起来重复"的调用, 值得读者自行验证。


12. 自定义 ApplicationContextFactory:三种方式与四个坑

12.1 方式一:lambda / of(...)

new SpringApplicationBuilder(App.class)
    .contextFactory(ApplicationContextFactory.of(MyContext::new))
    .run(args);

或者:

application.setApplicationContextFactory((webApplicationType) -> new MyContext());

特点:对所有 WebApplicationType 返回同一种上下文。

12.2 方式二:ofContextClass(...)

application.setApplicationContextFactory(
    ApplicationContextFactory.ofContextClass(MyServletWebServerApplicationContext.class));

特点:用 BeanUtils.instantiateClass(contextClass) 反射构造,要求目标类有可用构造器。

12.3 方式三:完整实现接口

public class MyApplicationContextFactory implements ApplicationContextFactory {

    @Override
    public Class<? extends ConfigurableEnvironment> getEnvironmentType(WebApplicationType type) {
        return (type == WebApplicationType.SERVLET) ? ApplicationServletEnvironment.class : null;
    }

    @Override
    public ConfigurableEnvironment createEnvironment(WebApplicationType type) {
        return (type == WebApplicationType.SERVLET) ? new ApplicationServletEnvironment() : null;
    }

    @Override
    public ConfigurableApplicationContext create(WebApplicationType type) {
        return (type == WebApplicationType.SERVLET) ? new MyServletContext() : null;
    }

}

特点:可以"部分覆盖"——不关心的类型返回 null,靠 deduceEnvironmentClass() 的兜底链回退到 DEFAULT。

12.4 坑一:setApplicationContextFactory(null) 不是"取消"

public void setApplicationContextFactory(@Nullable ApplicationContextFactory applicationContextFactory) {
    this.applicationContextFactory = (applicationContextFactory != null) ? applicationContextFactory
            : ApplicationContextFactory.DEFAULT;
}

传 null = 恢复默认,不是"禁用"。 这个语义很友好,但容易与 setEnvironment(null)(真的是清空)混淆。

12.5 坑二:自定义工厂后,"环境类型兜底链"只在特定条件下生效

回顾 deduceEnvironmentClass():

Class<? extends ConfigurableEnvironment> environmentType = this.applicationContextFactory
    .getEnvironmentType(webApplicationType);
if (environmentType == null && this.applicationContextFactory != ApplicationContextFactory.DEFAULT) {
    environmentType = ApplicationContextFactory.DEFAULT.getEnvironmentType(webApplicationType);
}

只有当你传的 factory 不是 DEFAULT 时,才会回退到 DEFAULT。

这看起来理所当然,但它的后果是:

如果你的自定义工厂只覆写了 create()(用 ApplicationContextFactory.of(...) 创建), 那么 getEnvironmentType() 走的是接口的 default 方法,返回 null。 于是回退到 DEFAULT → DEFAULT 遍历 SPI → 得到 ApplicationServletEnvironment.class(SERVLET 时)。 转换后,环境是 servlet 的,但上下文是你自定义的(可能是非 servlet 的)。

这就是"环境与上下文类型不匹配"的来源。

12.6 坑三:AOT / native 下自定义工厂的 hints

如果你自定义工厂里用了反射(BeanUtils.instantiateClass), 在 native image 下需要为上下文类注册反射提示:

hints.reflection().registerType(MyContext.class, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS);

否则 native 构建会通过(因为反射调用在运行时才发生),但运行时报 MissingReflectionRegistrationError。

这也解释了为什么 WebApplicationType 里那个 WebApplicationTypeRuntimeHints 要为指示类注册反射提示—— 凡是"字符串类名 + 反射探测"的地方,native 下都需要 hints。

12.7 坑四:测试环境下 SpringBootContextLoader 的额外处理

SpringBootContextLoader 里有一段注释提到 spring.main.web-application-type 被设置时的问题:

// SpringBootContextLoader.java:445
* was registered. This can occur when spring.main.web-application-type is set to ...

(原注释未完整读取,只引用行号与方向。)

实战含义:@SpringBootTest 的 WebEnvironment 与 spring.main.web-application-type 可能冲突, SpringBootTestContextBootstrapper 通过注入属性来覆盖(4.4 节讲过)。 如果你在测试中同时用了两者且行为诡异,第一件事是检查 SpringBootTestContextBootstrapper.getWebEnvironmentType() 的返回值。


13. 排障手册

13.1 症状 → 原因 → 验证 → 修复

症状最可能的原因验证方法修复
端口没监听,但启动成功WebApplicationType 被判成 NONE启动时加 --debug,看 ConditionEvaluationReport;或 System.out.println(SpringApplication.getWebApplicationType())检查 classpath 是否有 spring-webmvc/spring-webflux;或显式设置 spring.main.web-application-type
"Unable to start ServletWebServerApplicationContext due to missing ServletWebServerFactory bean"上下文是 servlet 的,但没有 starter-tomcat/jetty看 spring-boot-starter-web 是否真的在运行时 classpath加 spring-boot-starter-web;或设置 web-application-type: none
同时有 webflux 和 webmvc,却被判成 SERVLET这是预期行为(MVC 的 @Order(10) 先命中)打印 WebApplicationType.deduce()想用 WebFlux 就去掉 spring-webmvc 依赖,或显式设 web-application-type: reactive
显式设了 setWebApplicationType(SERVLET) 但上下文不是 servlet 的spring.main.web-application-type 在配置文件里,覆盖了编程式设置检查 application.yaml / 命令行参数删掉配置项,或用自定义工厂(配置无法覆盖工厂)
"Unable create a default ApplicationContext instance, you may need a custom ApplicationContextFactory"自定义工厂返回 null,且 SPI 链无人接住;或 createDefaultApplicationContext() 抛异常看 cause 链最底层异常自定义工厂保证非 null;或检查 AotDetector 分支
"ApplicationContextFactory created null context"自定义工厂返回 null(DEFAULT 不会走到这)同上同上
项目里有 Mono,但还是 SERVLET只有 reactor-core 没有 spring-webflux(缺 DispatcherHandler)检查 org.springframework.web.reactive.DispatcherHandler 是否存在这是正确行为,不需要修复
只有 servlet-api 却没起 web serverSERVLET_INDICATOR_CLASSES 缺 ConfigurableWebApplicationContext(缺 spring-web)检查 spring-web 是否在加 spring-web(通常由 starter 带入)
WAR 部署到外部容器,却起了内嵌 TomcatWebApplicationType 是 SERVLET 但没有走 SpringBootServletInitializer检查你的 SpringBootServletInitializer 子类是否被容器发现确保 META-INF/services/jakarta.servlet.ServletContainerInitializer 生效;或 web.xml 配置
native 下报 MissingReflectionRegistrationError 关于上下文类自定义工厂用了反射但没注册 hints看 native 构建日志的 hints 缺失提示加 RuntimeHintsRegistrar

13.2 一行诊断代码

public static void main(String[] args) {
    SpringApplication app = new SpringApplication(MyApp.class);
    System.out.println("deduced = " + WebApplicationType.deduce());
    System.out.println("aot     = " + AotDetector.useGeneratedArtifacts());
    ConfigurableApplicationContext ctx = app.run(args);
    System.out.println("context = " + ctx.getClass().getName());
    System.out.println("env     = " + ctx.getEnvironment().getClass().getName());
    System.out.println("server  = " + extractWebServer(ctx));
}

private static Object extractWebServer(ApplicationContext ctx) {
    return (ctx instanceof WebServerApplicationContext wsac) ? wsac.getWebServer() : "not a web context";
}

这五行输出能覆盖本章 90% 的排查需求。

13.3 用 --debug 看条件报告

WebApplicationType 本身不产生条件报告,但它决定了哪些自动配置类会被评估。 --debug(或 spring.main.banner-mode 无关,是 DebugConditionEvaluationReportListener)会打印 ConditionEvaluationReport,其中:

  • TomcatServletWebServerAutoConfiguration 是否 matched
  • 它的 @ConditionalOnWebApplication(type = Type.SERVLET) 是否通过

如果 @ConditionalOnWebApplication(type = SERVLET) 显示 "did not find required web application type", 那就说明 WebApplicationType 不是 SERVLET。这比直接打印类型更能说明"下游影响"。


14. 实证实验

实验 1:验证"MVC 赢过 WebFlux"

依赖(Gradle):

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("org.springframework.boot:spring-boot-starter-webflux")
}

主类:

@SpringBootApplication
public class DemoApplication {
    public static void main(String[] args) {
        System.out.println("deduced = " + WebApplicationType.deduce());
        SpringApplication.run(DemoApplication.class, args);
    }
}

预期输出:deduced = SERVLET 验证点:WebMvcWebApplicationTypeDeducer(@Order(10))先于 WebFluxWebApplicationTypeDeducer(@Order(20))命中。

实验 2:验证"配置文件可以覆盖编程式设置"

public static void main(String[] args) {
    SpringApplication app = new SpringApplication(DemoApplication.class);
    app.setWebApplicationType(WebApplicationType.SERVLET);   // 编程式
    ConfigurableApplicationContext ctx = app.run(args);
    System.out.println(ctx.getClass());                       // ← 观察
}

application.properties:

spring.main.web-application-type=none

预期:输出 org.springframework.context.annotation.AnnotationConfigApplicationContext(非 web), 而不是 AnnotationConfigServletWebServerApplicationContext。

验证点:bindToSpringApplication 在 createApplicationContext 之前覆盖了值。

实验 3:验证 webApplicationType = null 的行为

public static void main(String[] args) {
    SpringApplication app = new SpringApplication(DemoApplication.class);
    ConfigurableApplicationContext ctx = app.run("--spring.main.web-application-type=");
    System.out.println(ctx.getClass());
}

预期(推测):空字符串绑定后可能得到 null 或抛绑定异常。 若得到 null,则两个 web 工厂都返回 null → createDefaultApplicationContext() → AnnotationConfigApplicationContext。

⚠️ 未实测:我没有运行这段代码。空值绑定的具体行为取决于 Binder 对空字符串的处理 (可能转成 null,也可能抛 BindException)。这是留给读者的实验。

实验 4:自定义工厂的"部分覆盖"

public class OnlyReactiveFactory implements ApplicationContextFactory {
    @Override
    public ConfigurableApplicationContext create(@Nullable WebApplicationType type) {
        return (type == WebApplicationType.REACTIVE) ? new MyReactiveContext() : null;
    }
    // 不覆写 getEnvironmentType / createEnvironment
}
app.setApplicationContextFactory(new OnlyReactiveFactory());
  • type = REACTIVE → 返回 MyReactiveContext;环境类型回退到 DEFAULT → ApplicationReactiveWebEnvironment
  • type = SERVLET → 返回 null;回退到 DEFAULT → AnnotationConfigServletWebServerApplicationContext;环境 → ApplicationServletEnvironment

验证点:deduceEnvironmentClass() 的兜底链让自定义工厂可以"只管一半"。

实验 5:观察 EnvironmentConverter 的 servlet 源保留

public static void main(String[] args) {
    SpringApplication app = new SpringApplication(DemoApplication.class);
    app.setEnvironment(new ApplicationServletEnvironment());  // 故意塞一个 servlet 环境
    // 注意:isCustomEnvironment = true,转换会被跳过!
    ConfigurableApplicationContext ctx = app.run(args);
}

预期:因为 isCustomEnvironment = true,不会发生转换。 要触发转换,需要在 setEnvironment 之后手动改 isCustomEnvironment(包私有字段,只能通过反射或子类)。

这个实验的价值在于:它证明了 isCustomEnvironment 的短路效应。 如果你想观察 EnvironmentConverter 的真实行为,应该用子类或反射绕过这个短路。


15. 方法论:本章沉淀的两条定律

15.1 第十三定律:SPI 化的判断依据是"核心模块是否知道"

当一个决策点的判断逻辑需要引用"核心模块不该知道的类"时,它就会被外置成 SPI。

本章的两处实证:

  1. WebApplicationType.Deducer —— 核心模块不认识 DispatcherServlet / DispatcherHandler, 所以把"是不是 MVC/WebFlux 应用"的判断下沉到 spring-boot-webmvc / spring-boot-webflux。
  2. EnvironmentConverter.isServletEnvironment 用字符串类名 "org.springframework.web.context.ConfigurableWebEnvironment" 而不是 .class 字面量——核心模块不认识这个类,只能用字符串软引用。

反向应用:当你看到一个 SPI 接口时,问一句"核心模块为什么不能直接写死?" 答案通常是:"因为它会引入一个不该有的依赖。"这个答案能帮你理解 SPI 的边界在哪里。

配套信号:SPI 实现类里出现 // Guard in case ... dependencies are excluded 这类注释, 说明 SPI 的存在就是为了处理"模块在、实现不在"的依赖裁剪场景。

15.2 第十四定律:三目表达式的"守卫方向"要按真值表读,不要按直觉读

看到 (x != CONST) ? null : value 这种"否定在前"的守卫写法,不要自动脑补 null 处理语义。 先用真值表把 x = null / x = CONST / x = 其他 三种情况算一遍。

我在本章 §7.4 犯了这个错——以为 != 写法是为了处理 webApplicationType == null。 逐值推演后发现两者完全等价。!= 只是 early-return 风格。

为什么这个错误值得记录? 因为它是一种高频的源码阅读失误: 在 Boot 的代码里,!= null ? x : y 这种带 null 的守卫到处都是, 读多了会形成"看到 != 就想到 null"的肌肉记忆。但这里的 != 比较的是枚举常量,不是 null。

防御方法:凡是比较对象不是 null 字面量的 !=,停下来列真值表。

15.3 第十五定律:兜底责任归"调用方兜不住"的那一方

SPI 的兜底不是随便放的。调用方能兜底 → SPI 不兜;调用方不能兜底 → SPI 必须兜。

本章的实证(DefaultApplicationContextFactory):

  • getEnvironmentType / createEnvironment:SpringApplication 有兜底(ApplicationEnvironment)→ 传 null 作为 defaultResult
  • create:SpringApplication 是 Assert.state(context != null) 硬失败 → 必须传 this::createDefaultApplicationContext

判定方法:看调用方拿到 null 之后做什么。 如果调用方有 ?: 默认值 或者 if (x == null) x = ...,那 SPI 可以不兜; 如果调用方是 Assert.notNull / Assert.state,那 SPI 必须兜。

这与第十定律(诊断能力提前布局)是同一个思维家族—— 都是"看调用方的消费方式,反推生产方的义务"。

15.4 三个"同一模式出现三次"的观察

模式出现位置含义
for (c : CLASSES) if (!isPresent(c, null)) return false;isServletApplication()、MVC Deducer、WebFlux Deducer指示类"全部满足"判定
if (!AotDetector.useGeneratedArtifacts()) return new AnnotationConfigXxx(); return new Xxx();三个 ApplicationContextFactoryAOT 双轨
doGetActiveProfilesProperty()/doGetDefaultProfilesProperty()/createPropertyResolver() 三覆写三个 Application*EnvironmentBoot 对 Environment 的统一改造

Boot 里"同一模式出现三次以上"时,它通常是一个稳定的设计惯例,值得记住成模式名。 出现一次可能是巧合,出现三次就是惯例。


16. 本章小结

这一章回答了两个问题,并顺带建立了三条方法论。

事实清单(可检索验证)

  1. WebApplicationType.deduce() 的 @since 是 4.0.1,与 Deducer 接口同时引入。
  2. deduce() 的兜底分支只可能返回 SERVLET 或 NONE,REACTIVE 必然来自 Deducer。
  3. SERVLET_INDICATOR_CLASSES 有两个类:jakarta.servlet.Servlet 与 org.springframework.web.context.ConfigurableWebApplicationContext。
  4. MVC Deducer 是 @Order(10),WebFlux Deducer 是 @Order(20);MVC 先命中 → 同时有两者时是 SERVLET。
  5. ApplicationContextFactory 的 @since 是 2.4.0;getEnvironmentType / createEnvironment 的 @since 是 2.6.14。
  6. spring-boot-web-server 的 spring.factories 注册了 5 类 SPI,其中 ApplicationContextFactory 有两个实现(Reactive 先、Servlet 后,顺序无关)。
  7. WebServerApplicationContext.GRACEFUL_SHUTDOWN_PHASE = Integer.MAX_VALUE - 1024; START_STOP_LIFECYCLE_PHASE = Integer.MAX_VALUE - 2048。
  8. EnvironmentConverter 用字符串类名引用 ConfigurableWebEnvironment(软依赖)。
  9. EnvironmentConverter 的合并语义是"目标环境的 servlet 源优先,源环境的其他源补充"。
  10. spring.main.web-application-type 会覆盖 setWebApplicationType(),因为 bindToSpringApplication 在 createApplicationContext 之前执行。
  11. setApplicationContextFactory(null) = 恢复 DEFAULT,不是禁用。
  12. AOT 路径用 GenericApplicationContext / ServletWebServerApplicationContext / ReactiveWebServerApplicationContext; 非 AOT 用带 AnnotationConfig 前缀的版本。

未核实清单(诚实标注)

#事项状态
1SpringFactoriesLoader.load() 是否按 @Order 排序Framework 源码不在本仓库;Boot 侧注释强烈暗示有排序
2AotDetector.useGeneratedArtifacts() 的具体判定机制Framework 源码不在本仓库
3WebApplicationTypeRuntimeHints 的注册点本仓库未检索到
43.x 的 deduceFromClasspath() 实现历史版本,代码片段来自记忆
5spring.main.web-environment 是否仍有兼容映射按 ApplicationProperties 无对应字段推断为不生效
6WAR 部署路径中 WebApplicationContextInitializer 的两次调用是否重复未读该类源码,不做断言
7spring.main.web-application-type= 空值的绑定结果未实测

与前后章的连接

  • 第 04 章(run() 的十二步状态机):本章是第 4、5、6 步的展开
  • 第 06 章(Environment 构建):ConfigurationPropertySources.createPropertyResolver 的注入点在本章 §9.3
  • 第 10 章(Profile 语义):doGetActiveProfilesProperty() 返回 null 的原因在本章 §9.2 得到解释
  • 第 12 章(事件总线与 Bootstrap):bootstrapContext.close(context) 在 prepareContext 中
  • 第 13 章(Bean 定义加载):load(context, sources) 在 AOT 下被跳过
  • 第 16 章(FailureAnalyzer):本章 §10.1 指出 ApplicationContextFactory created null context 没有对应的 FailureAnalyzer
  • 第 18 章(内嵌 Web 服务器):本章 §7.6 的 WebServerApplicationContext 两个 phase 常量是下一章 Lifecycle 排序的基础

17. 本章未逐行核实 / 待你实测的项

  1. 3.x 的代码片段来自记忆,未核实(文中已声明)。
  2. WebApplicationType.Deducer SPI 的全部实现未统计。
  3. ApplicationContextFactory 的其它实现(含 gRPC)在第 21 章。
  4. ApplicationServletEnvironment 等环境类的差异未逐个读。
  5. WebApplicationType 推断在 AOT 下的行为未验证。

18. 下一章预告

第 18 章进入 Web 栈的核心地带:内嵌 Web 服务器。

我们会从 WebServer 这个只有 5 个方法的接口出发,一路读到:

  • ServletWebServerFactory / ReactiveWebServerFactory 的双分支
  • TomcatServletWebServerFactory.getWebServer() 的 24 行代码里藏着的 5 个阶段
  • TomcatWebServer.initialize() 里那个"先移除 connector、再 start、再加回来"的诡异流程为什么必要
  • WebServerStartStopLifecycle 与 WebServerGracefulShutdownLifecycle 的 phase 编排
  • PortInUseException 的 "in use" 字符串匹配判定
  • Tomcat / Jetty / Reactor Netty 三个实现在优雅停机上的语义差异

第 17 章讲的是"决定造什么",第 18 章讲的是"怎么造出来并跑起来"。


(本章完)