Spring Boot自动装配原理揭秘:@Conditional注解全家桶详解

Spring Boot 自动装配原理揭秘:@Conditional 注解全家桶详解

摘要 :Spring Boot 的自动装配(Auto-Configuration)是其"约定优于配置"哲学的核心实现。本文深入源码层面,从 @EnableAutoConfiguration 的加载机制、SpringFactoriesLoader 的 SPI 扩展、到 @Conditional 条件注解的完整家族,逐一拆解自动装配的决策流程,并演示如何编写自定义 Starter。

关键词:Spring Boot、自动装配、Auto-Configuration、@Conditional、SpringFactoriesLoader、自定义 Starter、条件注解


一、自动装配的入口:@EnableAutoConfiguration

1.1 从 @SpringBootApplication 说起

每一个 Spring Boot 应用的主类上,我们都能看到这个熟悉的组合注解:

java 复制代码
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { 
    @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
    @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) 
})
public @interface SpringBootApplication {
}

@EnableAutoConfiguration 是自动装配的开关。它的核心定义:

java 复制代码
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
}

关键在 @Import(AutoConfigurationImportSelector.class) ------ 这是自动装配的触发点。

1.2 AutoConfigurationImportSelector 的加载机制

AutoConfigurationImportSelector 实现了 DeferredImportSelector 接口,这决定了它的特殊加载时机:

  1. 延迟加载 :在所有普通 @Configuration 类解析之后、Bean 定义注册之前执行
  2. 分组处理 :属于 AutoConfigurationGroup,确保同类配置集中处理

核心流程(selectImports() 方法):

java 复制代码
public class AutoConfigurationImportSelector implements DeferredImportSelector {

    @Override
    public String[] selectImports(AnnotationMetadata annotationMetadata) {
        // 1. 检查自动装配是否被禁用(spring.boot.enableautoconfiguration=false)
        if (!isEnabled(annotationMetadata)) {
            return NO_IMPORTS;
        }
        
        // 2. 从 META-INF/spring-autoconfigure-metadata.properties 读取元数据
        AutoConfigurationMetadata autoConfigurationMetadata = 
            AutoConfigurationMetadataLoader.loadMetadata(this.beanClassLoader);
        
        // 3. 通过 SpringFactoriesLoader 获取所有候选配置类
        List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
        
        // 4. 去重、排序、排除、过滤
        configurations = removeDuplicates(configurations);
        configurations = sort(configurations, autoConfigurationMetadata);
        Set<String> exclusions = getExclusions(annotationMetadata, attributes);
        configurations = removeAll(configurations, exclusions);
        configurations = filter(configurations, autoConfigurationMetadata);
        
        // 5. 触发自动装配事件,便于监听器介入
        fireAutoConfigurationImportEvents(configurations, exclusions);
        
        return StringUtils.toStringArray(configurations);
    }

    protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, 
                                                      AnnotationAttributes attributes) {
        // 核心:加载 META-INF/spring.factories 中 EnableAutoConfiguration 对应的值
        List<String> configurations = SpringFactoriesLoader.loadFactoryNames(
            getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader()
        );
        Assert.notEmpty(configurations,
            "No auto configuration classes found in META-INF/spring.factories"
        );
        return configurations;
    }
}

二、SpringFactoriesLoader:SPI 机制的基石

2.1 META-INF/spring.factories 的结构

spring.factories 是 Spring Boot 的 SPI(Service Provider Interface)实现文件,采用 Properties 格式:

properties 复制代码
# Spring Boot 2.7+ 推荐使用 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
# 但 spring.factories 仍兼容

# 自动装配配置类列表
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration,\
  org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration,\
  org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration,\
  org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration,\
  com.example.mystarter.MyCustomAutoConfiguration

# 其他 SPI 扩展点
org.springframework.boot.diagnostics.FailureAnalyzer=\
  com.example.mystarter.MyFailureAnalyzer

org.springframework.boot.env.EnvironmentPostProcessor=\
  com.example.mystarter.MyEnvironmentPostProcessor

2.2 Spring Boot 2.7+ 的新文件格式

为了避免 spring.factories 文件过于庞大,Spring Boot 2.7 引入了新的文件格式:

复制代码
META-INF/spring/
└── org.springframework.boot.autoconfigure.AutoConfiguration.imports

内容变为纯文本列表(每行一个配置类):

复制代码
com.example.mystarter.autoconfigure.MyCustomAutoConfiguration
com.example.mystarter.autoconfigure.MyDataSourceAutoConfiguration

兼容性spring.factories 中的 EnableAutoConfiguration 键仍然有效,但新格式更清晰,且支持按配置类单独管理。Spring Boot 3.x 建议全面迁移到新格式。


三、@Conditional 条件注解全家桶

自动装配的核心决策逻辑是:在候选配置类中,哪些应该被实际加载?这由 @Conditional 注解家族决定。

3.1 条件注解的通用机制

@Conditional 本身是一个元注解,接受一个 Condition 接口实现类:

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

Condition 接口定义:

java 复制代码
@FunctionalInterface
public interface Condition {
    boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);
}
  • ConditionContext:提供 BeanDefinitionRegistry、Environment、ResourceLoader 等上下文
  • AnnotatedTypeMetadata:获取注解的元数据信息

3.2 内置条件注解详解

注解 条件 典型使用场景
@ConditionalOnClass 类路径存在指定类 依赖库存在时启用配置(如 DataSource 存在时启用 JPA)
@ConditionalOnMissingClass 类路径不存在指定类 避免与旧版本库冲突
@ConditionalOnBean 容器中存在指定 Bean 某数据源已配置时启用 DAO 层
@ConditionalOnMissingBean 容器中不存在指定 Bean 提供默认实现,允许用户自定义覆盖
@ConditionalOnProperty 配置属性匹配指定值 通过 application.yml 开关控制功能
@ConditionalOnResource 类路径存在指定资源 配置文件存在时启用特定功能
@ConditionalOnWebApplication 当前是 Web 应用 区分 Servlet / Reactive 环境
@ConditionalOnNotWebApplication 当前不是 Web 应用 批处理、CLI 工具等场景
@ConditionalOnExpression SpEL 表达式为 true 复杂逻辑条件判断
@ConditionalOnJava 匹配的 Java 版本 利用 Java 新特性的配置
@ConditionalOnJndi JNDI 存在指定位置 传统 EE 容器兼容
@ConditionalOnSingleCandidate 指定类型只有一个候选 Bean 消除歧义,自动选择唯一实现
@ConditionalOnWarDeployment 以 WAR 方式部署 嵌入式 vs 外置容器区分

3.3 条件注解源码解析

@ConditionalOnClass 为例:

java 复制代码
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(OnClassCondition.class)
public @interface ConditionalOnClass {
    Class<?>[] value() default {};       // 类引用(编译时安全)
    String[] name() default {};            // 类名字符串(避免 ClassNotFoundException)
}

对应的 OnClassCondition 实现:

java 复制代码
@Order(Ordered.HIGHEST_PRECEDENCE)  // 最高优先级,优先过滤
class OnClassCondition extends FilteringSpringBootCondition {

    @Override
    protected ConditionOutcome[] getOutcomes(String[] autoConfigurationClasses,
                                             AutoConfigurationMetadata autoConfigurationMetadata) {
        // 使用 ASM 读取 .class 文件,避免直接加载类
        // 这样可以在不触发 ClassLoader 的情况下检查类是否存在
        List<ConditionOutcome> outcomes = new ArrayList<>(autoConfigurationClasses.length);
        
        for (String autoConfigurationClass : autoConfigurationClasses) {
            ConditionOutcome outcome = null;
            if (autoConfigurationClass != null) {
                String candidates = autoConfigurationMetadata.get(autoConfigurationClass, "ConditionalOnClass");
                if (candidates != null) {
                    outcome = getOutcome(candidates);
                }
            }
            outcomes.add(outcome);
        }
        return outcomes.toArray(new ConditionOutcome[0]);
    }

    private ConditionOutcome getOutcome(String candidates) {
        try {
            List<String> classNames = Arrays.asList(StringUtils.commaDelimitedListToStringArray(candidates));
            for (String className : classNames) {
                if (!ClassNameFilter.MATCH.match(className, getBeanClassLoader())) {
                    return ConditionOutcome.noMatch("@ConditionalOnClass missing " + className);
                }
            }
        } catch (Exception ex) {
            // 处理异常
        }
        return null; // 所有类都存在,条件匹配
    }
}

关键设计OnClassCondition 使用 ClassNameFilter 进行 ASM 字节码扫描 ,而非 Class.forName()。这样做的原因是:如果直接加载类,可能会触发该类的静态初始化,甚至引发 NoClassDefFoundError。ASM 扫描只读取类名,不加载类,更安全。

3.4 组合条件:AnyCondition 与 AllNestedConditions

Spring Boot 也支持条件组合:

java 复制代码
@ConditionalOnClass(DataSource.class)
@ConditionalOnBean(DataSource.class)
@ConditionalOnProperty(prefix = "app.mybatis", name = "enabled", havingValue = "true", matchIfMissing = true)
public class MyBatisAutoConfiguration {
    // 类上的多个条件注解是 AND 关系:必须全部满足
}

自定义 OR 组合条件:

java 复制代码
public class OnRedisOrCacheCondition extends AnyNestedCondition {

    OnRedisOrCacheCondition() {
        super(ConfigurationPhase.REGISTER_BEAN);  // 在 Bean 注册阶段判断
    }

    @ConditionalOnClass(RedisConnectionFactory.class)
    @ConditionalOnBean(RedisConnectionFactory.class)
    static class RedisCondition {}

    @ConditionalOnClass(CacheManager.class)
    @ConditionalOnProperty(prefix = "spring.cache", name = "type", havingValue = "simple")
    static class CacheCondition {}
}
java 复制代码
@Conditional(OnRedisOrCacheCondition.class)
public class SessionAutoConfiguration {
    // 满足 RedisCondition OR CacheCondition 任一即可激活
}

四、自动装配的执行顺序:@AutoConfigureOrder 与 @AutoConfigureAfter

4.1 配置类的加载顺序控制

自动配置类之间往往有依赖关系。例如:

  • DataSourceAutoConfiguration 必须在 HibernateJpaAutoConfiguration 之前
  • ServletWebServerFactoryAutoConfiguration 必须在 DispatcherServletAutoConfiguration 之前

Spring Boot 提供了三种顺序控制机制:

注解 作用 示例
@AutoConfigureOrder 全局排序值(Ordered 接口) @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
@AutoConfigureAfter 在指定配置类之后加载 @AutoConfigureAfter(DataSourceAutoConfiguration.class)
@AutoConfigureBefore 在指定配置类之前加载 @AutoConfigureBefore(HibernateJpaAutoConfiguration.class)

4.2 源码中的典型示例

java 复制代码
@Configuration(proxyBeanMethods = false)
@AutoConfigureAfter(DataSourceAutoConfiguration.class)      // 数据源配置完成后
@AutoConfigureBefore(HibernateJpaAutoConfiguration.class)    // 在 JPA 之前
@ConditionalOnClass({ LocalContainerEntityManagerFactoryBean.class, EntityManager.class })
@ConditionalOnBean(DataSource.class)                         // 需要 DataSource Bean
@EnableConfigurationProperties(JpaProperties.class)
@Import({ HibernateJpaConfiguration.class, DataSourceInitializedPublisher.class })
public class HibernateJpaAutoConfiguration {
    // 配置内容...
}

五、自定义 Starter 实战:从零到发布

5.1 Starter 的项目结构

一个标准的自定义 Starter 项目结构:

复制代码
my-custom-spring-boot-starter/
├── pom.xml
├── src/
│   ├── main/
│   │   ├── java/
│   │   │   └── com/example/mystarter/
│   │   │       ├── auto/
│   │   │       │   ├── MyServiceAutoConfiguration.java
│   │   │       │   └── MyServiceProperties.java
│   │   │       ├── service/
│   │   │       │   ├── MyService.java
│   │   │       │   └── DefaultMyService.java
│   │   │       └── condition/
│   │   │           └── OnMyFeatureEnabledCondition.java
│   │   └── resources/
│   │       └── META-INF/
│   │           └── spring/
│   │               └── org.springframework.boot.autoconfigure.AutoConfiguration.imports
│   └── test/
│       └── java/
│           └── com/example/mystarter/
│               └── MyServiceAutoConfigurationTest.java

5.2 核心文件实现

AutoConfiguration.imports(Spring Boot 3.x 格式):

复制代码
com.example.mystarter.auto.MyServiceAutoConfiguration

属性配置类

java 复制代码
package com.example.mystarter.auto;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.bind.DefaultValue;

@ConfigurationProperties(prefix = "my.starter")
public class MyServiceProperties {

    /**
     * 是否启用该 Starter 的功能
     */
    private boolean enabled = true;

    /**
     * 服务名称
     */
    private String serviceName = "default-service";

    /**
     * 最大重试次数
     */
    @DefaultValue("3")
    private int maxRetries;

    /**
     * 超时时间(毫秒)
     */
    @DefaultValue("5000")
    private long timeoutMillis;

    // Getters and Setters
    public boolean isEnabled() { return enabled; }
    public void setEnabled(boolean enabled) { this.enabled = enabled; }
    public String getServiceName() { return serviceName; }
    public void setServiceName(String serviceName) { this.serviceName = serviceName; }
    public int getMaxRetries() { return maxRetries; }
    public void setMaxRetries(int maxRetries) { this.maxRetries = maxRetries; }
    public long getTimeoutMillis() { return timeoutMillis; }
    public void setTimeoutMillis(long timeoutMillis) { this.timeoutMillis = timeoutMillis; }
}

服务接口与默认实现

java 复制代码
package com.example.mystarter.service;

public interface MyService {
    String process(String input);
    boolean healthCheck();
}
java 复制代码
package com.example.mystarter.service;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class DefaultMyService implements MyService {

    private static final Logger log = LoggerFactory.getLogger(DefaultMyService.class);

    private final String serviceName;
    private final int maxRetries;
    private final long timeoutMillis;

    public DefaultMyService(String serviceName, int maxRetries, long timeoutMillis) {
        this.serviceName = serviceName;
        this.maxRetries = maxRetries;
        this.timeoutMillis = timeoutMillis;
    }

    @Override
    public String process(String input) {
        log.info("[{}] Processing input with timeout={}ms, retries={}", 
            serviceName, timeoutMillis, maxRetries);
        return String.format("[%s] Processed: %s", serviceName, input.toUpperCase());
    }

    @Override
    public boolean healthCheck() {
        return true;
    }
}

自动配置类

java 复制代码
package com.example.mystarter.auto;

import com.example.mystarter.service.DefaultMyService;
import com.example.mystarter.service.MyService;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;

/**
 * 自定义 Starter 的自动配置类
 */
@AutoConfiguration
@ConditionalOnClass(MyService.class)                                    // 类路径存在 MyService
@ConditionalOnProperty(prefix = "my.starter", name = "enabled", 
                       havingValue = "true", matchIfMissing = true)    // 默认启用
@EnableConfigurationProperties(MyServiceProperties.class)
public class MyServiceAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean(MyService.class)  // 用户未自定义时提供默认实现
    public MyService myService(MyServiceProperties properties) {
        return new DefaultMyService(
            properties.getServiceName(),
            properties.getMaxRetries(),
            properties.getTimeoutMillis()
        );
    }
}

5.3 条件注解的深度应用:自定义 Condition

java 复制代码
package com.example.mystarter.condition;

import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
import org.springframework.boot.autoconfigure.condition.SpringBootCondition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;

/**
 * 自定义条件:检查是否处于生产环境
 */
public class OnProductionEnvironmentCondition extends SpringBootCondition {

    @Override
    public ConditionOutcome getMatchOutcome(ConditionContext context, 
                                           AnnotatedTypeMetadata metadata) {
        String activeProfile = context.getEnvironment().getProperty("spring.profiles.active", "");
        boolean isProd = activeProfile.contains("prod") || 
                        activeProfile.contains("production");
        
        return isProd 
            ? ConditionOutcome.match("Production environment detected")
            : ConditionOutcome.noMatch("Not in production environment");
    }
}
java 复制代码
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(OnProductionEnvironmentCondition.class)
public @interface ConditionalOnProduction {
}

5.4 使用 Starter 的应用端配置

yaml 复制代码
# application.yml
my:
  starter:
    enabled: true
    service-name: payment-gateway
    max-retries: 5
    timeout-millis: 10000

spring:
  profiles:
    active: prod
java 复制代码
package com.example.myapp;

import com.example.mystarter.service.MyService;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

@Component
public class AppRunner implements CommandLineRunner {

    private final MyService myService;

    public AppRunner(MyService myService) {
        this.myService = myService;
    }

    @Override
    public void run(String... args) {
        String result = myService.process("hello world");
        System.out.println(result);  // 输出: [payment-gateway] Processed: HELLO WORLD
        System.out.println("Health: " + myService.healthCheck());
    }
}

5.5 测试自动配置

java 复制代码
package com.example.mystarter;

import com.example.mystarter.auto.MyServiceAutoConfiguration;
import com.example.mystarter.service.MyService;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import static org.assertj.core.api.Assertions.assertThat;

class MyServiceAutoConfigurationTest {

    private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
        .withConfiguration(AutoConfigurations.of(MyServiceAutoConfiguration.class));

    @Test
    void serviceBeanIsCreatedByDefault() {
        this.contextRunner
            .withPropertyValues("my.starter.service-name=test-service")
            .run(context -> {
                assertThat(context).hasSingleBean(MyService.class);
                assertThat(context.getBean(MyService.class).process("test"))
                    .contains("test-service");
            });
    }

    @Test
    void serviceBeanIsNotCreatedWhenDisabled() {
        this.contextRunner
            .withPropertyValues("my.starter.enabled=false")
            .run(context -> assertThat(context).doesNotHaveBean(MyService.class));
    }

    @Test
    void userDefinedBeanOverridesDefault() {
        this.contextRunner
            .withBean(MyService.class, () -> input -> "Custom: " + input)
            .run(context -> {
                assertThat(context).hasSingleBean(MyService.class);
                assertThat(context.getBean(MyService.class).process("test"))
                    .isEqualTo("Custom: test");
            });
    }
}

六、自动装配的调试与诊断

6.1 开启自动装配报告

application.yml 中添加:

yaml 复制代码
debug: true

或启动时添加参数:

bash 复制代码
java -jar myapp.jar --debug

控制台将输出详细的 Positive matches (已匹配)和 Negative matches(未匹配)报告:

复制代码
============================
CONDITIONS EVALUATION REPORT
============================

Positive matches:
-----------------
   AopAutoConfiguration matched:
      - @ConditionalOnProperty (spring.aop.auto=true) matched (OnPropertyCondition)
   DataSourceAutoConfiguration matched:
      - @ConditionalOnClass (javax.sql.DataSource, ...) found required classes

Negative matches:
-----------------
   ActiveMQAutoConfiguration:
      Did not match:
         - @ConditionalOnClass did not find required class 'jakarta.jms.ConnectionFactory'

6.2 使用 EnvironmentEndpoint 查看加载的配置

bash 复制代码
curl http://localhost:8080/actuator/conditions

返回 JSON 格式的条件评估结果,便于程序化分析。


总结

Spring Boot 的自动装配机制是一个精心设计的分层决策系统:

  1. 入口层@EnableAutoConfigurationAutoConfigurationImportSelectorSpringFactoriesLoader
  2. 发现层 :从 META-INF/spring.factoriesMETA-INF/spring/*.imports 读取候选配置
  3. 决策层@Conditional 注解家族执行条件匹配,过滤出实际需要加载的配置
  4. 顺序层@AutoConfigureOrder / @AutoConfigureAfter / @AutoConfigureBefore 控制依赖顺序
  5. 执行层:Spring 容器注册 Bean 定义,完成自动装配

关键要点回顾:

  • SpringFactoriesLoader 是 SPI 扩展的核心,新格式 AutoConfiguration.imports 更清晰
  • @ConditionalOnClass 使用 ASM 扫描避免类加载副作用,是设计亮点
  • @ConditionalOnMissingBean 提供默认实现同时允许用户覆盖,是"约定优于配置"的精髓
  • 自定义 Starter 需遵循 AutoConfigurationPropertiesCondition 三层结构
  • 使用 ApplicationContextRunner 进行自动配置测试,是 Spring Boot 2.0+ 推荐方式

建议 :在生产环境中使用 debug: true 或 Actuator 的 conditions 端点排查自动装配问题。当多个 Starter 冲突时,优先通过 @ConditionalOnProperty 提供显式开关,而非依赖 @Order 调整优先级,这样更可控、更透明。