Spring Boot 自动配置原理与手写一个可用的 Starter

从 @EnableAutoConfiguration 到条件装配的加载顺序,再手写一个能用的 Starter。搞懂自动配置,才敢改别人的脚手架。

Spring Boot 最核心的能力是"依赖了就自动配好"。你加个 spring-boot-starter-data-redis,什么都没写,RedisTemplate 就能注入了。这背后不是魔法,是一套设计得相当克制的加载机制。

这篇从注解拆到源码,最后手写一个能用的 Starter。

一、@SpringBootApplication 拆开是什么

@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

这个注解是三个注解的组合:

@SpringBootConfiguration      // 本质是 @Configuration,标记配置类
@EnableAutoConfiguration      // 开启自动配置,核心
@ComponentScan                // 扫描当前包及子包的 @Component

@ComponentScan 的默认范围是启动类所在包及其子包。这就是为什么"我加了 @Service 但注入失败"最常见的原因是包放错位置——启动类在 com.example.app,服务类在 com.example.service,扫不到。

@EnableAutoConfiguration 才是自动配置的入口:

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

关键在 @Import(AutoConfigurationImportSelector.class)——它把一个 ImportSelector 注册进容器,这个 selector 返回的类名列表就是所有要生效的自动配置类。

二、自动配置的加载流程

AutoConfigurationImportSelector.selectImports() 做的事,简化后是这样:

// 1. 找出所有候选配置类
List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);

// 2. 去重
configurations = removeDuplicates(configurations);

// 3. 按条件过滤(这一步才是精华)
configurations = filter(configurations, autoConfigurationMetadata);

// 4. 排序(@AutoConfigureBefore / @AutoConfigureAfter)
configurations = sortAutoConfigurations(configurations, ...);

// 5. 排除用户显式 exclude 的
return configurations.toArray(new String[0]);

第 1 步读的是 classpath 下所有 jar 里的清单文件。Spring Boot 2.7 之前是:

META-INF/spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.FooAutoConfiguration,\
com.example.BarAutoConfiguration

Spring Boot 2.7 之后改用新文件:

META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
com.example.FooAutoConfiguration
com.example.BarAutoConfiguration

这个变化值得特别注意:2.7 开始两种都支持(spring.factories 标记为废弃),3.0 之后新文件是唯一方式。如果你维护的 Starter 还在用 spring.factories 注册自动配置,升级到 Boot 3 后会静默失效——不报错,只是不生效。

三、条件注解:什么时候该生效

自动配置类全部加载进容器是不可能的——DataSourceAutoConfiguration 在没有数据库驱动时就不该生效。控制这件事的是条件注解族:

注解 判断依据 典型用法
@ConditionalOnClass classpath 有某个类 有 DataSource 才配数据源
@ConditionalOnMissingBean 容器里没有某个 Bean 用户没自定义时才用默认的
@ConditionalOnProperty 配置项存在且等于指定值 开关某个功能
@ConditionalOnBean 容器里已有某个 Bean 依赖前置 Bean
@ConditionalOnWebApplication 是 Web 应用 排除非 Web 场景
@ConditionalOnResource 存在某个资源文件 配置文件驱动的功能
@ConditionalOnExpression SpEL 表达式为真 复杂组合条件

抓一个真实的例子看,DataSourceAutoConfiguration:

@AutoConfiguration
@ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class })
@EnableConfigurationProperties(DataSourceProperties.class)
@Import({ DataSourcePoolMetadataProvidersConfiguration.class,
          DataSourceCheckpointRestoreConfiguration.class })
public class DataSourceAutoConfiguration {

    @Configuration(proxyBeanMethods = false)
    @Conditional(PooledDataSourceCondition.class)
    @ConditionalOnMissingBean({ DataSource.class, XADataSource.class })
    @Import({ DataSourceConfiguration.Hikari.class, ... })
    protected static class PooledDataSourceConfiguration { }
}

读法就是一句话:classpath 里有 DataSource 类,并且用户没有自己定义 DataSource Bean,才用内置的连接池配置。

@ConditionalOnMissingBean 是"约定优于配置"能落地的关键——它保证了用户的定义永远优先于框架的默认。这也是为什么你加一个 @Bean DataSource 就能整个接管数据源配置。

四、手写一个 Starter

假设我们要做一个"审计日志"组件:只要方法加了 @AuditLog,就自动把参数、耗时、结果记下来。

4.1 项目结构

推荐拆成两个模块:

audit-spring-boot-starter/
├── pom.xml                                    <- 只做依赖聚合
└── audit-spring-boot-autoconfigure/
    ├── pom.xml
    └── src/main/
        ├── java/com/example/audit/
        │   ├── AuditLog.java                  <- 注解
        │   ├── AuditProperties.java           <- 配置项
        │   ├── AuditAspect.java               <- 切面
        │   └── AuditAutoConfiguration.java    <- 自动配置
        └── resources/META-INF/spring/
            └── org.springframework.boot.autoconfigure.AutoConfiguration.imports

为什么拆两个模块:Starter 只放依赖,autoconfigure 放代码。这样用户可以只依赖 autoconfigure(比如自己组装依赖版本),也是 Spring Boot 官方 Starter 的统一做法。

4.2 注解

package com.example.audit;

import java.lang.annotation.*;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AuditLog {
    /** 操作描述 */
    String value() default "";
    /** 是否记录返回值 */
    boolean recordResult() default false;
}

4.3 配置属性

package com.example.audit;

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

import java.time.Duration;
import java.util.ArrayList;
import java.util.List;

@ConfigurationProperties(prefix = "audit")
public class AuditProperties {

    /** 是否启用 */
    private boolean enabled = true;

    /** 超过这个耗时才记录,默认全记 */
    private Duration slowThreshold = Duration.ZERO;

    /** 不记录参数的字段名(脱敏) */
    private List<String> excludeFields = new ArrayList<>();

    /** 日志前缀 */
    private String prefix = "[AUDIT]";

    // getter / setter 省略
}

关键点:配置类只做数据载体,不加 @Component。@ConfigurationProperties 会被 @EnableConfigurationProperties 注册,加了 @Component 反而会导致在非 Boot 环境重复注册。

4.4 切面

package com.example.audit;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@Aspect
public class AuditAspect {

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

    private final AuditProperties properties;

    public AuditAspect(AuditProperties properties) {
        this.properties = properties;
    }

    @Around("@annotation(auditLog)")
    public Object around(ProceedingJoinPoint pjp, AuditLog auditLog) throws Throwable {
        long start = System.currentTimeMillis();
        MethodSignature sig = (MethodSignature) pjp.getSignature();
        String method = sig.getDeclaringType().getSimpleName() + "#" + sig.getName();
        String desc = auditLog.value().isEmpty() ? method : auditLog.value();

        try {
            Object result = pjp.proceed();
            long cost = System.currentTimeMillis() - start;
            if (cost >= properties.getSlowThreshold().toMillis()) {
                log.info("{} {} 成功 cost={}ms args={}", properties.getPrefix(), desc, cost,
                        auditLog.recordResult() ? result : maskArgs(pjp.getArgs()));
            }
            return result;
        } catch (Throwable e) {
            long cost = System.currentTimeMillis() - start;
            log.warn("{} {} 失败 cost={}ms err={}", properties.getPrefix(), desc, cost, e.getMessage());
            throw e;
        }
    }

    private String maskArgs(Object[] args) {
        // 简化实现:实际项目里按字段名遍历脱敏
        return args == null ? "" : java.util.Arrays.toString(args);
    }
}

注意这里用构造器注入,不加 @Component——Bean 的注册交给自动配置类,这样才受 @ConditionalOnMissingBean 控制。

4.5 自动配置类

package com.example.audit;

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;
import org.aspectj.lang.annotation.Aspect;

@AutoConfiguration
@ConditionalOnClass({ Aspect.class })
@ConditionalOnProperty(prefix = "audit", name = "enabled", havingValue = "true", matchIfMissing = true)
@EnableConfigurationProperties(AuditProperties.class)
public class AuditAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean
    public AuditAspect auditAspect(AuditProperties properties) {
        return new AuditAspect(properties);
    }
}

四个注解各司其职:

  • @AutoConfiguration —— Boot 2.7+ 的专用注解,本质是 @Configuration(proxyBeanMethods = false),额外支持 before/after 排序
  • @ConditionalOnClass —— 没有 AspectJ 就不启用(防止用户没引 aop 依赖时启动报错)
  • @ConditionalOnProperty —— 支持 audit.enabled=false 一键关闭
  • @ConditionalOnMissingBean —— 用户自己定义了 AuditAspect 就用他的

proxyBeanMethods = false 值得单独说。设为 false 后,@Bean 方法之间不能直接调用来获取单例(会每次 new 一个),但省掉了 CGLIB 子类生成,启动更快、内存更省。对于"Bean 之间没有互相引用"的自动配置类,这是推荐值。

4.6 注册自动配置

src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports:

com.example.audit.AuditAutoConfiguration

一行,全限定类名,不要多余空行和注释(解析器容错有限)。

4.7 依赖打包

autoconfigure 模块的 pom 里,自动配置类需要的依赖用 optional:

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-autoconfigure</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-configuration-processor</artifactId>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjweaver</artifactId>
        <optional>true</optional>
    </dependency>
</dependencies>

optional 的意义:编译需要它,但不强制传递给使用者。配合 @ConditionalOnClass,用户没引 aspectj 时自动配置不生效,而不是启动直接 NoClassDefFoundError。这是 Starter 健壮性的关键设计。

spring-boot-configuration-processor 会在编译期生成 META-INF/spring-configuration-metadata.json,IDE 里就有配置提示了。

4.8 使用

<dependency>
    <groupId>com.example</groupId>
    <artifactId>audit-spring-boot-starter</artifactId>
    <version>1.0.0</version>
</dependency>
audit:
  enabled: true
  slow-threshold: 500ms
  exclude-fields:
    - password
    - idCard
@AuditLog("创建订单")
public void createOrder(Order order) { ... }

零配置即可工作,audit.enabled 一键关闭。

五、排查自动配置为什么没生效

5.1 打开条件评估报告

java -jar app.jar --debug

或者配置:

debug: true

启动日志里会出现 CONDITIONS EVALUATION REPORT,分成三块:

Positive matches:        <- 生效了,以及为什么
   DataSourceAutoConfiguration matched:
      - @ConditionalOnClass found required classes 'javax.sql.DataSource'...

Negative matches:        <- 没生效,以及为什么
   RedisAutoConfiguration did not match:
      - @ConditionalOnClass did not find required class 'redis.clients.jedis.Jedis'

Exclusions:              <- 被排除的

"Negative matches" 是排查的核心,它直接告诉你缺了哪个类、哪个属性不对。

5.2 用 Actuator 看运行时状态

management:
  endpoints:
    web:
      exposure:
        include: beans,conditions,configprops
curl -s localhost:8080/actuator/conditions | jq '.contexts.application.negativeMatches'
curl -s localhost:8080/actuator/configprops | jq '.contexts.application.beans[].prefix'

configprops 能看到所有 @ConfigurationProperties 的实际绑定值,排查"配置写了没生效"特别有效——如果这里显示的是默认值,说明前缀写错了或者没有 @EnableConfigurationProperties。

5.3 几个高频错误

现象 原因
自动配置类完全没加载 AutoConfiguration.imports 路径写错(注意是 META-INF/spring/ 子目录)
报 BeanDefinitionOverrideException 自动配置类没加 @ConditionalOnMissingBean,与用户 Bean 冲突
NoClassDefFoundError 依赖没标 optional,或漏了 @ConditionalOnClass
配置项不生效 前缀写错、属性名没有 kebab-case、缺 @EnableConfigurationProperties
Boot 3 升级后失效 还在用 spring.factories,需迁到 AutoConfiguration.imports

六、Starter 的命名与分层的约定

Spring Boot 官方的命名规则值得遵循:

  • 第三方 Starter:xxx-spring-boot-starter(如 mybatis-spring-boot-starter)
  • 官方 Starter:spring-boot-starter-xxx(如 spring-boot-starter-web)
  • 不要用 spring-boot-starter- 开头做自己的包,会和官方混淆

依赖层次上有个经验值:Starter 里的传递依赖要尽可能少。一个只做审计的 Starter 不该顺带引入整个 spring-boot-starter-web,那会让不支持 Web 的项目无法使用。用 optional + 条件注解把依赖关系变成"可选的增强",而不是"强制的前置"。

小结

自动配置的机制可以用一句话概括:"把一堆候选配置类列成清单,逐个用条件注解判断该不该生效,用户定义永远优先"。

理解这个模型之后,那些曾经神秘的行为就都有解释了:

  • 为什么加了依赖就有 Bean —— 清单里有对应的自动配置类
  • 为什么自己定义的 Bean 能覆盖默认 —— @ConditionalOnMissingBean
  • 为什么某些功能"莫名不生效" —— 某个条件不满足,--debug 一看便知

写 Starter 时最容易漏的三件事,值得贴在显示器上:清单文件放在 META-INF/spring/ 子目录下、依赖标 optional、自动配置类加 @ConditionalOnMissingBean。

查看「Java 与 JVM」全部文章 →

同分类文章

查看 0 评论 0

评论

正在加载评论…

留言提交后自动审核:正常内容立即显示,含敏感信息的会转人工复核。

Spring Boot 自动配置原理与手写一个可用的 Starter - 起航社区