Spring Framework 7.0.9 深度剖析-07-条件装配与自动配置

3 阅读1小时+

Spring Framework 7.0.9 深度剖析-07-条件装配与自动配置

一、本章导读

1.1 本章要解决的问题

在 Spring 框架的日常使用中,下面这些语句几乎随处可见:

@Configuration
@ConditionalOnClass(DataSource.class)
@ConditionalOnProperty(prefix = "app.cache", name = "enabled", havingValue = "true")
public class CacheAutoConfiguration { ... }

@Component
@Profile("!test")
public class MetricReporter { ... }

无论是 @Profile 按环境装配、@ConditionalOnClass 按类路径存在与否装配,还是 Spring Boot 那套"约定大于配置"的自动配置,落到框架层,都归结为同一个问题:一个 bean 定义(或一个配置类、或一个 @Bean 方法)在真正注册进 BeanDefinitionRegistry 之前,框架如何允许一段用户编写的逻辑"投票否决"这次注册?

这个问题的答案,就是 org.springframework.context.annotation 包下的 Condition 接口、@Conditional 注解,以及它们的评估引擎 ConditionEvaluator。本章要讲透的就是这一套机制在 Spring Framework 7.0.9 中的完整实现:条件是什么、谁来评估、在什么时机评估、两个评估阶段(解析阶段与注册阶段)有何本质区别、条件否决之后 bean 定义会发生什么、以及这套机制如何成为上层"自动配置"能力的基石。

1.2 涉及的模块与核心类

本章全部内容扎根于 spring-context 模块,核心类及其仓库相对路径如下(均位于 spring-context/src/main/java/org/springframework/context/annotation/ 下):

  • Condition.java —— 条件的顶层函数式接口,唯一方法 matches(ConditionContext, AnnotatedTypeMetadata)。
  • ConditionContext.java —— 条件评估时暴露给条件实现的上下文(注册表、环境、资源加载器、类加载器)。
  • Conditional.java —— 标注在类或方法上的注解,持有一组 Condition 实现类。
  • ConfigurationCondition.java —— 带阶段语义的条件子接口,内含枚举 ConfigurationPhase{PARSE_CONFIGURATION, REGISTER_BEAN}。
  • ConditionEvaluator.java —— 条件评估引擎,shouldSkip(...) 是本章的绝对主角。
  • ProfileCondition.java —— @Profile 的条件实现,框架内置的唯一一个具体 Condition。
  • Profile.java —— 元注解 @Conditional(ProfileCondition.class) 的组合注解范例。
  • ConfigurationClassParser.java —— 配置类解析器,PARSE 阶段条件的评估点。
  • ConfigurationClassBeanDefinitionReader.java —— 把解析结果落地为 bean 定义的读取器,REGISTER_BEAN 阶段条件的评估点,内含 TrackedConditionEvaluator。
  • ImportSelector.java / DeferredImportSelector.java —— 与条件装配紧密配合的"按需导入"机制,是自动配置的桥梁。
  • 另外会跨到 spring-core 模块的 org.springframework.core.env 包:Environment、AbstractEnvironment、Profiles,用于讲清 @Profile 的判定底层。

这里必须先点出一个与常见二手资料不符的版本事实:在 Spring Framework 7.0.9 中,条件相关的类并不位于很多文章所写的 context/annotation/condition/ 子包,而是直接平铺在 org.springframework.context.annotation 包下。读者可以在仓库中自行核对:spring-context/src/main/java/org/springframework/context/annotation/ 目录下与 Condition.java、Configuration.java、Bean.java 同列,不存在 condition 子目录。这是 7.x 包结构整理后的结果,本章所有路径一律以仓库现状为准。

1.3 前置知识

阅读本章需要:

  • 了解 @Configuration / @Bean / @ComponentScan / @Import 的基本用法,知道配置类解析大致经历"解析(parse)"与"注册 bean 定义(register bean definitions)"两个阶段;
  • 了解 BeanDefinitionRegistry、ConfigurableListableBeanFactory、Environment 在容器启动早期的角色;
  • 不需要读过其他章节——本章所有结论都自带源码出处,自包含。

1.4 读完能获得什么

  • 能背出 ConditionEvaluator.shouldSkip 每一行在做什么,包括阶段推导逻辑、条件排序、实例化方式;
  • 能准确说出类上的 @Conditional 与 @Bean 方法上的 @Conditional 分别在哪一刻被评估,为什么会有两个阶段;
  • 能解释为什么"在 @Bean 方法的条件里去查另一个 @Bean 是否已注册"有时成立、有时不成立——这正是 ConfigurationCondition 两阶段设计要解决的问题;
  • 能写出生产级的自定义条件(按环境、按类路径、按属性开关),并知道 @Order 如何影响多个条件的短路顺序;
  • 能讲清 DeferredImportSelector 的延迟注册时机,以及 Spring Boot 自动配置为什么依赖它;
  • 能避开条件装配中最经典的几个坑:错误阶段评估、条件放在接口/父类上、matches 里过早触碰 bean 工厂、重载 @Bean 方法上的 @Profile 不一致。

二、核心概念

本章涉及的抽象并不多,但每一个都对应一个具体的接口或类。下面逐个结合源码定义讲清职责,而不是罗列名词表。

2.1 Condition:可以否决注册的"投票者"

Condition 是整个条件装配体系的根抽象,定义在 spring-context/src/main/java/org/springframework/context/annotation/Condition.java。它是一个 @FunctionalInterface,只有一个方法(见该文件第 48-61 行):

@FunctionalInterface
public interface Condition {

	/**
	 * Determine if the condition matches.
	 * @param context the condition context
	 * @param metadata the metadata of the class or method being checked
	 * @return {@code true} if the condition matches and the component can be registered,
	 * or {@code false} to veto the annotated component's registration
	 */
	boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);

}

逐句拆解这个接口的语义:

  1. 一个条件 = 一个 matches 判定函数。返回 true 表示"条件满足,允许注册";返回 false 表示"否决,跳过这个组件"。注意方向是"匹配才放行",而不是"命中才跳过"——ConditionEvaluator 内部把 !matches(...) 翻译成 skip。
  2. metadata 参数承载了被检查对象的注解元数据,类型是 AnnotatedTypeMetadata。它既可能是类级别的 AnnotationMetadata(标注在 @Configuration 类或组件类上的 @Conditional),也可能是方法级别的 MethodMetadata(标注在 @Bean 方法上的 @Conditional)。条件实现通过 metadata.getAnnotationAttributes(...) 读出自己注解上携带的参数——例如 ProfileCondition 就是这样从 @Profile 注解里读出 profile 名字的。
  3. javadoc 里有一句极其重要的约束:"Conditions must follow the same restrictions as a BeanFactoryPostProcessor and take care to never interact with bean instances."——条件和 BeanFactoryPostProcessor 一样,工作在 bean 实例化之前的"定义注册阶段",不允许也不应该去触碰 bean 实例。条件里可以读环境、读注册表、读类路径,但 context.getBeanFactory().getBean(...) 是明确的反模式(本章"常见问题与坑"一节会用源码解释为什么)。
  4. 接口上 @since 4.0 的标注说明:@Conditional 体系是 Spring 4.0 引入的,比 3.1 的 @Profile 晚——事实上 @Profile 在 4.0 被重构成了 @Conditional 的一个特例(见 2.5 节)。

2.2 ConditionContext:条件能看到的"世界"

条件实现不能凭空判断,它需要知道"我在为谁、在什么环境下做判断"。这个上下文由 ConditionContext 接口定义(ConditionContext.java,第 33-67 行),一共五个方法:

public interface ConditionContext {
	BeanDefinitionRegistry getRegistry();                       // 将要承接 bean 定义的注册表
	@Nullable ConfigurableListableBeanFactory getBeanFactory(); // 对应的 bean 工厂(可能为 null)
	Environment getEnvironment();                               // 当前运行环境(profile、属性源)
	ResourceLoader getResourceLoader();                         // 资源加载器
	@Nullable ClassLoader getClassLoader();                     // 类加载器
}

五个方法对应条件实现最常见的五种判定依据:

  • getRegistry():查"某个名字的 bean 定义是否已经注册"(例如"如果用户自己定义了 XxxTemplate,就不要再注册默认的")。javadoc 特别注明它可能在"一个纯粹的 ClassPathScanningCandidateComponentProvider"场景下不可用,此时会抛 IllegalStateException——也就是说条件里直接调 getRegistry() 是有风险的,稳健的写法是只在 ConfigurationCondition 的 REGISTER_BEAN 阶段用它。
  • getEnvironment():读激活的 profile、读配置属性(@ConditionalOnProperty 的框架层原型就是读 environment.getProperty(...))。这是最安全、最常用的判定依据,因为环境在容器启动极早期就已准备好。
  • getResourceLoader() / getClassLoader():做类路径探测(ClassUtils.isPresent("com.foo.Bar", classLoader) 是 @ConditionalOnClass 的原始范式)。
  • getBeanFactory():可空。拿到它主要是为了查"已注册的 bean 定义"(getBeanFactory().containsBeanDefinition(name)),而不是为了拿 bean 实例。

这个接口的实现类是 ConditionEvaluator 的私有静态内部类 ConditionContextImpl,本章源码剖析部分会完整展开它的五个 deduce 方法。

2.3 @Conditional:挂在类型或方法上的条件容器

@Conditional 注解本身极简(Conditional.java,第 59-69 行):

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Conditional {
	Class<? extends Condition>[] value();
}

四个设计决策值得逐条咀嚼:

  1. @Target 只有 TYPE 和 METHOD。它不能标在字段上、不能标在构造器上、不能标在参数上——因为条件判定的对象是"要不要注册这个 bean 定义",而 bean 定义的来源只有两类:类(组件扫描扫出来的类、@Configuration 类、@Import 进来的类)和 @Bean 工厂方法。字段/参数级别的条件没有对应的"注册时机"可以挂。
  2. @Retention(RUNTIME):条件评估发生在容器启动期(运行时通过反射/ASM 读注解),不是编译期或类加载期,所以必须保留到运行时。
  3. value() 是 Class<? extends Condition>[]:注意存的是条件实现类的 Class 对象,而不是实例。这意味着条件实例不是由用户 new 出来的,而是框架在评估时反射实例化的——每次评估都走无参构造。这个细节直接影响后面"条件实例无状态、不要在里面缓存东西"的最佳实践。
  4. javadoc 明确列出三种合法用法:标在任何 @Component(含 @Configuration)类上;作为元注解组合出自定义注解;标在任何 @Bean 方法上。同时还有一句警告:"@Conditional 不支持继承"——它自己没有标 @Inherited,父类上的条件、被重写方法上的条件都不会生效。这个坑后面会专门讲。

2.4 ConfigurationCondition:带阶段语义的条件

普通 Condition 不知道自己"在哪一刻被评估"。但有些条件天然需要区分时机:比如"检查 bean 定义是否已经注册"的条件,如果在解析早期跑,此时几乎什么都还没注册,必然误判;它必须等到所有配置类都解析完、bean 定义即将注册时再跑。

ConfigurationCondition 就是为这个诉求设计的(ConfigurationCondition.java,第 30-59 行):

public interface ConfigurationCondition extends Condition {

	ConfigurationPhase getConfigurationPhase();

	enum ConfigurationPhase {
		PARSE_CONFIGURATION,   // 作为 @Configuration 类被解析时评估
		REGISTER_BEAN         // 添加普通(非配置类)bean 时评估
	}
}

它只是在 Condition 之上多了一个 getConfigurationPhase(),告诉评估器"我只关心某一个阶段"。两个枚举值的 javadoc 给出了权威语义:

  • PARSE_CONFIGURATION:在 @Configuration 类正在被解析时评估。此阶段条件不匹配,整个配置类都不会被加入——连它的 @Bean 方法、@Import、@ComponentScan 都一并被跳过。
  • REGISTER_BEAN:在添加普通 bean(非 @Configuration 类)时评估。此时所有 @Configuration 类都已经解析完毕。此阶段条件不匹配,不会阻止 @Configuration 类本身被加入,只影响普通 bean 的注册。

这两个阶段的名字本身就是本章的文眼:条件不是"评估一次",而是可能在两个不同的时间点各评估一次,由条件自己声明它关心哪个。下一节讲 ConditionEvaluator.shouldSkip 时会看到这个声明是如何被翻译成"此阶段该不该跑这个条件"的。

2.5 @Profile:条件体系的第一个官方应用

Profile.java 第 107-116 行给出了 @Profile 的完整定义:

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(ProfileCondition.class)
public @interface Profile {
	String[] value();
}

注意第三行元注解:@Conditional(ProfileCondition.class)。这就是"@Profile 本质上是一个预制的 @Conditional"的铁证——它不是平行于条件体系的另一套机制,而是条件体系的第一个组合注解样例:框架先写好 ProfileCondition,再用元注解把它绑在 @Profile 上。后面自定义条件注解的写法(@Target/@Retention/@Conditional(MyCondition.class) 三件套)完全是对这个模式的复制。

2.6 ImportSelector 家族:自动配置的桥梁

条件决定"一个候选要不要装",而 ImportSelector 决定"把哪些类拉进来当候选"。两者在自动配置里是接力关系:

  • ImportSelector.selectImports(AnnotationMetadata):同步返回要导入的类名数组,解析 @Import 时立刻处理。
  • DeferredImportSelector:继承 ImportSelector,但延迟到所有配置类解析完毕之后再执行 selectImports,并且支持 Group 跨选择器聚合。Spring Boot 的 AutoConfigurationImportSelector 正是它的子类——框架层在这里留好了挂钩,上层自动配置只是站在这个肩膀上。

本章只讲框架层机制(本仓库不含 spring-boot 源码),第 3.9 节会把 processImports 与 DeferredImportSelectorHandler 的源码逐行展开。

2.7 概念关系一图流

            @Conditional(ClassA.class, ClassB.class)
                    │ (value 里装的是 Condition 实现类)
                    ▼
        ConditionEvaluator.shouldSkip(metadata, phase)
                    │
        ┌───────────┼────────────────────┐
        ▼           ▼                    ▼
  collectConditions  getConditionClasses  ConditionContextImpl
  (排序后逐个 matches)(读元注解属性)   (registry/env/loader/cl)
        │
        ▼  true=跳过,return true
   返回给调用方:
     · ConfigurationClassParser.processConfigurationClass  (PARSE_CONFIGURATION)
     · ConfigurationClassBeanDefinitionReader.loadBeanDefinitionsForBeanMethod (REGISTER_BEAN)
     · ClassPathScanningCandidateComponentProvider.isConditionMatch (组件扫描)
     · AnnotatedBeanDefinitionReader.doRegisterBean (编程式注册)

这张图后面每一节都会回来对位:谁调用、传什么 phase、跳过之后发生什么。

三、源码剖析

本章源码剖析按"评估引擎本体 → 条件收集与实例化 → 上下文 → 两个阶段的调用点 → @Profile 实现 → ImportSelector 桥梁 → 扫描与编程式注册入口"的顺序展开。

3.1 评估引擎本体:ConditionEvaluator 的全貌

ConditionEvaluator 是一个包可见(package-private)的内部类,定义在 spring-context/src/main/java/org/springframework/context/annotation/ConditionEvaluator.java,类签名第 49 行:

class ConditionEvaluator {

	private final ConditionContextImpl context;

	public ConditionEvaluator(@Nullable BeanDefinitionRegistry registry,
			@Nullable Environment environment, @Nullable ResourceLoader resourceLoader) {
		this.context = new ConditionContextImpl(registry, environment, resourceLoader);
	}
	……
}

注意它的三个字段/构造参数:BeanDefinitionRegistry、Environment、ResourceLoader。三者全部可能为 null(构造参数都是 @Nullable),ConditionContextImpl 会在内部用 deduce 逻辑兜底(3.4 节展开)。评估引擎本身无状态——它没有任何缓存字段。

这里要特别强调一个版本事实,因为它与网上绝大多数资料相悖:很多旧版书籍和博客会告诉你 ConditionEvaluator 里有一个 skipCache(ConcurrentHashMap<AnnotatedTypeMetadata, Boolean>)缓存评估结果。在 Spring Framework 7.0.9 的这份源码里,通读全类(共 234 行),字段区只有一个 private final ConditionContextImpl context;,不存在任何 skipCache、conditionEvaluationCache 之类的字段。也就是说,7.0.9 中每次 shouldSkip 调用都会重新收集条件、重新实例化、重新 matches。唯一的"结果缓存"出现在调用方 ConfigurationClassBeanDefinitionReader.TrackedConditionEvaluator 内部(3.6 节讲),它缓存的是"一个配置类在本轮注册加载中是否被跳过",且只在 REGISTER_BEAN 阶段的类级评估处用。写文章时若还照抄旧版"ConditionEvaluator 用 skipCache 缓存"就是与仓库不符,必须纠正。

3.1.1 入口:两个重载的 shouldSkip

引擎对外暴露两个方法(第 71-92 行):

public boolean shouldSkip(AnnotatedTypeMetadata metadata) {
	return shouldSkip(metadata, null);
}

public boolean shouldSkip(@Nullable AnnotatedTypeMetadata metadata, @Nullable ConfigurationPhase phase) {
	if (metadata == null || !metadata.isAnnotated(Conditional.class.getName())) {
		return false;
	}

	if (phase == null) {
		if (metadata instanceof AnnotationMetadata annotationMetadata &&
				ConfigurationClassUtils.isConfigurationCandidate(annotationMetadata)) {
			return shouldSkip(metadata, ConfigurationPhase.PARSE_CONFIGURATION);
		}
		return shouldSkip(metadata, ConfigurationPhase.REGISTER_BEAN);
	}
	……
}

逐行拆解:

第一关:快速无注解短路。 if (metadata == null || !metadata.isAnnotated(Conditional.class.getName())) return false;——没有 @Conditional 就无需评估,直接放行。isAnnotated 是 AnnotatedTypeMetadata 的方法,对 ASM 读取的元数据而言它只做类型名匹配,开销极小,是热路径上的第一道闸门。

第二关:阶段推导(phase == null 分支)。 单参数 shouldSkip(metadata) 是"我不知道现在是什么阶段,你帮我判"的入口,组件扫描、编程式注册走的都是这个入口。推导规则只有一条:

if (metadata instanceof AnnotationMetadata annotationMetadata &&
		ConfigurationClassUtils.isConfigurationCandidate(annotationMetadata)) {
	return shouldSkip(metadata, ConfigurationPhase.PARSE_CONFIGURATION);
}
return shouldSkip(metadata, ConfigurationPhase.REGISTER_BEAN);

即:如果被检查的是一个"配置候选类"(AnnotationMetadata 是类级元数据,且 ConfigurationClassUtils.isConfigurationCandidate 判定它带 @Configuration/@Component/@ComponentScan/@Import/@Bean 中任意一个,或是接口除外的普通组件类),就按 PARSE_CONFIGURATION 处理;否则按 REGISTER_BEAN 处理。这个推导解释了组件扫描时一个带 @Conditional 的 @Component 为什么走 PARSE 阶段:它本身是配置候选类,扫出来后要作为配置类去解析,所以条件在解析阶段就要否决它——一旦否决,整个类连 bean 定义都不会进注册表。

为什么要做这个推导:因为 Condition 接口本身没有阶段概念(只有 ConfigurationCondition 子接口才有)。对于普通条件,"在哪个阶段跑"其实无所谓——matches 里通常只读 environment/classpath,这些在容器启动早期就稳定了。阶段推导只是为了让"碰巧实现了 ConfigurationCondition 的条件"在扫描入口也能拿到一个合理的默认阶段。真正区分两个阶段的,是显式传入 phase 的两个调用点(3.5、3.6 节)。

3.1.2 核心循环:收集条件并逐个 matches

进入显式 phase 后,主体逻辑(第 94-106 行)非常短,但每一句都有讲究:

List<Condition> conditions = collectConditions(metadata);
for (Condition condition : conditions) {
	ConfigurationPhase requiredPhase = null;
	if (condition instanceof ConfigurationCondition configurationCondition) {
		requiredPhase = configurationCondition.getConfigurationPhase();
	}
	if ((requiredPhase == null || requiredPhase == phase) && !condition.matches(this.context, metadata)) {
		return true;
	}
}

return false;

逐句讲:

  1. collectConditions(metadata):从注解元数据里取出所有 @Conditional(含元注解上的)声明的条件类,反射实例化,并按 @Order 排序。下一小节专门拆。
  2. 短路与运算:多个条件之间是"逻辑与"关系——任何一个条件 matches 返回 false,整个方法立刻 return true(跳过);只有全部条件都 matches 成功,才落到方法末尾 return false(放行)。这就是 @Conditional({A.class, B.class}) 的语义:A、B 都要满足。
  3. 阶段过滤是这门课的精髓:
    • 普通条件(不是 ConfigurationCondition):requiredPhase == null,恒为真,两个阶段都会跑。
    • ConfigurationCondition:requiredPhase 非空,只有当 requiredPhase == 当前 phase 时才进入 matches;阶段不符的条件直接跳过、不参与本次判定。
  4. 这个设计精确表达了两阶段语义:一个声明了 REGISTER_BEAN 的 ConfigurationCondition,在 PARSE 阶段评估时被静默忽略(不影响配置类是否加入),等到 REGISTER_BEAN 阶段才真正跑 matches;反之亦然。条件作者把"我只在某阶段说话"的权利握在自己手里,评估器只负责按时召唤。
  5. 异常传播:注意循环里没有任何 try/catch。condition.matches(...) 抛出的任何异常(运行时异常、BeanInstantiationException 等)都会原样向上抛。任务书里提到的 ConditionException 在 7.0.9 仓库中不存在——全文 grep class ConditionException 零命中。旧版本(Spring 4.x 早期)确实曾把条件评估异常包装成一个专用异常,7.0.9 已改为异常裸奔、由外层 ConfigurationClassParser 的 catch (Throwable ex) 统一包成 BeanDefinitionStoreException("Failed to parse configuration class [...]")(见 ConfigurationClassParser.java 第 194-197 行与第 643-647 行)。读者若在网上看到"matches 抛异常会被收集后统一抛 ConditionException"的说法,那是旧版行为,以本仓库为准。

3.2 条件是怎么被收集出来的:collectConditions / getConditionClasses / getCondition

collectConditions(第 114-128 行):

List<Condition> collectConditions(@Nullable AnnotatedTypeMetadata metadata) {
	if (metadata == null || !metadata.isAnnotated(Conditional.class.getName())) {
		return Collections.emptyList();
	}

	List<Condition> conditions = new ArrayList<>();
	for (String[] conditionClasses : getConditionClasses(metadata)) {
		for (String conditionClass : conditionClasses) {
			Condition condition = getCondition(conditionClass, this.context.getClassLoader());
			conditions.add(condition);
		}
	}
	AnnotationAwareOrderComparator.sort(conditions);
	return conditions;
}

三个要点:

第一,为什么 getConditionClasses 返回 List<String[]>(套了一层嵌套)? 看它的实现(第 130-135 行):

@SuppressWarnings("unchecked")
private List<String[]> getConditionClasses(AnnotatedTypeMetadata metadata) {
	MultiValueMap<String, @Nullable Object> attributes =
			metadata.getAllAnnotationAttributes(Conditional.class.getName(), true);
	Object values = (attributes != null ? attributes.get("value") : null);
	return (List<String[]>) (values != null ? values : Collections.emptyList());
}

关键在 getAllAnnotationAttributes(Conditional.class.getName(), true) 第二个参数 classValuesAsString = true——它返回的是所有声明了 @Conditional 的注解位置的属性聚合:直接标在类上的 @Conditional 是一份;组合注解(元注解上的 @Conditional,比如 @Profile、@ConditionalOnXxx)每一份也是一个 String[]。所以 MultiValueMap 里 key="value" 对应的是一个 List<String[]>:外层 List 的每个元素来自一处 @Conditional 声明,内层 String[] 是该处声明的条件类列表。

这就是"@Conditional 作为元注解可被组合"在源码层面的落点:@Profile 上的 @Conditional(ProfileCondition.class) 和你自己类上的 @Conditional(MyCondition.class) 会被合并到同一个条件列表里一起评估。例如一个类同时标了 @Profile("prod") 和 @Conditional(MyCondition.class),收集到的条件列表就是 [ProfileCondition, MyCondition],两者都要 matches 成功。

第二,条件实例化:每次都 new,无单例。 getCondition(第 137-140 行):

private Condition getCondition(String conditionClassName, @Nullable ClassLoader classloader) {
	Class<?> conditionClass = ClassUtils.resolveClassName(conditionClassName, classloader);
	return (Condition) BeanUtils.instantiateClass(conditionClass);
}

BeanUtils.instantiateClass 走无参构造反射 new 实例。结合 3.1 节"引擎无缓存"的事实,推论非常直接:每次 shouldSkip 调用,列表里每个条件都会被重新实例化一次。条件类必须有无参构造、必须无状态——你在条件里放一个计数器或缓存字段,每次评估都是全新对象,计数器永远是 0。Spring 官方条件实现(如 ProfileCondition)全部是无字段的瘦类,正是这个约束的产物。

第三,AnnotationAwareOrderComparator.sort(conditions):多条件按顺序短路。 排序依据是 Spring 的 Ordered 接口与 @Order 注解。由于 3.1.2 的循环是"遇到第一个不匹配就 return true",排序直接决定哪个条件先被求值、哪个条件优先短路。性能敏感时,应把"判定便宜、失败概率高"的条件(如属性开关)用 @Order(Ordered.HIGHEST_PRECEDENCE) 排到前面,把"要扫描类路径、耗时"的条件排到后面——一旦前者失败,后者根本不会被实例化和求值。Condition 接口 javadoc 里专门写了这段(见 Condition.java 第 35-39 行),不是可选项。

3.3 ConditionContextImpl:条件能看到的世界长什么样

上下文实现是 ConditionEvaluator 的私有静态内部类(第 146-232 行)。先看字段与构造(第 148-166 行):

private static class ConditionContextImpl implements ConditionContext {

	private final @Nullable BeanDefinitionRegistry registry;
	private final @Nullable ConfigurableListableBeanFactory beanFactory;
	private final Environment environment;
	private final ResourceLoader resourceLoader;
	private final @Nullable ClassLoader classLoader;

	public ConditionContextImpl(@Nullable BeanDefinitionRegistry registry,
			@Nullable Environment environment, @Nullable ResourceLoader resourceLoader) {
		this.registry = registry;
		this.beanFactory = deduceBeanFactory(registry);
		this.environment = (environment != null ? environment : deduceEnvironment(registry));
		this.resourceLoader = (resourceLoader != null ? resourceLoader : deduceResourceLoader(registry));
		this.classLoader = deduceClassLoader(resourceLoader, this.beanFactory);
	}
	……
}

设计模式很典型:构造函数收到什么就用什么,收到 null 就从 registry 这个"万能入口"反推。三个 deduce 静态方法(第 168-205 行):

private static @Nullable ConfigurableListableBeanFactory deduceBeanFactory(@Nullable BeanDefinitionRegistry source) {
	if (source instanceof ConfigurableListableBeanFactory configurableListableBeanFactory) {
		return configurableListableBeanFactory;
	}
	if (source instanceof ConfigurableApplicationContext configurableApplicationContext) {
		return configurableApplicationContext.getBeanFactory();
	}
	return null;
}

private static Environment deduceEnvironment(@Nullable BeanDefinitionRegistry source) {
	if (source instanceof EnvironmentCapable environmentCapable) {
		return environmentCapable.getEnvironment();
	}
	return new StandardEnvironment();
}

private static ResourceLoader deduceResourceLoader(@Nullable BeanDefinitionRegistry source) {
	if (source instanceof ResourceLoader resourceLoader) {
		return resourceLoader;
	}
	return new DefaultResourceLoader();
}

逐句理解:

  • deduceBeanFactory:在 AnnotationConfigApplicationContext 这种场景下,registry 本身就是 DefaultListableBeanFactory(它实现了 BeanDefinitionRegistry 又实现了 ConfigurableListableBeanFactory),直接 instanceof 拿到;如果 registry 是 ApplicationContext(如某些非典型用法),再从 context 里取 beanFactory;都不是就给 null。getBeanFactory() 接口因此标注 @Nullable——条件实现里若要调它,必须自己判空。
  • deduceEnvironment:registry 若实现了 EnvironmentCapable(应用上下文都实现了),用它的 environment;实在不行 new 一个 StandardEnvironment 兜底——注意这个兜底 environment 不是用户配置的那个,它没有用户的 profile 和属性源!这意味着在"裸 ClassPathScanningCandidateComponentProvider"这种脱离上下文的场景下,条件里读到的环境可能是个空环境。这正是 getRegistry() javadoc 警告的"unusual case"。
  • deduceResourceLoader 同理,兜底 new DefaultResourceLoader()。
  • deduceClassLoader:优先取 resourceLoader 的 classLoader,其次 beanFactory 的,最后 ClassUtils.getDefaultClassLoader()——三级回退保证条件里 ClassUtils.isPresent(...) 永远有个可用的类加载器。

五个 getter 里唯一带断言的是 getRegistry()(第 207-211 行):

@Override
public BeanDefinitionRegistry getRegistry() {
	Assert.state(this.registry != null, "No BeanDefinitionRegistry available");
	return this.registry;
}

registry 不可用(null)时直接 IllegalStateException,而不是悄悄返回 null——这是有意的:让"条件错误地依赖了注册表"尽早暴露,而不是静默放行所有 bean。

3.4 PARSE 阶段调用点:processConfigurationClass 的第一道闸门

引擎讲完了,现在看谁在什么时机调用它。第一处是 ConfigurationClassParser.processConfigurationClass(ConfigurationClassParser.java 第 248-251 行):

protected void processConfigurationClass(ConfigurationClass configClass, Predicate<String> filter) {
	if (this.conditionEvaluator.shouldSkip(configClass.getMetadata(), ConfigurationPhase.PARSE_CONFIGURATION)) {
		return;
	}
	……

就两行,但位置决定了语义:它是整个配置类处理流程的第一行。一旦 shouldSkip 返回 true,方法立刻 return,后续的一切——类层次递归处理、@ComponentScan 解析、@Import 收集、@Bean 方法登记、superclass 链遍历——全部不发生。被跳过的配置类甚至不会被放进 this.configurationClasses 这个解析结果集(该集合的 put 在方法末尾第 294 行,skip 路径走不到)。

逐段对照完整方法(第 248-295 行)理解"不 skip 时会发生什么",从而知道 skip 到底否决了什么:

ConfigurationClass existingClass = this.configurationClasses.get(configClass);
if (existingClass != null) {
	if (configClass.isImported()) { … mergeImportedBy … return; }
	else if (configClass.isScanned()) { … scanned 不覆盖 import … return; }
	else { … 显式定义替换旧 import … }
}

SourceClass sourceClass = null;
try {
	sourceClass = asSourceClass(configClass, filter);
	do {
		sourceClass = doProcessConfigurationClass(configClass, sourceClass, filter);
	}
	while (sourceClass != null);   // 沿 superclass 链向上递归
}
catch (IOException ex) {
	throw new BeanDefinitionStoreException(
			"I/O failure while processing configuration class [" + sourceClass + "]", ex);
}

this.configurationClasses.put(configClass, configClass);

几个与条件语义直接相关的点:

  1. PARSE 阶段否决是"连根拔"式的。被跳过的配置类不进 configurationClasses,意味着后续 ConfigurationClassBeanDefinitionReader.loadBeanDefinitions 遍历时根本看不到它——它的所有 @Bean 方法、@ImportResource、ImportBeanDefinitionRegistrar、@ComponentScan 扫出来的组件,统统不存在。这就是 @Conditional 标在类上与标在方法上的量级差异:类上否决的是整个配置类生态。
  2. doProcessConfigurationClass 内部还会触发组件扫描与 @Import 处理,而这些子流程里又可能再调 processConfigurationClass(递归)或 processImports(第 636 行 processConfigurationClass(candidate.asConfigClass(configClass), filter))——也就是说,被 import 进来的每一个配置类、被 @ComponentScan 扫出来的每一个候选类,都会再次走到这个 shouldSkip(..., PARSE_CONFIGURATION) 门前。条件评估不是一次性事件,而是沿解析树的每条边各发生一次。
  3. 异常包装:doProcessConfigurationClass 抛出的 IOException 会被包成 BeanDefinitionStoreException;而 shouldSkip 内部 matches 抛出的运行时异常不在这里被 catch,会一路穿透到 parse() 外层第 194-197 行:
catch (BeanDefinitionStoreException ex) {
	throw ex;
}
catch (Throwable ex) {
	throw new BeanDefinitionStoreException(
			"Failed to parse configuration class [" + bd.getBeanClassName() + "]", ex);
}

即:条件实现里抛异常 = 容器启动直接失败(BeanDefinitionStoreException),不会"跳过这个类然后继续"。这是新手最容易误解的一点——条件返回 false 是"安静地跳过",条件抛异常是"启动爆炸"。

3.5 REGISTER_BEAN 阶段调用点之一:@Bean 方法级条件

第二处显式 phase 调用在 ConfigurationClassBeanDefinitionReader.loadBeanDefinitionsForBeanMethod(ConfigurationClassBeanDefinitionReader.java 第 190-202 行):

private void loadBeanDefinitionsForBeanMethod(BeanMethod beanMethod) {
	ConfigurationClass configClass = beanMethod.getConfigurationClass();
	MethodMetadata metadata = beanMethod.getMetadata();
	String methodName = metadata.getMethodName();

	// Do we need to mark the bean as skipped by its condition?
	if (this.conditionEvaluator.shouldSkip(metadata, ConfigurationPhase.REGISTER_BEAN)) {
		configClass.skippedBeanMethods.add(methodName);
		return;
	}
	if (configClass.skippedBeanMethods.contains(methodName)) {
		return;
	}
	……

这段代码回答了一个经典问题:类已经在 PARSE 阶段通过了,为什么 @Bean 方法上的条件还要再评估一次? 答案在于阶段分离:

  • PARSE 阶段评估的是"类级"元数据(configClass.getMetadata(),类型是 AnnotationMetadata)。@Bean 方法上的 @Conditional 标在方法上,PARSE 阶段的 shouldSkip 拿到的是类元数据,getAllAnnotationAttributes 根本读不到方法级注解——所以类级条件在 PARSE 跑,方法级条件必须等到遍历 @Bean 方法时(REGISTER_BEAN 阶段)才有机会被看到。
  • 注意传给引擎的 metadata 是 beanMethod.getMetadata()(MethodMetadata),phase 写死 REGISTER_BEAN。
  • 跳过动作不是简单 return:configClass.skippedBeanMethods.add(methodName) 会把方法名记进 ConfigurationClass 的 skippedBeanMethods 集合,随后 if (configClass.skippedBeanMethods.contains(methodName)) return; 是一个去重短路——当同一个配置类因继承/增强出现重复遍历路径时,被跳过的方法不会重复评估(这里的"缓存"粒度是方法名集合,不是条件评估结果)。
  • 跳过之后:方法对应的 ConfigurationClassBeanDefinition 根本不创建,后面第 220 行起的 new ConfigurationClassBeanDefinition(...)、第 225 行 isOverriddenByExistingDefinition、第 234 行 static/instance 工厂方法设定统统不执行。注册表中没有这个 bean 名。

3.6 REGISTER_BEAN 阶段调用点之二:TrackedConditionEvaluator 的类级复审

第三处,也是最容易被忽略的一处:ConfigurationClassBeanDefinitionReader 的私有内部类 TrackedConditionEvaluator(第 503-530 行):

private class TrackedConditionEvaluator {

	private final Map<ConfigurationClass, Boolean> skipped = new HashMap<>();

	public boolean shouldSkip(ConfigurationClass configClass) {
		Boolean skip = this.skipped.get(configClass);
		if (skip == null) {
			if (configClass.isImported()) {
				boolean allSkipped = true;
				for (ConfigurationClass importedBy : configClass.getImportedBy()) {
					if (!shouldSkip(importedBy)) {
						allSkipped = false;
						break;
					}
				}
				if (allSkipped) {
					// The config classes that imported this one were all skipped, therefore we are skipped...
					skip = true;
				}
			}
			if (skip == null) {
				skip = conditionEvaluator.shouldSkip(configClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN);
			}
			this.skipped.put(configClass, skip);
		}
		return skip;
	}
}

它的调用入口在 loadBeanDefinitionsForConfigurationClass(第 132-142 行):

private void loadBeanDefinitionsForConfigurationClass(
		ConfigurationClass configClass, TrackedConditionEvaluator trackedConditionEvaluator) {

	if (trackedConditionEvaluator.shouldSkip(configClass)) {
		String beanName = configClass.getBeanName();
		if (StringUtils.hasLength(beanName) && this.registry.containsBeanDefinition(beanName)) {
			this.registry.removeBeanDefinition(beanName);
		}
		this.importRegistry.removeImportingClass(configClass.getMetadata().getClassName());
		return;
	}

	if (configClass.isImported()) {
		registerBeanDefinitionForImportedConfigurationClass(configClass);
	}
	for (BeanMethod beanMethod : configClass.getBeanMethods()) {
		loadBeanDefinitionsForBeanMethod(beanMethod);
	}
	……
}

这两段源码合起来讲清了三件事:

第一,类级条件为什么在 REGISTER_BEAN 阶段还要复审一次? 回忆 3.1.2 的阶段过滤逻辑:如果一个条件是普通 Condition(不是 ConfigurationCondition),它在 PARSE 和 REGISTER_BEAN 两个阶段都会被求值。PARSE 阶段它已经"放行了"配置类,为什么 REGISTER_BEAN 阶段还要再问一遍?因为有些条件的答案会随时间变化——最典型的就是检查 bean 定义是否已存在的条件("如果用户没自己配过这个 bean,才注册默认的")。PARSE 早期用户 bean 定义还没注册,REGISTER_BEAN 阶段用户配置类都解析完了,答案才稳定。普通条件作者虽然不写 phase,但引擎两个阶段都叫它,它自己根据 ConditionContext 里 registry 的实时状态做判断即可。而对于明确声明了 PARSE_CONFIGURATION 的 ConfigurationCondition,第二次复审时 requiredPhase != phase,引擎自动跳过它、不会重复评估——这就是阶段声明的保护作用。

第二,importedBy 传递性否决。 if (configClass.isImported()) { for (ConfigurationClass importedBy : configClass.getImportedBy()) { if (!shouldSkip(importedBy)) { allSkipped = false; break; } } }——一个被 @Import 进来的配置类,如果所有导入它的配置类都被跳过了,那它自己也应该被跳过。这是传递依赖:导入者被砍了,被导入者没有存在的意义。注意这里是递归调用 shouldSkip(importedBy),配合外层 skipped Map,天然带记忆化——同一个 importedBy 不会重复评估。

第三,跳过之后的收尾动作很硬核。 一旦复审决定跳过,if (StringUtils.hasLength(beanName) && this.registry.containsBeanDefinition(beanName)) this.registry.removeBeanDefinition(beanName);——把这个配置类已经注册进去的 bean 定义再删回来。为什么会"已经注册"?因为 @Import 一个配置类时,它的 bean 定义可能在解析阶段就被别的路径登记了(或者组件扫描先于条件复审把类注册成了 bean 定义)。条件否决是可以"撤回"一次注册的。同时 importRegistry.removeImportingClass(...) 把导入关系记录也清理掉,保持 ImportRegistry 与最终注册表一致。

skipped 这个 Map 就是 7.0.9 里条件评估唯一的结果缓存:它是 TrackedConditionEvaluator 的实例字段,生命周期等于一次 loadBeanDefinitions(...) 调用(第 121-126 行):

public void loadBeanDefinitions(Set<ConfigurationClass> configurationModel) {
	TrackedConditionEvaluator trackedConditionEvaluator = new TrackedConditionEvaluator();
	for (ConfigurationClass configClass : configurationModel) {
		loadBeanDefinitionsForConfigurationClass(configClass, trackedConditionEvaluator);
	}
}

每轮 loadBeanDefinitions new 一个新的 evaluator,skipped Map 随之重建——缓存不跨轮次、不跨容器。这与旧版资料里"ConditionEvaluator.skipCache 跨整个解析生命周期缓存"的描述完全不同,再次印证 3.1 节的版本差异判断。

3.7 两阶段调用点全景对照

把三处显式调用与一处推导式调用并列,全章至此闭环:

调用点文件与位置传入 phase评估对象否决后果
processConfigurationClassConfigurationClassParser.java 第 249 行PARSE_CONFIGURATION配置类/组件类级元数据整个类不进解析结果,连带 @Bean/@Import/@ComponentScan 全跳过
isConditionMatch(组件扫描)ClassPathScanningCandidateComponentProvider.java 第 559 行null→推导扫描到的类元数据该类不注册为候选 bean 定义
doRegisterBean(编程式注册)AnnotatedBeanDefinitionReader.java 第 257 行null→推导编程式注册的类元数据该类不注册 bean 定义
loadBeanDefinitionsForBeanMethodConfigurationClassBeanDefinitionReader.java 第 196 行REGISTER_BEAN@Bean 方法元数据该方法不产生 bean 定义,记入 skippedBeanMethods
TrackedConditionEvaluator.shouldSkipConfigurationClassBeanDefinitionReader.java 第 524 行REGISTER_BEAN配置类元数据 + importedBy 传递已注册的类 bean 定义被 remove,导入关系清理

这张表回答了任务书的核心问题:"类级条件与 @Bean 方法级条件的评估时机差异"——类级条件在解析阶段就决定整个类的生死,方法级条件要等到所有配置类解析完毕、bean 定义开始落地时才逐方法裁决。

3.8 @Profile 的完整实现:从注解到 profile 表达式求值

3.8.1 ProfileCondition 的源码

@Profile 的条件实现是 ProfileCondition(ProfileCondition.java 第 33-50 行),包可见类:

class ProfileCondition implements Condition {

	@Override
	@SuppressWarnings("NullAway") // Reflection
	public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
		MultiValueMap<String, @Nullable Object> attrs = metadata.getAllAnnotationAttributes(Profile.class.getName());
		if (attrs != null) {
			for (Object value : attrs.get("value")) {
				if (context.getEnvironment().matchesProfiles((String[]) value)) {
					return true;
				}
			}
			return false;
		}
		return true;
	}

}

逐行精读:

  1. 它不读 @Conditional 的 value,而是回头读 @Profile 自己的属性。metadata.getAllAnnotationAttributes(Profile.class.getName()) 取出 @Profile 注解上 value 数组的所有声明位置。注意这里没有传第二个布尔参数(对比 3.2 节 getConditionClasses 里传了 true)——这里用的是"不带 classValuesAsString"的版本,因为 @Profile.value() 是字符串数组,不涉及 Class 转换。
  2. 外层 for (Object value : attrs.get("value")) 遍历的是"多份 @Profile 声明":类上直接标了一个 @Profile({"p1","p2"}) 是一份(String[]{"p1","p2"});如果 @Profile 又被组合进自定义注解 @Prod 里,@Prod 标注的类上 getAllAnnotationAttributes(Profile.class.getName()) 会聚合出多份 value 数组。每一份都是一个独立的 String[]。
  3. 份与份之间是"或"关系:for 循环里任何一份 matchesProfiles 返回 true,立刻 return true;全部份都不匹配才 return false。也就是说"直接标 @Profile("p1")"或"通过组合注解 @Prod(内含 @Profile("p2"))",只要 p1、p2 任一激活就放行。
  4. 份内部是"或"关系 + profile 表达式:每一份 String[] 交给 environment.matchesProfiles(...),而这个方法本身对数组也是"任一命中即 true"。
  5. attrs == null 时返回 true——这是防御性兜底:理论上 ProfileCondition 只在带 @Profile 的元数据上被收集,attrs 不应为空;一旦为空,放行而不是否决,避免误杀。
  6. @SuppressWarnings("NullAway") // Reflection 是 7.0.9 引入的静态分析标注,说明 attrs.get("value") 的返回在静态分析看来可能为 null,作者明确知道并接受(运行时这里一定有值)。
3.8.2 Environment.matchesProfiles:7.0.9 的新入口

任务书提示"ProfileCondition 读 environment.acceptsProfiles(Profiles.of(注解值))"——这是旧版本的写法,7.0.9 已经变了。以仓库为准,实际调用的是 Environment 接口上的默认方法 matchesProfiles(spring-core/src/main/java/org/springframework/core/env/Environment.java 第 108-114 行):

/**
 * Return whether the {@linkplain #getActiveProfiles() active profiles}
 * match <em>any</em> of the given {@code profile expressions} — for example,
 * {@code env.matchesProfiles(Profiles.of(profileExpressions))}.
 * @since 6.2
 */
default boolean matchesProfiles(String... profileExpressions) {
	return acceptsProfiles(Profiles.of(profileExpressions));
}

@since 6.2——这是 6.2 才加入的便捷默认方法,7.0.9 沿用。它把"Profiles.of(...) 包装 + acceptsProfiles(Profiles)"两步合成一步,ProfileCondition 正是直接调它。而老的 acceptsProfiles(String...) 在 Environment.java 第 133 行已经标了 @deprecated in favor of acceptsProfiles(Profiles) or matchesProfiles(String...)。

实现侧在 AbstractEnvironment(spring-core/src/main/java/org/springframework/core/env/AbstractEnvironment.java 第 396-412 行):

@Override
public boolean acceptsProfiles(Profiles profiles) {
	Assert.notNull(profiles, "Profiles must not be null");
	return profiles.matches(this::isProfileActive);
}

/**
 * Return whether the given profile is active, or if active profiles are empty
 * whether the profile should be active by default.
 */
protected boolean isProfileActive(String profile) {
	validateProfile(profile);
	Set<String> currentActiveProfiles = doGetActiveProfiles();
	return (currentActiveProfiles.contains(profile) ||
			(currentActiveProfiles.isEmpty() && doGetDefaultProfiles().contains(profile)));
}

逻辑三层:

  • profiles.matches(this::isProfileActive):把"单个 profile 名是否激活"的判定函数作为谓词传给 Profiles,由 Profiles 内部根据表达式语法(&、|、!、括号)做逻辑组合求值。这就是 @Profile("p1 & p2")、@Profile("!test") 这类表达式的求值引擎——语法解析在 ProfilesParser,真值表在 Profiles 的实现类里,Environment 只提供"单个 profile 是否激活"这一个原子判定。
  • isProfileActive 的第二半句是 default profile 机制:当没有任何 profile 被显式激活时,doGetActiveProfiles() 为空集,此时命中的是"默认 profile 集合"(defaultProfiles,初始含 default,保留名定义在 AbstractEnvironment.RESERVED_DEFAULT_PROFILE_NAME)。所以一个标了 @Profile("default") 的类在"啥也没配"的启动场景下会被装配——这解释了为什么测试环境里不设 active profiles 时某些 bean 仍然出现。
  • validateProfile(profile) 会拒绝 ! 开头的非法 profile 名(NOT 运算符只能出现在表达式字符串里,不能作为 profile 本名)、拒绝 null/空/纯空白。

被 @Deprecated(since = "5.1") 的旧方法 acceptsProfiles(String...)(第 379-394 行)在 7.0.9 里仍然保留实现,逻辑是手写的"遍历数组、! 前缀取反、任一命中即 true"——它不支持 &/| 表达式,只支持数组级 OR 与单个 !。ProfileCondition 已经不调它,但读者写自定义条件时若图省事调了它,会丢失表达式能力,应当走 matchesProfiles。

3.8.3 Profiles.of 与表达式语法

Profiles 是函数式接口(Profiles.java 第 34-73 行):

@FunctionalInterface
public interface Profiles {
	boolean matches(Predicate<String> isProfileActive);

	static Profiles of(String... profileExpressions) {
		return ProfilesParser.parse(profileExpressions);
	}
}

ProfilesParser.parse 把表达式字符串编译成一个 Predicate<String> 树(&=AND、|=OR、!=NOT、括号分组)。javadoc 给出的语法约束很重要:& 与 | 不能裸混写,"a & b | c" 非法,必须写成 "(a & b) | c"。@Profile 的 javadoc(Profile.java 第 60-63 行、第 72-75 行)与 Environment.java 第 124 行都举了同一个例子:@Profile({"p1", "!p2"}) 的语义是"p1 激活,或 p2 不激活"——数组元素之间是 OR,元素内部 ! 是 NOT。

3.8.4 @Profile 与 @Conditional 的关系小结

把本节收束成一张"调用链":

@Profile("p1 & p2")  ──(元注解)──▶  @Conditional(ProfileCondition.class)
                                            │
                              ConditionEvaluator.collectConditions
                                            │
                              new ProfileCondition()(无参反射实例化)
                                            │
                              matches(context, metadata)
                                            │
                    metadata.getAllAnnotationAttributes(Profile) → value
                                            │
                    context.getEnvironment().matchesProfiles("p1 & p2")
                                            │
                    Profiles.of(...) 解析表达式 → Profiles.matches(isProfileActive)
                                            │
                    AbstractEnvironment.isProfileActive(name)
                                            │
              doGetActiveProfiles().contains(name) 或 空激活集时 defaultProfiles.contains(name)

@Profile 没有任何"特权通道"——它就是一个用元注解绑死了 ProfileCondition 的普通组合注解。你完全可以照抄这个模式写一个 @EnabledOnFeature("xxx"),这就是 5.9 节要给的范式。

3.9 ImportSelector 与自动配置的桥梁

条件决定"候选要不要装",而自动配置要回答的是"到底有哪些候选该被拉进来"。框架层的答案是 @Import + ImportSelector。本节把这条桥的源码逐段拆开。

3.9.1 processImports:@Import 候选的四分类

ConfigurationClassParser.processImports(第 581-652 行)是 @Import 的总调度。它遍历所有 import 候选,按"候选类实现了什么接口"分四路:

private void processImports(ConfigurationClass configClass, SourceClass currentSourceClass,
		Collection<SourceClass> importCandidates, Predicate<String> filter, boolean checkForCircularImports) {

	if (importCandidates.isEmpty()) {
		return;
	}

	if (checkForCircularImports && isChainedImportOnStack(configClass)) {
		this.problemReporter.error(new CircularImportProblem(configClass, this.importStack));
	}
	else {
		this.importStack.push(configClass);
		try {
			for (SourceClass candidate : importCandidates) {
				if (candidate.isAssignable(ImportSelector.class)) {
					// Candidate class is an ImportSelector -> delegate to it to determine imports
					Class<?> candidateClass = candidate.loadClass();
					ImportSelector selector = ParserStrategyUtils.instantiateClass(candidateClass, ImportSelector.class,
							this.environment, this.resourceLoader, this.registry);
					Predicate<String> selectorFilter = selector.getExclusionFilter();
					if (selectorFilter != null) {
						filter = filter.or(selectorFilter);
					}
					if (selector instanceof DeferredImportSelector deferredImportSelector) {
						this.deferredImportSelectorHandler.handle(configClass, deferredImportSelector);
					}
					else {
						String[] importClassNames = selector.selectImports(currentSourceClass.getMetadata());
						Collection<SourceClass> importSourceClasses = asSourceClasses(importClassNames, filter);
						processImports(configClass, currentSourceClass, importSourceClasses, filter, false);
					}
				}
				else if (candidate.isAssignable(BeanRegistrar.class)) { … }
				else if (candidate.isAssignable(ImportBeanDefinitionRegistrar.class)) { … }
				else {
					// Candidate class not an ImportSelector or ImportBeanDefinitionRegistrar ->
					// process it as an @Configuration class
					this.importStack.registerImport(
							currentSourceClass.getMetadata(), candidate.getMetadata().getClassName());
					processConfigurationClass(candidate.asConfigClass(configClass), filter);
				}
			}
		}
		……
	}
}

逐段讲与本章主题相关的部分:

  1. ImportSelector 的实例化用 ParserStrategyUtils.instantiateClass,而不是裸 BeanUtils.instantiateClass——这个工具方法会检查选择器是否实现了 EnvironmentAware/BeanFactoryAware/BeanClassLoaderAware/ResourceLoaderAware,在实例化后回调注入,也支持构造器注入。这就是 ImportSelector javadoc(ImportSelector.java 第 30-47 行)列出的那一串 Aware 接口的落点。
  2. getExclusionFilter()(5.2.4 引入,ImportSelector.java 第 81-83 行):选择器可以返回一个 Predicate<String>,对它"transitively 导入"的所有类名做二次过滤——命中即跳过,连类文件都不 ASM 读。自动配置里大量类名经过选择器转交给下一层,这个排除过滤器是性能与冲突规避的关键挂钩点。
  3. 普通 ImportSelector 是同步递归的:selectImports(...) 立刻拿到类名数组,包成 SourceClass 后立刻递归调 processImports。也就是说普通选择器选中的类,在"当前配置类解析的当下"就被解析,它们上面的 @Conditional(PARSE_CONFIGURATION) 也就在此刻评估。
  4. DeferredImportSelector 走另一条路:不立刻 selectImports,而是 this.deferredImportSelectorHandler.handle(configClass, deferredImportSelector) 先存起来。
  5. 第四路(else 分支):既不是选择器也不是 registrar,就把候选类当普通 @Configuration 类处理——processConfigurationClass(candidate.asConfigClass(configClass), filter)。注意这里会再次走进 3.4 节那道 PARSE 阶段 shouldSkip 闸门。被 import 进来的类,其类级条件同样会被评估。
3.9.2 DeferredImportSelectorHandler:先攒后发

DeferredImportSelectorHandler 是 ConfigurationClassParser 的私有内部类(第 801-840 行):

private class DeferredImportSelectorHandler {

	private @Nullable List<DeferredImportSelectorHolder> deferredImportSelectors = new ArrayList<>();

	void handle(ConfigurationClass configClass, DeferredImportSelector importSelector) {
		DeferredImportSelectorHolder holder = new DeferredImportSelectorHolder(configClass, importSelector);
		if (this.deferredImportSelectors == null) {
			DeferredImportSelectorGroupingHandler handler = new DeferredImportSelectorGroupingHandler();
			handler.register(holder);
			handler.processGroupImports();
		}
		else {
			this.deferredImportSelectors.add(holder);
		}
	}

	void process() {
		List<DeferredImportSelectorHolder> deferredImports = this.deferredImportSelectors;
		this.deferredImportSelectors = null;
		try {
			if (deferredImports != null) {
				DeferredImportSelectorGroupingHandler handler = new DeferredImportSelectorGroupingHandler();
				deferredImports.sort(DEFERRED_IMPORT_COMPARATOR);
				deferredImports.forEach(handler::register);
				handler.processGroupImports();
			}
		}
		finally {
			this.deferredImportSelectors = new ArrayList<>();
		}
	}
}

这段代码藏着两个精妙设计:

第一,"攒着"的时机由一个可空 List 控制。 deferredImportSelectors == new ArrayList<>() 时是"收集期",handle 只是 add(holder);process() 一进来先把字段置 null(第 827 行),随后若在"已置 null"状态下又收到新的 handle(例如某个延迟选择器 import 进来的类又触发了一个新的延迟选择器),就走 if (this.deferredImportSelectors == null) 分支——当场建一个 GroupingHandler 立刻处理,不再攒。这个"边处理边可能混入新成员"的设计,保证延迟选择器递归嵌套时语义仍然正确。finally 里再 new ArrayList<>() 复位,供下一轮(AOT/嵌套场景)使用。

第二,process() 在整个 parse() 的最后被调用。 调用点在 ConfigurationClassParser.parse(Set) 末尾,第 200 行:

		}
		this.deferredImportSelectorHandler.process();
	}

也就是说:所有普通配置类、普通 ImportSelector、组件扫描都处理完之后,才轮到 DeferredImportSelector 执行 selectImports。这正是 DeferredImportSelector javadoc(DeferredImportSelector.java 第 24-27 行)那句"runs after all @Configuration beans have been processed. This type of selector can be particularly useful when the selected imports are @Conditional"的源码落点——延迟选择器选中的类上的 @Conditional,是在"全世界其他配置类都解析完"之后才评估的,此时 ConditionContext 里能看到的注册表状态最接近最终态,条件判断最准确。这就是 Spring Boot 自动配置必须用 DeferredImportSelector 而不是普通 ImportSelector 的框架层原因(AutoConfigurationImportSelector 属于 spring-boot-autoconfigure,不在本仓库,本章只指出这个依赖关系,不展开其源码)。

3.9.3 Group:跨选择器的聚合与排序

多个 DeferredImportSelector 怎么排序、怎么合批?看 DeferredImportSelectorGroupingHandler(第 843-889 行):

private class DeferredImportSelectorGroupingHandler {

	private final Map<Object, DeferredImportSelectorGrouping> groupings = new LinkedHashMap<>();
	private final Map<AnnotationMetadata, ConfigurationClass> configurationClasses = new HashMap<>();

	void register(DeferredImportSelectorHolder deferredImport) {
		Class<? extends Group> group = deferredImport.getImportSelector().getImportGroup();
		DeferredImportSelectorGrouping grouping = this.groupings.computeIfAbsent(
				(group != null ? group : deferredImport),
				key -> new DeferredImportSelectorGrouping(createGroup(group)));
		grouping.add(deferredImport);
		this.configurationClasses.put(deferredImport.getConfigurationClass().getMetadata(),
				deferredImport.getConfigurationClass());
	}

	void processGroupImports() {
		for (DeferredImportSelectorGrouping grouping : this.groupings.values()) {
			Predicate<String> filter = grouping.getCandidateFilter();
			grouping.getImports().forEach(entry -> {
				ConfigurationClass configurationClass = this.configurationClasses.get(entry.getMetadata());
				Assert.state(configurationClass != null, "ConfigurationClass must not be null");
				try {
					processImports(configurationClass, asSourceClass(configurationClass, filter),
							Collections.singleton(asSourceClass(entry.getImportClassName(), filter)),
							filter, false);
				}
				……
			});
		}
	}
}

要点:

  • 分组键是 getImportGroup() 返回的 Group 类:返回 null 的选择器,各自成组(key 用 selector 自己);返回同一个 Group 类的多个选择器,被合到一个 DeferredImportSelectorGrouping 里,共享一个 Group 实例。这就是"Group 接口用于跨不同选择器聚合结果"的机制——自动配置里多个 @EnableXxx 若声明同一个 Group,可以把大家的候选统一排序、去重后再导入。
  • groupings 是 LinkedHashMap:组的处理顺序 = 注册顺序;而注册前 deferredImports.sort(DEFERRED_IMPORT_COMPARATOR)(第 831 行)先排过一轮,比较器(第 114-115 行)是 AnnotationAwareOrderComparator.INSTANCE.compare(o1.getImportSelector(), o2.getImportSelector())——即按 Ordered/@Order 排延迟选择器。
  • DeferredImportSelectorGrouping.getImports()(第 931-937 行)是组的求值点:
Iterable<Group.Entry> getImports() {
	for (DeferredImportSelectorHolder deferredImport : this.deferredImports) {
		this.group.process(deferredImport.getConfigurationClass().getMetadata(),
				deferredImport.getImportSelector());
	}
	return this.group.selectImports();
}

先把组内每个选择器逐个喂给 group.process(metadata, selector),再统一 group.selectImports() 吐出 Entry 列表。默认实现 DefaultDeferredImportSelectorGroup(第 952-967 行)就是简单收集:

private static class DefaultDeferredImportSelectorGroup implements Group {

	private final List<Entry> imports = new ArrayList<>();

	@Override
	public void process(AnnotationMetadata metadata, DeferredImportSelector selector) {
		for (String importClassName : selector.selectImports(metadata)) {
			this.imports.add(new Entry(metadata, importClassName));
		}
	}

	@Override
	public Iterable<Entry> selectImports() {
		return this.imports;
	}
}
  • Entry 记住"是谁 import 的":Group.Entry 持有 AnnotationMetadata metadata(导入方)与 importClassName(被导入者),processGroupImports 靠 entry.getMetadata() 反查回 configurationClasses Map 里的那个 ConfigurationClass,再走一遍标准 processImports——于是延迟选中的类,在导入时依然要过 PARSE 阶段条件闸门(3.4 节)。

至此,"自动配置的框架基石"这句话可以落到实处:DeferredImportSelector 提供"全部配置类解析完再选"的时机,Group 提供跨选择器合批排序,@Conditional/ConfigurationCondition 提供对选中类的逐一票否决,@ComponentScan 的 TypeFilter 提供类路径过滤——spring-boot-autoconfigure 的 AutoConfigurationImportSelector 只是在这套机制之上做了"从 spring.factories/自动配置索引文件读类名"这一件事。本仓库不含那些类,但机制全部在这里。

3.12 补深:阶段推导分支的判定依据——isConfigurationCandidate 到底在判什么

3.1.1 节留下一个悬念:单参数 shouldSkip(metadata) 在 phase == null 时,靠 ConfigurationClassUtils.isConfigurationCandidate(annotationMetadata) 把场景二分为 PARSE 与 REGISTER_BEAN。这个判定函数(ConfigurationClassUtils.java 第 174-189 行)不长,但每个判断都值得咬碎:

static boolean isConfigurationCandidate(AnnotationMetadata metadata) {
	// Do not consider an interface or an annotation...
	if (metadata.isInterface()) {
		return false;
	}

	// Any of the typical annotations found?
	for (String indicator : candidateIndicators) {
		if (metadata.isAnnotated(indicator)) {
			return true;
		}
	}

	// Finally, let's look for @Bean methods...
	return hasBeanMethods(metadata);
}

配套的 candidateIndicators 集合在第 79-83 行:

private static final Set<String> candidateIndicators = Set.of(
		Component.class.getName(),
		ComponentScan.class.getName(),
		Import.class.getName(),
		ImportResource.class.getName());

逐句理解推导的含义:

  1. 接口直接 false:metadata.isInterface() 为真时返回 false,于是 phase 推导落到 REGISTER_BEAN。接口上即使标了 @Conditional,也不会按配置类去解析——这与 6.4 节"条件标在接口上不生效"互为印证:接口既不会成为配置候选,它上面的条件自然也不会被 PARSE 流程裁决。
  2. 四个指示注解任一命中即 true:@Component(及其所有派生,因为 isAnnotated 沿元注解链展开)、@ComponentScan、@Import、@ImportResource——带这些注解的类被视为"配置候选",走 PARSE。注意 @Configuration 本身不在这个集合里:因为 @Configuration 元注解上就标了 @Component(@Configuration 是 @Component 的派生),isAnnotated(Component) 自然为 true。
  3. 兜底看有没有 @Bean 方法:hasBeanMethods(metadata) → metadata.hasAnnotatedMethods(Bean.class.getName())。一个类即使没有任何指示注解,只要里面有 @Bean 方法,它也算配置候选。这是给"没标 @Configuration 但写了 @Bean"的 lite 配置类准备的。

于是推导分支的完整语义可以写成一句:

一个带 @Conditional 的元数据,如果它描述的是一个"可能要被当成配置类解析的类"(非接口、带组件/扫描/导入标记、或含 @Bean 方法),条件按 PARSE_CONFIGURATION 评估;否则(比如一个普通组件类被别的方式注册进来、或一个方法级元数据走错了分支)按 REGISTER_BEAN 评估。

这个推导只影响"碰巧实现了 ConfigurationCondition 的条件"拿到哪个 phase;对普通条件,phase 是什么都不影响——普通条件两个阶段都跑(3.1.2)。理解这一点,就理解了为什么任务书把"PARSE 阶段跳过时 REGISTER 阶段的处理"专门列为考点:一个声明了 PARSE 的条件,在 REGISTER 阶段被引擎静默跳过;它在 PARSE 阶段否决掉的配置类,到了 REGISTER 阶段 TrackedConditionEvaluator 复审时不会再问它一遍——因为 requiredPhase == PARSE_CONFIGURATION != REGISTER_BEAN,条件被跳过。真正在 REGISTER 阶段复审的,是普通条件(恒跑)与显式声明 REGISTER 的条件。

3.13 补深:条件实例化 vs 选择器实例化——两条不同的反射路径

本章已经出现过两次"反射 new 一个策略对象",但它们走的是两条不同的路,值得对照清楚:

路径一:条件实例化(ConditionEvaluator.getCondition,第 137-140 行):

private Condition getCondition(String conditionClassName, @Nullable ClassLoader classloader) {
	Class<?> conditionClass = ClassUtils.resolveClassName(conditionClassName, classloader);
	return (Condition) BeanUtils.instantiateClass(conditionClass);
}

裸 BeanUtils.instantiateClass——只调无参构造,不回调任何 Aware 接口,不支持构造器注入。条件实现里想拿 environment?只能通过 ConditionContext 参数拿(这正是 ConditionContext 存在的意义),不能指望 EnvironmentAware。这解释了为什么 Condition 接口的方法签名是 matches(ConditionContext, AnnotatedTypeMetadata)——框架把一切外部依赖显式塞给你,而不是让你实现 Aware 接口自己捞。

路径二:ImportSelector / Registrar 实例化(ParserStrategyUtils.instantiateClass,第 56-69 行):

static <T> T instantiateClass(Class<?> clazz, Class<T> assignableTo, Environment environment,
		ResourceLoader resourceLoader, BeanDefinitionRegistry registry) {

	Assert.notNull(clazz, "Class must not be null");
	Assert.isAssignable(assignableTo, clazz);
	if (clazz.isInterface()) {
		throw new BeanInstantiationException(clazz, "Specified class is an interface");
	}
	ClassLoader classLoader = (registry instanceof ConfigurableBeanFactory cbf ?
			cbf.getBeanClassLoader() : resourceLoader.getClassLoader());
	T instance = (T) createInstance(clazz, environment, resourceLoader, registry, classLoader);
	ParserStrategyUtils.invokeAwareMethods(instance, environment, resourceLoader, registry, classLoader);
	return instance;
}

差别在两个函数:

  • createInstance(第 71-88 行):如果策略类只有一个构造器且带参数,就走 resolveArgs 按参数类型匹配注入(resolveParameter,第 102-119 行:Environment/ResourceLoader/BeanFactory/ClassLoader 四种类型可注入,其他类型直接 IllegalStateException);否则退回到无参构造。这就是 ImportSelector javadoc 里"Alternatively, the class may provide a single constructor with one or more of the following supported parameter types"的落点。
  • invokeAwareMethods(第 121-139 行):实例化后检查 instanceof EnvironmentAware / BeanFactoryAware / BeanClassLoaderAware / ResourceLoaderAware,逐个回调 setter。

为什么条件不走这条路? 因为条件的"注入"需求已经被 ConditionContext 参数满足了——框架在每次 matches 调用时把上下文作为实参递进去,比 Aware 接口更直接、更无状态。而 ImportSelector 是在 processImports 里被 new 一次的,它需要在 selectImports 时持有 environment/registry,Aware 回调是合适的载体。两种实例化哲学的差异,本质是"条件每次调用传参"vs"选择器一次性注入字段"。

3.14 补深:@Import 的元注解收集与条件的接力

3.9 节讲了 processImports 的四分类,但还有一个前置步骤容易被忽略:@Import 是怎么"被收集到"的?看 collectImports(第 564-579 行):

private void collectImports(SourceClass sourceClass, Set<SourceClass> imports, Set<SourceClass> visited)
		throws IOException {

	if (visited.add(sourceClass)) {
		for (SourceClass ifc : sourceClass.getInterfaces()) {
			collectImports(ifc, imports, visited);
		}
		for (SourceClass annotation : sourceClass.getAnnotations()) {
			String annName = annotation.getMetadata().getClassName();
			if (!annName.equals(Import.class.getName())) {
				collectImports(annotation, imports, visited);
			}
		}
		imports.addAll(sourceClass.getAnnotationAttributes(Import.class.getName(), "value"));
	}
}

它递归做两件事:沿接口链和元注解链向上找所有 @Import。这意味着:@EnableXxx 这种组合注解(自身标了 @Import(YyySelector.class))标注在配置类上时,YyySelector 会被递归收集出来。

这与条件体系的接力点在于:@Import 进来的每一个候选类,最终都走 processImports,普通类走第四路 processConfigurationClass(candidate.asConfigClass(configClass), filter)(第 636 行)——再次经过 PARSE 阶段条件闸门(3.4 节)。也就是说,延迟选择器在 processGroupImports 里吐出来的自动配置类,每一个都会被重新过一遍"自己的 @Conditional 满不满足"。选择器负责"提出候选名单",条件负责"逐个投票否决"——两者接力完成自动配置。processGroupImports 第 866-868 行调 processImports(..., false) 时传的 checkForCircularImports=false,也说明这条"延迟导入再裁决"的路径是被精心设计过的正常通道,而不是异常回路。

3.15 补深:profile 表达式是怎么被求值的——ProfilesParser 递归下降

3.8.2 节说"Profiles.of 把表达式编译成谓词树",这一小节把编译过程打开。解析器在 spring-core/src/main/java/org/springframework/core/env/ProfilesParser.java。

入口 parse(String... expressions)(第 46-53 行):

static Profiles parse(String... expressions) {
	Assert.notEmpty(expressions, "Must specify at least one profile expression");
	Profiles[] parsed = new Profiles[expressions.length];
	for (int i = 0; i < expressions.length; i++) {
		parsed[i] = parseExpression(expressions[i]);
	}
	return new ParsedProfiles(expressions, parsed);
}

注意返回的是 ParsedProfiles 包装:数组里每个表达式独立解析成一棵谓词树,最后由 ParsedProfiles.matches 做"数组间 OR"(第 162-169 行):

@Override
public boolean matches(Predicate<String> activeProfiles) {
	for (Profiles candidate : this.parsed) {
		if (candidate.matches(activeProfiles)) {
			return true;
		}
	}
	return false;
}

这就是为什么 @Profile({"p1", "p2"}) 是"p1 或 p2"——数组元素各自是一棵树,任一树求值为 true 即放行。

单棵树的解析是递归下降 parseTokens(第 65-109 行),词法器是 StringTokenizer(expression, "()&|!", true)——把运算符和括号都当单字符 token 切出来。核心 switch:

switch (token) {
	case "(" -> {
		Profiles contents = parseTokens(expression, tokens, Context.PARENTHESIS);
		if (context == Context.NEGATE) {
			return contents;
		}
		elements.add(contents);
	}
	case "&" -> {
		assertWellFormed(expression, operator == null || operator == Operator.AND);
		operator = Operator.AND;
	}
	case "|" -> {
		assertWellFormed(expression, operator == null || operator == Operator.OR);
		operator = Operator.OR;
	}
	case "!" -> elements.add(not(parseTokens(expression, tokens, Context.NEGATE)));
	case ")" -> {
		Profiles merged = merge(expression, elements, operator);
		if (context == Context.PARENTHESIS) {
			return merged;
		}
		elements.clear();
		elements.add(merged);
		operator = null;
	}
	default -> {
		Profiles value = equals(token);
		if (context == Context.NEGATE) {
			return value;
		}
		elements.add(value);
	}
}

逐段理解:

  1. ( 递归进括号,上下文标记为 PARENTHESIS;遇到对应的 ) 时 merge(expression, elements, operator) 把括号内的元素用记录的 operator 合并成一棵子树返回。
  2. & 与 | 只记录 operator,不立即合并——assertWellFormed(expression, operator == null || operator == Operator.AND) 这句断言就是 Profiles javadoc 里"& 和 | 不能裸混写"的强制点:在一个没有括号分组的元素列表里,operator 一旦定为 AND,下一个 token 不能是 OR(反之亦然),否则抛 Malformed profile expression。这就是 "a & b | c" 非法、"(a & b) | c" 合法的原因——括号切开了 operator 上下文。
  3. ! 是前缀递归:case "!" -> elements.add(not(parseTokens(expression, tokens, Context.NEGATE)))——! 后面跟的 token 立刻在 NEGATE 上下文里再 parse 一次(支持 !(a & b) 这种括号取反),再用 not(...) 包装成 activeProfile -> !profiles.matches(activeProfile)(第 132-134 行)。
  4. 叶子节点是 equals(token)(第 136-138 行):activeProfile -> activeProfile.test(profile)——即把"name 是否激活"这个原子判断包成一个 Profiles。
  5. 合并工厂方法(第 124-130 行):or = anyMatch,and = allMatch,not = 取反。整棵树就是一个 Predicate<String> 的组合(这里 Profiles.matches(Predicate<String> isProfileActive) 的入参是 AbstractEnvironment.isProfileActive 这个方法引用)。

拿 @Profile("prod & (cloud | !test)") 走一遍:词法切出 prod & ( cloud | !test );prod 入 elements;& 记 operator=AND;( 递归:cloud 入子列表,| 记 OR,! 触发 NEGATE 上下文解析 test 得到 equals("test"),外层 not(...);) 把子列表按 OR 合并成 cloud | !test 子树;外层 merge 按 AND 合并 prod 与子树。最终 matches(isProfileActive) 求值 = isProfileActive("prod") && (isProfileActive("cloud") || !isProfileActive("test"))。

3.16 补深:default profile 兜底与 active profiles 的来源

回到 AbstractEnvironment.isProfileActive(第 407-412 行):

protected boolean isProfileActive(String profile) {
	validateProfile(profile);
	Set<String> currentActiveProfiles = doGetActiveProfiles();
	return (currentActiveProfiles.contains(profile) ||
			(currentActiveProfiles.isEmpty() && doGetDefaultProfiles().contains(profile)));
}

两层语义:

  • 正常路径:doGetActiveProfiles() 返回激活 profile 集合,命中即激活。激活集合的来源(AbstractEnvironment 的属性常量名在 Profile.java javadoc 里列全了):setActiveProfiles 编程式设置、spring.profiles.active 属性(系统属性/环境变量/servlet 上下文参数)、测试框架的 @ActiveProfiles。
  • 兜底路径:当 currentActiveProfiles.isEmpty()——一个 active profile 都没配过时——判定权交给 doGetDefaultProfiles()。默认集合初始是 {default}(保留名 RESERVED_DEFAULT_PROFILE_NAME),可被 setDefaultProfiles 或 spring.profiles.default 属性改写。

这个"空激活集 → default"规则的实际效果:

  • 标 @Profile("default") 的类,在"啥 profile 都没设"的裸启动场景下会被装配;
  • 但一旦你 setActiveProfiles("prod"),激活集非空,default 立刻退出舞台——标 @Profile("default") 的类反而不装了。

很多人第一次踩这个坑:本地裸跑测试一切正常,上服务器配了 spring.profiles.active=prod 之后某些 bean 神秘消失——元凶往往就是这个兜底语义。6.8 节排障清单里"翻激活的 profile",翻的就是这两个集合的实时状态。

3.17 补深:条件与 BeanRegistrar——7.0.9 的一个新成员

3.9.1 节 processImports 的四路分类里出现了一个 7.0.9 的新分支:else if (candidate.isAssignable(BeanRegistrar.class))(第 613-621 行)。这是旧资料里没有的:

else if (candidate.isAssignable(BeanRegistrar.class)) {
	Class<?> candidateClass = candidate.loadClass();
	BeanRegistrar registrar = (BeanRegistrar) BeanUtils.instantiateClass(candidateClass);
	AnnotationMetadata metadata = currentSourceClass.getMetadata();
	if (registrar instanceof ImportAware importAware) {
		importAware.setImportMetadata(metadata);
	}
	configClass.addBeanRegistrar(metadata.getClassName(), registrar);
}

BeanRegistrar 是 7.x 引入的新策略接口(spring-context 包内),定位是"更现代的 ImportBeanDefinitionRegistrar"。与本章主题相关的是它的执行时机:configClass.addBeanRegistrar(...) 只是登记,真正执行在 ConfigurationClassBeanDefinitionReader.loadBeanDefinitionsFromBeanRegistrars(configClass.getBeanRegistrars())(第 153 行)——与 ImportBeanDefinitionRegistrar 一样,处于 REGISTER_BEAN 阶段、晚于类级条件复审。也就是说:条件否决配置类之后,它登记的 registrar 根本不会被执行(因为配置类没进 configurationClasses);但类级条件通过之后,registrar 执行时注册的 bean 定义不再受任何条件二次裁决——registrar 自己负责判断要不要注册。写自定义 registrar 时,"按条件注册"的判断要放在 registrar 内部做,指望 @Conditional 标在 registrar 类身上是不够的(registrar 类不是 bean,它上面的条件只影响"它自己作为被 import 类"是否被解析,不影响它注册的那些 bean)。

3.18 完整案例追踪:一次条件否决的逐行现场

把本章所有零件串到一个可复现的最小案例上。代码如下:

// ---- 条件实现:app.feature.search=true 才放行 ----
public class SearchFeatureCondition implements Condition {
	@Override
	public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
		String v = context.getEnvironment().getProperty("app.feature.search");
		boolean ok = Boolean.parseBoolean(v);
		System.out.println("[cond] SearchFeatureCondition.matches -> " + ok + " (raw=" + v + ")");
		return ok;
	}
}

// ---- 组合注解 ----
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Conditional(SearchFeatureCondition.class)
public @interface EnabledOnSearch { }

// ---- 配置类 ----
@Configuration
public class AppConfig {

	@Bean
	public SearchEngine searchEngine() { return new SearchEngine(); }

	@Bean
	@EnabledOnSearch
	public SearchService searchService() { return new SearchService(); }
}

启动 new AnnotationConfigApplicationContext(AppConfig.class),环境属性 app.feature.search=true 与 =false 各跑一遍。逐行对照源码推演:

第一步:ConfigurationClassPostProcessor.postProcessBeanDefinitionRegistry → parser.parse(...)。

第二步:processConfigurationClass(AppConfig)(第 248 行)。 AppConfig 类元数据上有没有 @Conditional?没有——它没标 @EnabledOnSearch,shouldSkip 第一关 !metadata.isAnnotated(Conditional.class.getName()) 成立,直接 return false。配置类进入解析流程,doProcessConfigurationClass 收集到两个 @Bean 方法,存入 configClass.getBeanMethods()。

第三步:parse 末尾 deferredImportSelectorHandler.process()(第 200 行)。 本例没有延迟选择器,空跑。

第四步:ConfigurationClassBeanDefinitionReader.loadBeanDefinitions(...)。 新建 TrackedConditionEvaluator,遍历 configurationClasses。

第五步:loadBeanDefinitionsForConfigurationClass(AppConfig, tracked)(第 132 行)。 tracked.shouldSkip(configClass):AppConfig 不是 imported,直接 conditionEvaluator.shouldSkip(AppConfig.getMetadata(), REGISTER_BEAN)——类元数据无 @Conditional,第一关短路,return false。类不被跳。

第六步:遍历 bean 方法。 先 searchEngine():loadBeanDefinitionsForBeanMethod,方法元数据上无 @Conditional(它没标 @EnabledOnSearch),shouldSkip 第一关短路,正常注册 searchEngine 定义。

再 searchService():方法元数据有 @Conditional——来自组合注解 @EnabledOnSearch 的元注解。collectConditions 通过 getAllAnnotationAttributes(Conditional, true) 聚合出 [SearchFeatureCondition.class],无参反射 new 一个 SearchFeatureCondition(注意:是这一次调用才 new 的新实例,3.13 节路径一)。

  • app.feature.search=true:matches 打印 [cond] ... -> true (raw=true),返回 true;!matches(...) = false,循环走完 return false——searchService 正常注册。
  • app.feature.search=false:matches 打印 -> false (raw=false),返回 false;!matches(...) = true,loadBeanDefinitionsForBeanMethod 第 197 行 configClass.skippedBeanMethods.add("searchService") 后 return——注册表中没有 searchService 这个 bean 定义。

第七步:finishBeanFactoryInitialization。 若 searchEngine 构造器 @Autowired SearchService,此时抛 NoSuchBeanDefinitionException: No qualifying bean of type '...SearchService'——条件否决的代价在这一刻才显现。

这个案例同时验证了本章三个核心结论:(1) 组合注解 @EnabledOnSearch 上的元注解 @Conditional 被 getAllAnnotationAttributes(..., true) 正确收集;(2) 方法级条件在 REGISTER_BEAN 阶段(第六步)才评估,PARSE 阶段(第二、五步)两次都不涉及它;(3) 条件失败静默,代价延迟到装配期。

3.19 排障实战:断点该下在哪

自己写条件遇到"bean 死活不出现"时,按这个顺序下断点/加日志,比瞎猜高效:

  1. ConditionEvaluator.shouldSkip(第 81 行)入口:看有没有走到这里、metadata 是哪个类/方法、phase 是什么。走不到 = 你的 @Conditional 根本没被收集到(检查是不是标在了接口/父类上,6.4 节)。
  2. collectConditions(第 114 行)返回处:看条件列表里到底有几个 Condition 类、是哪些。少了 = 组合注解没被元注解链展开(检查自定义注解有没有标 @Target(METHOD)、方法上的注解能否被 getAllAnnotationAttributes 读到)。
  3. 你自己的 matches:打印 context.getEnvironment().getProperty(...) 的原始值——90% 的"条件不生效"是属性名拼错或值带空格。
  4. TrackedConditionEvaluator.shouldSkip(第 507 行):如果类在 PARSE 阶段明明通过、REGISTER 阶段却消失,看 importedBy 递归是不是把它传递否决了(3.6 节)——所有导入它的类都被跳,它才被跳。
  5. loadBeanDefinitionsForBeanMethod 第 196 行:方法级条件的现场,看 metadata.getMethodName() 是哪个方法、skippedBeanMethods 里记了谁。

3.20 本章补充:几个没展开但必须知道的边界

  • static @Bean 方法上的条件:loadBeanDefinitionsForBeanMethod 第 234-243 行对 static 方法与实例方法做了区分(static 用类名作 beanClass、实例用 factoryBeanName),但条件闸门在区分之前(第 196 行)——static @Bean 方法的条件同样在 REGISTER_BEAN 阶段评估,语义与实例方法一致。
  • 嵌套 @Configuration 类(@InnerConfiguration):doProcessConfigurationClass 会递归处理成员类,成员类上的 @Conditional 走 PARSE 阶段闸门,否决后连同外层配置类里的其他内容不受影响(成员类是独立的 ConfigurationClass)。
  • 条件评估与 AOT:7.0.9 的 AOT 处理在构建期跑 ConfigurationClassPostProcessor,条件评估结果被烘焙成 bean 定义。条件里若依赖运行期才存在的属性,AOT 生成代码会与运行期行为不一致——面向 AOT 的条件必须只用构建期可确定的信息。
  • 多 @Conditional 同标:@Conditional(A.class) @Conditional(B.class) 这种写法 Java 不允许(@Conditional 不是 @Repeatable),要表达"同时满足"只能写在一个数组里 @Conditional({A.class, B.class})——数组内本身就是 AND(3.1.2 短路循环)。

3.10 组件扫描中的条件评估:isConditionMatch 与 TypeFilter 的分工

条件体系与组件扫描的关系,是任务书第⑦点要求讲清的"区别与配合"。看 ClassPathScanningCandidateComponentProvider(spring-context/src/main/java/org/springframework/context/annotation/ClassPathScanningCandidateComponentProvider.java)。

候选类的判定主链路在 isCandidateComponent(MetadataReader)(第 540-546 行附近截取):

			if (filter.match(metadataReader, getMetadataReaderFactory())) {
				registerCandidateTypeForIncludeFilter(metadataReader.getClassMetadata().getClassName(), filter);
				return isConditionMatch(metadataReader);
			}
		}
		return false;
	}

	/**
	 * Determine whether the given class is a candidate component based on any
	 * {@code @Conditional} annotations.
	 */
	private boolean isConditionMatch(MetadataReader metadataReader) {
		if (this.conditionEvaluator == null) {
			this.conditionEvaluator =
					new ConditionEvaluator(getRegistry(), this.environment, this.resourcePatternResolver);
		}
		return !this.conditionEvaluator.shouldSkip(metadataReader.getAnnotationMetadata());
	}

逐句拆解:

  1. TypeFilter 在前,Condition 在后。filter.match(...) 是 @ComponentScan 的 includeFilters/excludeFilters(按注解类型、正则、AspectsJ、自定义 TypeFilter)——它决定"这个类有没有资格成为候选"。只有 include filter 命中后,才走到 isConditionMatch——它决定"这个候选要不要被条件否决"。两者是串联的两道闸,职责不同:TypeFilter 是静态的"类特征匹配"(带不带某注解、名字像不像),Condition 是动态的"运行时状态判断"(环境、属性、类路径)。
  2. 扫描路径用的是单参数 shouldSkip(metadataReader.getAnnotationMetadata())——不传 phase,走 3.1.1 的推导分支。被扫到的类是 @Component(或其派生),isConfigurationCandidate 判为 true,所以走 PARSE_CONFIGURATION。也就是说:组件扫描出来的类,条件否决发生在"把它注册成 bean 定义之前",否决后连 ScannedGenericBeanDefinition 都不会产生——比"注册了再删"(3.6 节 TrackedConditionEvaluator 的撤回动作)更早、更省。
  3. conditionEvaluator 懒初始化:字段声明在第 120 行 private @Nullable ConditionEvaluator conditionEvaluator;,构造器里第 238 行显式置 null(setEnvironment/setResourceLoader 变更时也会清掉,让下次重建时带上新的环境)。isConditionMatch 第一次被调时才 new——扫描器可以被用户在脱离上下文的场景下反复 setEnvironment 后复用,懒初始化保证拿到的是最新环境。这是个很实用的细节。
  4. metadata 来自 ASM,不需要类加载:metadataReader.getAnnotationMetadata() 是 ASM 读出来的元数据,条件里若调 ClassUtils.isPresent(...) 做类路径探测,被探测类甚至不需要真的被加载——这正是条件体系"省类加载"优势的来源(尤其在自动配置场景,成百上千个候选类只看元数据)。

3.11 编程式注册入口:AnnotatedBeanDefinitionReader 的条件闸门

第三条评估路径是编程式注册:AnnotationConfigApplicationContext.register(AppConfig.class) 最终走到 AnnotatedBeanDefinitionReader。看第 257 行:

if (this.conditionEvaluator.shouldSkip(abd.getMetadata())) {
	return;
}

上下文里第 89 行与第 107 行两次构造 ConditionEvaluator(registry, environment, null)——注意 resourceLoader 传 null,走 3.3 节的 deduceResourceLoader 兜底(registry 若是 context 就用 context 的,否则 DefaultResourceLoader)。

这个闸门的存在解释了一个现象:哪怕你是手动 register() 进去的类,只要它带 @Conditional,条件不满足照样不注册。编程式注册并不豁免条件体系。


四、运行机制:一次带条件的完整启动时序

前面逐点拆完了零件,本节把它们串成一次真实启动的时间线。场景设定:

// app.props: app.cache.enabled=false, spring.profiles.active=prod
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.register(MyAppConfig.class);
ctx.refresh();

其中:

@Configuration
@ComponentScan("com.example.app")
public class MyAppConfig {
	@Bean @Conditional(CacheOffCondition.class)   // 读 app.cache.enabled,false 则否决
	public CacheService cacheService() { return new CacheService(); }
}

@Component
@Profile("prod")                                // PARSE 阶段否决 test 环境的采集器
public class MetricsReporter { }

4.1 时序主干

ConfigurableApplicationContext.refresh()
  │
  ├─ prepareBeanFactory / 准备 Environment(activeProfiles=[prod],propertySource 已就位)
  │
  ├─ invokeBeanFactoryPostProcessors()
  │     │
  │     └─ ConfigurationClassPostProcessor(BeanDefinitionRegistryPostProcessor)执行
  │           │  postProcessBeanDefinitionRegistry
  │           ▼
  │     ConfigurationClassParser.parse(...)
  │           │
  │           ├─ processConfigurationClass(MyAppConfig)
  │           │     └─ shouldSkip(MyAppConfig元数据, PARSE_CONFIGURATION)
  │           │           无 @Conditional → return false,继续
  │           │
  │           ├─ doProcessConfigurationClass:解析 @ComponentScan
  │           │     └─ ComponentScanAnnotationParser → ClassPathBeanDefinitionScanner.doScan
  │           │           └─ 对每个候选类 isCandidateComponent(MetadataReader)
  │           │                 ├─ TypeFilter.match(带 @Component?)
  │           │                 └─ isConditionMatch → shouldSkip(MetricsReporter元数据)
  │           │                       └─ 推导出 PARSE_CONFIGURATION → collectConditions
  │           │                             → [ProfileCondition] matches?
  │           │                             environment.matchesProfiles("prod") → true → 放行
  │           │                             (test 环境时此处 false → MetricsReporter 不注册)
  │           │
  │           ├─ processImports 收集所有 @Import;普通 ImportSelector 此刻递归解析
  │           │
  │           └─ deferredImportSelectorHandler.process()   ← 第 200 行,parse 末尾
  │                 └─ 所有 DeferredImportSelector.selectImports 在此刻才执行
  │                       └─ 选中的类再走 processConfigurationClass → PARSE 阶段条件评估
  │
  │     【此刻:configurationClasses 集合已定型,但 @Bean 方法级条件尚未评估】
  │
  │           ▼
  │     ConfigurationClassBeanDefinitionReader.loadBeanDefinitions(configurationClasses)
  │           └─ 对每个 ConfigurationClass:
  │                 ├─ TrackedConditionEvaluator.shouldSkip(configClass)
  │                 │     ├─ importedBy 传递性否决检查
  │                 │     └─ shouldSkip(类元数据, REGISTER_BEAN) → 普通条件在此复审
  │                 ├─ registerBeanDefinitionForImportedConfigurationClass(如被 import)
  │                 └─ for each @Bean method:
  │                       └─ shouldSkip(方法元数据, REGISTER_BEAN)
  │                             → collectConditions → [CacheOffCondition] matches?
  │                             app.cache.enabled=false → matches 返回 false
  │                             → 方法名记入 skippedBeanMethods,不产生 bean 定义
  │
  ├─ registerBeanPostProcessors()
  ├─ finishBeanFactoryInitialization:实例化所有单例
  │     └─ cacheService beanName 不存在 → 若别处 @Autowired CacheService 则 NoSuchBeanDefinitionException
  │
  └─ finishRefresh()

4.2 从时序里读出的三个设计结论

结论一:条件评估全部发生在 BeanFactoryPostProcessor 执行期,早于任何 bean 实例化。 ConfigurationClassPostProcessor 本身是 BeanDefinitionRegistryPostProcessor(BeanFactoryPostProcessor 的子接口),条件评估作为它的内部步骤,天然处于"bean 定义注册表正在被填充、bean 实例尚未创建"的窗口。这就是 Condition javadoc 那句"与 BeanFactoryPostProcessor 同限制"的运行时根据:你的 matches 里即使能拿到 getBeanFactory(),此刻它里面也只有 bean 定义、没有 bean 实例——getBean(...) 会触发容器提前实例化,破坏 BDPP 的执行顺序契约。

结论二:类级条件(PARSE)与方法级条件(REGISTER_BEAN)之间隔着"全部配置类解析完毕"这条线。 时序图上,MetricsReporter 的 profile 条件在扫描/解析期就裁决;cacheService 的方法级条件却要等到 loadBeanDefinitionsForBeanMethod。中间这段时间里,所有配置类都互相见过了面——所以 REGISTER_BEAN 阶段的条件,才能安全地读 registry 里"用户自己注册的 bean 定义"。这正是 ConfigurationCondition.REGISTER_BEAN 枚举 javadoc(ConfigurationCondition.java 第 52-58 行)写的:"At the time that the condition is evaluated, all @Configuration classes will have been parsed."

结论三:条件否决是"减法",不是"替换"。 整条时间线上没有任何地方因为条件不匹配而报错或回退——类不进集合、方法不进注册表、已注册的被 remove。依赖这个 bean 的地方要到 finishBeanFactoryInitialization 按类型/按名装配时才发现"bean 不存在",抛 NoSuchBeanDefinitionException。条件失败是静默的,这是设计哲学(装配开关本就该默认沉默),也是排障难点(启动日志里看不到任何"条件 X 失败"的字样——除非自己开 DEBUG 日志给 ConditionEvaluator)。

4.3 第二个时序场景:延迟选择器里的条件,究竟何时被裁决

4.1 节的场景走的是"组件扫描 + 方法级条件",这一节换一个"延迟选择器导入 + 类级条件"的场景,把 3.9 节的机制再压一遍时间线。

@Configuration
@Import(MyDeferredSelector.class)
public class RootConfig { }

public class MyDeferredSelector implements DeferredImportSelector {
	public String[] selectImports(AnnotationMetadata importingClassMetadata) {
		return new String[] { "com.example.ConditionalFeatureConfig" };
	}
}

@Configuration
@Conditional(FeatureXCondition.class)
public class ConditionalFeatureConfig {
	@Bean
	public FeatureX featureX() { return new FeatureX(); }
}

逐时刻对位:

t1: processConfigurationClass(RootConfig),PARSE 闸门——RootConfig 无 @Conditional,放行。 t2: doProcessConfigurationClass 收集 @Import(MyDeferredSelector.class),processImports 第三/四路判定:selector 是 DeferredImportSelector → deferredImportSelectorHandler.handle(rootConfig, selector),只登记进 deferredImportSelectors 列表,此刻不调 selectImports。ConditionalFeatureConfig 这个名字此刻甚至还不存在于任何地方。 t3: parse 继续处理其他配置类……全部处理完。 t4: parse() 末尾第 200 行 deferredImportSelectorHandler.process()——列表按 DEFERRED_IMPORT_COMPARATOR(@Order)排序,逐组 processGroupImports()。此刻才真正调 MyDeferredSelector.selectImports(...),拿到 "com.example.ConditionalFeatureConfig"。 t5: processGroupImports 第 866 行对这个类走标准 processImports → 第四路 processConfigurationClass(candidate.asConfigClass(...))——此刻 ConditionalFeatureConfig 才第一次见到 PARSE 阶段条件闸门,FeatureXCondition.matches 被调用。 t6: t5 通过后,ConditionalFeatureConfig 进 configurationClasses;随后 loadBeanDefinitions 阶段,featureX() 方法级条件(本例没有)与类级 TrackedConditionEvaluator 复审再走一轮。

这个时序回答了"为什么自动配置要延迟":假如 MyDeferredSelector 是普通 ImportSelector,t2 时刻就会立刻递归解析 ConditionalFeatureConfig,FeatureXCondition 在 t2 就裁决——而那时 t3 的其他配置类还没被处理,FeatureXCondition 里若查"用户有没有自定义 FeatureX",答案必然是"没有",导致默认配置被误注册。延迟到 t4(全部解析完)再裁决,条件看到的注册表面才是完整的。

4.4 条件"不会被评估"的盲区清单

排障时还有一类问题:写了 @Conditional 却完全没生效。根据源码,以下盲区条件不会被评估,列全以绝后患:

  1. 接口上的 @Conditional:isConfigurationCandidate 对接口直接 false(3.12 节),且接口不会被注册成 bean 定义。
  2. 父类上的 @Conditional:@Conditional 无 @Inherited,子类元数据里读不到(6.4 节)。
  3. @ImportBeanDefinitionRegistrar/BeanRegistrar 注册出来的 bean:registrar 是注册动作本身,它注册的 bean 定义不会再被 ConditionEvaluator 过一遍(3.17 节)。
  4. XML 里 <bean> 定义的类:条件体系只管注解元数据路径;XML bean 定义上想用条件,得靠 Spring 4+ 的 spring-context XML 命名空间里 <beans profile="..."> 那一套独立机制,与 ConditionEvaluator 无关。
  5. @Conditional 标在非 TYPE/METHOD 位置:@Target 只有 TYPE/METHOD,标字段/参数编译就不过。
  6. 被跳过的配置类里嵌套的 @Bean 方法:类被 PARSE 否决后不进 configurationClasses,方法列表根本不存在,无需再评估。
  7. registrar 在 REGISTER_BEAN 阶段执行时(第 152-153 行),它注册的 bean 定义不受类级复审影响——但它执行前,所属配置类已经过了 TrackedConditionEvaluator.shouldSkip,被跳过的类其 registrar 不会执行。

4.5 与 BeanFactoryPostProcessor 执行顺序的关系

最后把条件评估放进 refresh() 的大顺序里钉死:

invokeBeanFactoryPostProcessors()
  └─ 先执行所有 BeanDefinitionRegistryPostProcessor.postProcessBeanDefinitionRegistry
       └─ ConfigurationClassPostProcessor(最高优先级,Ordered 最低值先跑)
            ├─ parser.parse(...)                    ← PARSE 阶段条件、组件扫描条件在此
            ├─ deferredImportSelectorHandler.process()
            └─ reader.loadBeanDefinitions(...)       ← REGISTER_BEAN 阶段条件在此
  └─ 再执行 BeanFactoryPostProcessor.postProcessBeanFactory
registerBeanPostProcessors()
finishBeanFactoryInitialization()   ← bean 实例化;条件早已全部裁决完毕

关键事实:所有条件评估都发生在 ConfigurationClassPostProcessor 这一个 BDPP 内部、在任何 bean 实例化之前。其他用户自定义的 BeanFactoryPostProcessor 在它之后才跑——这就是条件里能安全"读到所有 bean 定义、但读不到任何 bean 实例"的根本原因:条件工作在"定义世界",bean 实例化在"实例世界",两个世界被 ConfigurationClassPostProcessor 的执行边界严格分开。越过这个边界去 getBean,就是 6.5 节那个坑。

五、扩展用法与最佳实践

5.1 自定义 Condition 的完整范式

框架层最常用的三类条件——按环境、按类路径、按属性开关——下面给出可直接抄用的实现,并逐行对应前面读过的源码。

范式一:按属性开关(最常用)。

public class OnFeatureCondition implements Condition {

	@Override
	public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
		// 1. 从注解元数据读出自己携带的参数
		MultiValueMap<String, Object> attrs =
				metadata.getAnnotationAttributes(EnabledOnFeature.class.getName());
		String feature = (String) attrs.getFirst("value");

		// 2. 从 environment 读属性;读不到默认 false
		String v = context.getEnvironment().getProperty("app.feature." + feature);
		return Boolean.parseBoolean(v);
	}
}

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Conditional(OnFeatureCondition.class)
public @interface EnabledOnFeature {
	String value();
}

用法:@EnabledOnFeature("search") 标在 @Configuration 类或 @Bean 方法上。

源码对位:条件实现里 metadata.getAnnotationAttributes(...) 读到的正是 collectConditions 通过 getAllAnnotationAttributes(Conditional, true) 聚合后、再作为 AnnotatedTypeMetadata 传给 matches 的同一份元数据;context.getEnvironment() 就是 3.3 节那个 deduce 出来的环境——这里读属性是最安全的操作,因为 environment 在 BDPP 执行前已就绪。

范式二:按类路径存在与否。

public class OnClassCondition implements Condition {

	@Override
	public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
		MultiValueMap<String, Object> attrs =
				metadata.getAnnotationAttributes(EnabledOnClass.class.getName());
		String className = (String) attrs.getFirst("value");
		ClassLoader cl = context.getClassLoader();
		return ClassUtils.isPresent(className, cl);
	}
}

注意点:ClassUtils.isPresent 内部 catch Throwable(包括 NoClassDefFoundError),类不存在不会抛异常而是返回 false——这是它适合在条件里用的原因;不要换成 Class.forName(...),类不存在时会抛 NoClassDefFoundError,而 3.1.2 说过异常不被吞,容器直接启动失败。

范式三:按环境/profile 表达式。 直接复用 3.8 节链路:

public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
	return context.getEnvironment().matchesProfiles("prod", "!test");
}

5.2 三种位置的行为差异:类上 vs @Bean 方法上 vs 组件类上

同一个自定义条件,标在三个位置,效果完全不同(对照 3.7 节那张表):

标注位置评估时机否决范围
@Configuration 类上PARSE_CONFIGURATION(processConfigurationClass 第 249 行)整个配置类:全部 @Bean 方法、@Import、@ComponentScan 一扫而空
@Component 组件类上组件扫描 isConditionMatch(第 559 行),推导为 PARSE该类不产生 bean 定义,扫描写停
@Bean 方法上REGISTER_BEAN(loadBeanDefinitionsForBeanMethod 第 196 行)只砍这一个工厂方法,同配置类的其他 @Bean 方法不受影响

工程含义:

  • 想"整个功能模块开/关":条件标在 @Configuration 类上,一次砍掉整个模块——这是最干净的开关粒度。
  • 想"同一份配置里二选一/多选一":条件标在各自 @Bean 方法上,类本身始终解析,运行时按属性挑方法。
  • 想"某类只在某环境被组件扫描发现":@Profile/自定义条件标组件类上,扫描期就隐形。

5.3 组合条件注解与评估顺序

@Conditional 可以作为元注解被层层组合,这是 3.2 节 getConditionClasses 用 getAllAnnotationAttributes(Conditional, true) 聚合出来的能力。合法写法:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Conditional(ProfileCondition.class)        // 组合层一
public @interface ProdOnly { }

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Conditional(OnFeatureCondition.class)      // 组合层二
public @interface SearchEnabled { }

@Configuration
@ProdOnly
@SearchEnabled
public class SearchConfig { }

SearchConfig 上收集到的条件列表 = [ProfileCondition, OnFeatureCondition](外加任何直接写的 @Conditional(...)),两个都要 matches。

评估顺序由 AnnotationAwareOrderComparator.sort(conditions) 决定(3.2 节),对条件类加 @Order 即可干预:

@Order(Ordered.HIGHEST_PRECEDENCE)
public class CheapButLikelyFalseCondition implements Condition { ... }

排前面的条件先求值、且失败即短路——把"便宜、命中率高"的条件排前面是免费优化。多个条件类都不加 @Order 时,顺序由 AnnotationAwareOrderComparator 的稳定排序决定(未指定顺序的元素相对顺序保持收集顺序),不要依赖这个隐式顺序,想稳定就显式 @Order。

5.4 ConfigurationCondition:什么时候必须用两阶段

写条件时的一个判断题:你的 matches 里读什么?

  • 只读 environment(profile、属性)、类路径(ClassUtils.isPresent)、资源(ResourceLoader)——这些在容器启动极早期就稳定,普通 Condition 即可,PARSE 阶段跑没问题。
  • 要读 context.getRegistry() / getBeanFactory().containsBeanDefinition(name)——"某个 bean 定义是否已注册"——必须实现 ConfigurationCondition 并声明 getConfigurationPhase() == REGISTER_BEAN。理由见 3.6 节:PARSE 阶段注册表几乎是空的,你在那时问"用户有没有自己注册 X",答案必然是 false,导致默认 bean 被误注册。声明 REGISTER_BEAN 后,引擎在 PARSE 阶段自动跳过你这个条件(3.1.2 的阶段过滤),等所有配置类解析完、注册阶段复审时再问,答案才正确。

ConfigurationCondition 的 javadoc 原话(ConfigurationCondition.java 第 22-25 行)就是这个例子:"a condition that checks if a bean has already been registered might choose to only be evaluated during the REGISTER_BEAN phase."

5.5 性能注意点

结合 3.1 节"引擎无缓存、每次评估都重新实例化"的事实,三条工程建议:

  1. 条件类保持瘦:无字段、无重初始化逻辑。因为每次 shouldSkip 都 BeanUtils.instantiateClass 新实例(3.2 节),条件里放重对象等于每次评估都重建。
  2. 重条件排后面:类路径探测(ClassUtils.isPresent 虽快但仍要走类加载委托)、注册表遍历(getRegistry().getBeanDefinitionNames())这类操作,用 @Order 排到简单条件之后,靠短路省掉。
  3. 唯一的结果级缓存是 TrackedConditionEvaluator.skipped(3.6 节):它只在 REGISTER_BEAN 阶段类级复审时对"同一配置类同一轮"去重。方法级条件(loadBeanDefinitionsForBeanMethod)没有结果缓存——同一个 @Bean 方法不会重复评估,是因为遍历 beanMethods 本来就只走一次,而不是因为缓存。
  4. 组件扫描路径走 ASM 元数据,不加载类:条件里读注解属性用 metadata.getAnnotationAttributes 即可,不要 metadata.getIntrospectedClass()(后者会触发真正类加载,破坏扫描期的延迟加载优势)。

5.6 与相邻机制的配合

  • 与 @Primary/@ConditionalOnMissingBean 类语义:框架层没有 @ConditionalOnMissingBean(那是 Spring Boot 的),但其范式就是"REGISTER_BEAN 阶段条件 + registry.containsBeanDefinition(name)"。自己写时照 5.4 节声明 REGISTER_BEAN 即可。
  • 与 BeanRegistry/ImportBeanDefinitionRegistrar:registrar 在 REGISTER_BEAN 阶段(loadBeanDefinitionsFromImportBeanDefinitionRegistrars,第 152 行)执行,晚于类级条件复审、与方法级条件同阶段——registrar 内部注册 bean 定义时不再受条件约束(它自己就是注册动作)。
  • 与 AOT:7.0.9 的 AOT 场景下条件评估发生在构建期(ApplicationContextAotGenerator),此时 matches 里能用的只有 environment/类路径的构建期快照——构建期与运行期 environment 不一致会导致 AOT 生成的 bean 定义与运行期期望不符。写条件时若要面向 AOT,避免依赖启动后才能确定的状态。

六、常见问题与坑

6.1 典型坑一:条件在错误阶段评估,导致"该注册的没注册"或"不该注册的注册了"

症状:写了一个条件检查"用户是否自定义了 XxxTemplate":

public class OnUserTemplateMissingCondition implements Condition {
	public boolean matches(ConditionContext context, AnnotationMetadata metadata) {
		return !context.getRegistry().containsBeanDefinition("xxxTemplate");
	}
}

@Configuration
@Conditional(OnUserTemplateMissingCondition.class)   // 普通 Condition,没写 ConfigurationCondition
public class DefaultTemplateConfig { ... }

预期:用户自己配了 xxxTemplate 就不装配默认的;没配才装配默认的。实际:用户明明配了,默认的还是被装配了。

源码级解释:这个条件是普通 Condition,不声明 phase,根据 3.1.2 的逻辑,PARSE 阶段和 REGISTER_BEAN 阶段都会被求值。问题出在 PARSE 阶段那一次:processConfigurationClass 第 249 行调用时,用户的配置类可能还没被解析(取决于解析顺序),registry.containsBeanDefinition("xxxTemplate") 返回 false——条件 matches 成功,DefaultTemplateConfig 整个类被放行进了解析结果集。到 REGISTER_BEAN 阶段,TrackedConditionEvaluator 复审时即使查到用户已经注册了,虽然会触发"removeBeanDefinition"撤回动作,但此时类里的 @Bean 方法可能已经在别处造成影响,且撤回只删类级 bean 定义、语义复杂。

正确写法:实现 ConfigurationCondition,把条件钉在 REGISTER_BEAN:

public class OnUserTemplateMissingCondition implements ConfigurationCondition {
	public boolean matches(ConditionContext context, AnnotationMetadata metadata) { ... 同上 ... }
	public ConfigurationPhase getConfigurationPhase() {
		return ConfigurationPhase.REGISTER_BEAN;   // PARSE 阶段引擎自动跳过它
	}
}

这样 PARSE 阶段它被静默忽略(requiredPhase != phase,3.1.2),REGISTER_BEAN 阶段才问"用户注册了吗"——此时所有配置类都解析完(ConfigurationCondition 枚举 javadoc 的承诺),答案准确。

6.2 典型坑二:@Bean 方法条件依赖另一个 @Bean 方法是否存在

症状:

@Configuration
public class DataConfig {

	@Bean
	public DataSource dataSource() { ... }

	@Bean
	@Conditional(OnDataSourceBeanCondition.class)   // 想:有 dataSource 才注册 JdbcTemplate
	public JdbcTemplate jdbcTemplate() { ... }
}

OnDataSourceBeanCondition.matches 里 return context.getRegistry().containsBeanDefinition("dataSource");。

源码级解释:jdbcTemplate 的条件在 REGISTER_BEAN 阶段评估(loadBeanDefinitionsForBeanMethod 第 196 行),而此时 dataSource 方法有没有被注册,取决于两个方法在 configClass.getBeanMethods() 里的遍历顺序——loadBeanDefinitionsForConfigurationClass 第 147-149 行是 for (BeanMethod beanMethod : configClass.getBeanMethods()) 顺序遍历。如果 dataSource 排在 jdbcTemplate 前面,条件能查到;排后面,条件查不到、返回 false、jdbcTemplate 不注册,而 dataSource 随后照样注册——行为依赖方法声明顺序,极其隐蔽。

这正是为什么 Spring Boot 把"@ConditionalOnBean/@ConditionalOnMissingBean"严格限定在自动配置类之间使用、并警告"不要在自己业务 @Configuration 里用 @ConditionalOnBean"——框架层的 ConfigurationCondition 只提供了"阶段"这个粗粒度控制,方法间的相对注册顺序在框架层没有任何保证。业务代码想做"有 A 才有 B",更稳的做法是把 A、B 拆成两个独立配置类,让条件在类级(PARSE/REGISTER 复审)裁决,而不是方法级互相探测。

6.3 典型坑三:@Conditional 与 @Profile 混用的语义

@Profile 本身就是 @Conditional(ProfileCondition.class)(3.8 节),所以 @Profile 和 @Conditional 同时标在一个类上时,收集到的条件列表是 [ProfileCondition, 你的Condition],两者是 AND 关系(3.1.2 短路循环:全部 matches 才放行)。

容易踩的两个误读:

  1. "@Profile("prod") + @Conditional(X.class) = prod 下 X 才生效"——对,是 AND。
  2. "我写两个 @Profile,@Profile("p1") @Profile("p2") = p1 或 p2?"——Java 注解不允许重复标注(除非 @Repeatable),@Profile 不是 @Repeatable,写两个编译不过;要或关系用 @Profile({"p1","p2"}) 一个注解里给数组(3.8.1 节:数组内部也是 OR)。
  3. 组合注解场景:@Prod(内含 @Profile("prod"))和直接 @Profile("test") 同时标在一个类上——getAllAnnotationAttributes(Profile, true) 会聚合出两份 value,ProfileCondition 对两份做 OR(3.8.1):"prod 或 test" 任一激活就放行。这个"份间 OR"语义很少被意识到,但它是 ProfileCondition 源码里那个外层 for 循环的直接行为。

6.4 典型坑四:条件注解放在接口/父类上"不生效"

症状:父类 @Conditional(FooCondition.class) 或接口上标了 @Profile,子类/实现类却照样被装配。

源码级解释:两层原因。

  1. @Conditional 自己没有标 @Inherited,其 javadoc(Conditional.java 第 46-52 行)明说:"Inheritance of @Conditional annotations is not supported; any conditions from superclasses or from overridden methods will not be considered."——子类不会继承父类上的 @Conditional。ConfigurationClassParser 解析时用的 metadata 是子类自己的 ASM 元数据,isAnnotated(Conditional.class.getName()) 只看子类自身及其元注解链,看不到父类。
  2. getAllAnnotationAttributes(Conditional, true) 的"true"展开的是元注解链(注解上的注解),不是类继承链(父类上的注解)。这两条链常被混淆:@Prod 这个注解上的 @Conditional 能被收集到(元注解链),BaseConfig 父类上的 @Conditional 收集不到(类继承链)。

结论:条件注解必须标在"最终要被注册的那个具体类/方法"本身上,指望父类继承是无效的。被重写的 @Bean 方法上的条件同理不生效。

6.5 典型坑五:matches 里过早访问 bean 工厂/注册表

症状:条件实现里 context.getBeanFactory().getBean(SomeService.class),启动时抛 BeanCurrentlyInCreationException 或 IllegalStateException: ... not yet initialized。

源码级解释:条件评估发生在 ConfigurationClassPostProcessor(BeanDefinitionRegistryPostProcessor)执行期间(4.1 节时序),此刻 bean 工厂只有 bean 定义、没有 bean 实例。Condition javadoc 把话说死了:"take care to never interact with bean instances"。getBeanFactory() 返回的工厂对象虽然非空,但你对它调 getBean 会强行触发提前实例化,破坏 BDPP 执行顺序——这是比报错更糟的事,它会让 bean 的生命周期错乱。

该做的:只问"定义层面"的问题——containsBeanDefinition(name)、getBeanDefinition(name)、getEnvironment()、ClassUtils.isPresent(...)。getRegistry() 在裸扫描场景还可能直接 Assert.state 失败(3.3 节)。

6.6 典型坑六:重载 @Bean 方法上的 @Profile 不一致

Profile.java javadoc 第 80-90 行专门警告过这个场景:同一个 Java 方法名的多个重载 @Bean 方法(类比构造器重载),@Profile 必须在所有重载上一致声明;不一致时只有第一个声明上的条件算数。因为配置类解析时同 beanName 的工厂方法去重/合并逻辑只看首份注解元数据。javadoc 给的官方建议原话是:"Use distinct Java method names pointing to the same @Bean#name if you'd like to define alternative beans with different profile conditions"——想按 profile 二选一,就用两个不同方法名、再用 @Bean(name="...") 指定同一个 bean 名,不要靠重载。

6.7 典型坑七:条件里抛异常 = 启动失败,不是跳过

3.1.2 节已经看过:shouldSkip 循环里没有 try/catch,matches 抛出的运行时异常一路穿透,被 ConfigurationClassParser 包成 BeanDefinitionStoreException("Failed to parse configuration class [...]") 启动失败。ConditionContext.getRegistry() 拿不到时直接 IllegalStateException(3.3 节)。

排障含义:条件实现要对"环境里属性缺失"这类情况做防御(getProperty 可能返回 null,Boolean.parseBoolean(null) 返回 false 而不是抛——范式一已经这么写了)。条件返回 false 是安静跳过,条件抛异常是容器爆炸,二者天差地别。

6.8 典型坑八:条件静默失败、日志里查无此 bean

4.2 节结论三说过:条件否决是减法,不报错。当你发现"某个 bean 没了、别处装配报 NoSuchBeanDefinitionException",排障第一步不是怀疑装配逻辑,而是翻条件:

  • 类上有没有 @Profile、激活的 profile 对不对(env.getActiveProfiles())?
  • 属性开关配没配(app.feature.xxx)?
  • 自定义条件的 matches 里读的属性名拼对没有(getProperty("app.cache.enabled") 少个点就读成 null)?

可以临时给条件加一行 logger.info("condX: {}, env={}", ...)——因为条件实例每次评估都 new(3.2 节),日志里能看到每次评估时的真实输入。这比给 ConditionEvaluator 开 DEBUG 更直接,因为 7.0.9 的 ConditionEvaluator 本身几乎没有诊断日志输出。

6.9 版本差异坑:别拿旧版行为套 7.0.9

最后把本章勘探到的、与旧版资料不符的事实集中列一遍,避免读者按旧文章对号入座:

  1. 包位置:条件类在 org.springframework.context.annotation 平铺,不在 annotation/condition/ 子包。
  2. 无 skipCache:7.0.9 的 ConditionEvaluator 没有 skipCache 字段,每次评估都重新收集、重新实例化条件;唯一的结果缓存是 TrackedConditionEvaluator.skipped Map(一轮注册加载内)。
  3. 无 ConditionException:条件异常裸传,由外层包成 BeanDefinitionStoreException。
  4. ProfileCondition 调 matchesProfiles(Environment 6.2 起的默认方法),不再是旧资料写的 acceptsProfiles(Profiles.of(...)) 手写两步;acceptsProfiles(String...) 已 @Deprecated(since="5.1")。

七、本章小结

7.1 要点浓缩

本章从"@Conditional 到底由谁、在何时、如何裁决"这个问题出发,沿源码走了一遍完整链路:

  1. 抽象层:Condition 是唯一的 matches 投票函数;ConditionContext 向条件暴露 registry/beanFactory/environment/resourceLoader/classLoader 五样东西;@Conditional 是挂在 TYPE/METHOD 上的条件容器,存的是条件类的 Class 对象;ConfigurationCondition 用 getConfigurationPhase() 把条件钉在 PARSE 或 REGISTER_BEAN 某一个阶段。
  2. 引擎层:ConditionEvaluator.shouldSkip 三幕剧——无注解短路;phase 为 null 时用 isConfigurationCandidate 推导;显式 phase 下 collectConditions 收集+排序后逐个 matches,任一失败即跳过;ConfigurationCondition 阶段不符的条件被静默跳过。条件实例每次评估都无参反射新建,引擎自身无缓存。
  3. 时机层:PARSE 阶段在 ConfigurationClassParser.processConfigurationClass 第 249 行,否决是连根拔;REGISTER_BEAN 阶段有两处——loadBeanDefinitionsForBeanMethod 第 196 行裁决 @Bean 方法,TrackedConditionEvaluator 第 524 行复审配置类并沿 importedBy 传递否决、必要时 remove 已注册定义。组件扫描(isConditionMatch 第 559 行)与编程式注册(AnnotatedBeanDefinitionReader 第 257 行)走单参数推导入口。
  4. @Profile 层:@Profile = 元注解 @Conditional(ProfileCondition.class);ProfileCondition 读 @Profile 多份声明做份间 OR,每份交给 environment.matchesProfiles(...)(7.0.9 新入口,内部 Profiles.of 解析 &|! 表达式,AbstractEnvironment.isProfileActive 提供单名判定 + default profile 回退)。
  5. 桥梁层:普通 ImportSelector 同步递归导入;DeferredImportSelector 在 parse() 末尾(第 200 行 deferredImportSelectorHandler.process())才执行,Group 跨选择器合批,按 @Order 排序——这是 Spring Boot 自动配置"全部配置类解析完再选候选、再由条件逐一否决"的框架基石。
  6. 工程层:条件里只读 environment/类路径/定义层信息,绝不碰 bean 实例;查"bean 是否已注册"的条件必须声明 REGISTER_BEAN;条件注解不继承、必须标在最终类/方法上;条件失败是静默减法,异常才是启动失败。

7.2 本章关键类清单

类名模块与相对路径职责一句话
Conditionspring-context/src/main/java/org/springframework/context/annotation/Condition.java条件顶层函数式接口,matches(context, metadata) 返回 false 即否决注册
ConditionContextspring-context/src/main/java/org/springframework/context/annotation/ConditionContext.java条件上下文接口:registry/beanFactory/environment/resourceLoader/classLoader 五要素
Conditionalspring-context/src/main/java/org/springframework/context/annotation/Conditional.java标在 TYPE/METHOD 上的注解,value() 持有一组 Condition 实现类
ConfigurationConditionspring-context/src/main/java/org/springframework/context/annotation/ConfigurationCondition.java带阶段语义的 Condition 子接口,内含 ConfigurationPhase{PARSE_CONFIGURATION, REGISTER_BEAN}
ConditionEvaluatorspring-context/src/main/java/org/springframework/context/annotation/ConditionEvaluator.java条件评估引擎:shouldSkip 短路循环、条件收集排序、ConditionContextImpl
ConditionContextImplspring-context/src/main/java/org/springframework/context/annotation/ConditionEvaluator.java(内部类)ConditionContext 实现,五个 deduce 方法兜底 registry/env/resourceLoader/classLoader
Profilespring-context/src/main/java/org/springframework/context/annotation/Profile.java组合注解样例:@Conditional(ProfileCondition.class),按激活 profile 装配
ProfileConditionspring-context/src/main/java/org/springframework/context/annotation/ProfileCondition.java@Profile 的条件实现,读注解 value 调 environment.matchesProfiles(...)
ConfigurationClassParserspring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java配置类解析器:第 249 行 PARSE 阶段条件闸门、processImports、DeferredImportSelectorHandler
ConfigurationClassBeanDefinitionReaderspring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassBeanDefinitionReader.javabean 定义落地:第 196 行方法级 REGISTER_BEAN 条件、TrackedConditionEvaluator 类级复审
TrackedConditionEvaluatorspring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassBeanDefinitionReader.java(内部类)一轮注册加载内的类级条件去重 + importedBy 传递否决
ImportSelectorspring-context/src/main/java/org/springframework/context/annotation/ImportSelector.java按导入方元数据返回要导入的类名数组,可带 getExclusionFilter
DeferredImportSelectorspring-context/src/main/java/org/springframework/context/annotation/DeferredImportSelector.java延迟选择器:全部配置类解析完后才 selectImports,支持 Group 聚合
ImportBeanDefinitionRegistrarspring-context/src/main/java/org/springframework/context/annotation/ImportBeanDefinitionRegistrar.java@Import 的第三路:直接向 registry 注册 bean 定义
ClassPathScanningCandidateComponentProviderspring-context/src/main/java/org/springframework/context/annotation/ClassPathScanningCandidateComponentProvider.java组件扫描:TypeFilter 命中后 isConditionMatch 单参数推导评估条件
AnnotatedBeanDefinitionReaderspring-context/src/main/java/org/springframework/context/annotation/AnnotatedBeanDefinitionReader.java编程式注册:第 257 行 shouldSkip 闸门
Environmentspring-core/src/main/java/org/springframework/core/env/Environment.java环境接口:matchesProfiles(String...) 默认方法(6.2+),委托 acceptsProfiles(Profiles)
AbstractEnvironmentspring-core/src/main/java/org/springframework/core/env/AbstractEnvironment.javaacceptsProfiles(Profiles) 实现:profiles.matches(isProfileActive) + default profile 回退
Profilesspring-core/src/main/java/org/springframework/core/env/Profiles.javaprofile 表达式谓词:of(...) 委托 ProfilesParser,支持 & ` !` 与括号

7.3 收尾:为什么这套机制是"自动配置"的基石

回到本章开头的问题。Spring Framework 7.0.9 本身不含任何 @ConditionalOnClass/@EnableAutoConfiguration(那是 spring-boot-autoconfigure 的上层封装),但它把自动配置所需的四块积木全部备齐了:

  • 候选从哪来:ImportSelector/DeferredImportSelector(3.9 节)——"从一个清单里挑出一批配置类";
  • 何时挑:延迟到全部配置类解析完(DeferredImportSelectorHandler.process)——让被挑到的类在评估条件时能看到完整的注册表面;
  • 候选要不要装:@Conditional + ConditionEvaluator 两阶段裁决(3.1-3.7 节)——类路径、属性、profile、"用户是否已自定义",每一票都有;
  • 组件扫描期如何隐身:TypeFilter 与 isConditionMatch 分工(3.10 节)。

上层自动配置做的事,不过是"把几百个配置类的名字写进索引文件,用一个 DeferredImportSelector 读出来,再让每个配置类自己挂一组 Condition"。读懂了本章的框架层机制,自动配置在你眼里就不再是黑盒——它的每一个"约定",都对应本章某一行你已经读过的源码。