首页 / 资讯中心 / 文章详情

Spring Boot自动配置原理与Starter手写实战:从条件注解到Bean装配

Spring Boot自动配置原理与Starter手写实战:从条件注解到Bean装配 ★ FEATURED ARTICLE
Spring Boot 用得越久越容易把它当成一个只会读配置的魔法盒子。平时写接口、连数据库、接消息队列依赖一引、注解一放事情自己就跑起来了。直到有段日子我为了给团队封装公共组件硬着头皮去手写 starter才发现自动配置这套链路其实一点都不玄它无非就是读一个配置清单 做一堆条件判断 按顺序注册 Bean。这篇博客先顺着 Spring Boot 的启动链路把自动配置原理拆开讲清楚再带你把一个操作日志starter 从注解、切面、配置绑定到装配注册完整跑一遍。适合已经写过一段时间 Spring Boot、想搞明白自动装配背后逻辑的人也适合准备在团队内沉淀公共组件、减少各处重复代码的后端同学。1. Spring Boot 到底帮我们做了什么1.1 SpringBootApplication 其实是个伪装的三合一绝大多数项目入口都长这样SpringBootApplication public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } }这个注解看着人畜无害实际上是三个注解的组合SpringBootConfiguration、EnableAutoConfiguration、ComponentScan。SpringBootConfiguration本质是Configuration只是让 Spring 知道这是一个配置类ComponentScan扫描主类所在包以及子包把业务项目里自己写的Component、Service这类 Bean 收进容器EnableAutoConfiguration才是整条自动配置链路的入口它通过Import(AutoConfigurationImportSelector.class)引入一个选择器。所以关键不在主类而在那个选择器。你可以把它理解成一个大管家项目启动时它不会凭经验猜你要什么而是先拿到一张写满候选配置类的清单然后逐个问现在这个项目里条件满足吗满足就加载不满足就跳过。很多人第一次看自动配置以为是想加什么功能就随便写个类进去。其实自动配置不是乱配它遵循的是你依赖里有什么、配置里写了什么、容器里缺什么这三个事实。整个判断过程在启动阶段完成跑完以后该注册的 Bean 全部进了容器业务代码里直接注入就行。1.2 自动配置的完整判定流程AutoConfigurationImportSelector干的事可以简化成四步从 classpath 下找自动配置清单文件老版本是META-INF/spring.factories新版本是META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports读取文件里的候选配置类全限定名列表排除掉EnableAutoConfiguration的exclude、excludeName指定的类把剩下的配置类交给配置解析器处理再按AutoConfigureOrder、AutoConfigureBefore、AutoConfigureAfter排序逐个评估条件注解。这里有个很容易忽视的点每个自动配置类内部都有大量条件注解。明明是同一个候选配置类在依赖完整、配置项匹配时才会生效。所以自动配置类能不能加载和这个类在不在清单里是两回事后者由条件注解决定这也是排查问题时最要命的地方。举个典型的例子某个项目的 classpath 里没有 Web 相关依赖那WebMvcAutoConfiguration虽然在清单里但因为ConditionalOnClass(Servlet.class)不满足它整段就被跳过了。这种做法保证了自动配置的多退少补不会因为引入一个通用 starter 就把一堆无关 Bean 塞进容器。1.3 条件注解就是一群裁判条件注解是自动配置的核心裁判组每个裁判负责一类事实判断ConditionalOnClass/ConditionalOnMissingClass判断某个类在不在 classpath本质是判断你有没有引入某个依赖ConditionalOnProperty判断配置文件中是否出现了某个配置项适合用来自定义开关ConditionalOnBean/ConditionalOnMissingBean判断容器里有没有某个类型的 Bean用于防止用户自定义实现被默认实现顶掉ConditionalOnWebApplication判断当前应用是不是 Web 应用ConditionalOnSingleCandidate判断某个类型的 Bean 是否只有一个候选。这些条件可以用在类级别也可以用在方法级别。类级别不满足整个配置类直接跳过方法级别不满足只跳过这一个Bean方法。实际写 starter 时这两种用法都会遇到。想知道最终哪些自动配置生效了启动时加--debug或者在application.yml里写debug: true控制台会打印一份自动配置报告。里面分三块Positive matches条件命中、生效的配置、Negative matches条件没命中、被跳过的配置、Exclusions被显式排除的配置。排查为什么我的 starter 没生效时这份报告是第一现场后面实战部分我会反复用到一个命令。2. Starter 的组成与装配链路2.1 命名规范和依赖的边界Spring 官方的 starter 叫spring-boot-starter-xxx比如spring-boot-starter-web、spring-boot-starter-data-redis。第三方为了区分通常翻过来叫xxx-spring-boot-starter比如mybatis-spring-boot-starter。这个约定不是摆设别人一看 artifactId 就知道这是个组装包还是业务代码也方便在仓库里检索。starter 本质上就是一个普通 Maven 工程只是它额外包含自动配置类和注册文件。依赖设计上有一个重要原则把启动器需要的最小依赖和业务方按需自己引入的依赖分开。特别是可选的实现依赖要用optionaltrue/optional或者scopeprovided/scope标注否则会把整个依赖树强灌给使用方。拿我写过的操作日志 starter 举例它的 AOP 切面依赖了spring-boot-starter-aop但这个依赖被打成了 optional。业务方只想用简单注解记录日志他必须自己引入 AOP 依赖否则切面不生效。这是刻意为之starter 只提供能力不强制你为用不到的功能背负一堆传递依赖。2.2 从 spring.factories 到 AutoConfiguration.imports自动配置类要能被加载必须在清单文件里登记。这个登记位置经历过一次迁移。Spring Boot 2.7 之前写在META-INF/spring.factoriesorg.springframework.boot.autoconfigure.EnableAutoConfiguration\ com.example.auditlog.AuditLogAutoConfigurationSpring Boot 2.7 之后推荐写在META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.importscom.example.auditlog.AuditLogAutoConfiguration到 Spring Boot 3.0老的spring.factories方式对自动配置的支持已经被移除新的 imports 文件成了唯一入口。我建议新写的 starter 直接采用新方式。如果你需要同时兼容 2.x 和 3.x可以在工程里同时保留两个文件Boot 2.7 会优先读取 imports 文件Boot 3 只看 imports 文件两边都不耽误。文件名和路径不能写错路径差一级目录自动配置就静默消失了。2.3 装配顺序和覆盖规则自动配置类之间的顺序由AutoConfigureBefore、AutoConfigureAfter、AutoConfigureOrder控制。这个顺序在真实框架里很重要比如 Redis 自动配置往往排在 JSON 序列化配置后面因为它需要借助序列化器来装配RedisTemplate。顺序问题的本质是依赖关系某个配置要基于另一个配置已经注册的 Bean 来工作就必须晚于对方执行。如果你希望自己的 starter 覆盖某个基础配置可以用AutoConfigureBefore提前执行用自己的 Bean 抢在前面。覆盖规则还有一个经典约束默认实现必须配ConditionalOnMissingBean。意思是如果用户已经在业务代码里声明了同类型 Bean就别用我的默认实现。没加这个条件用户想替换默认实现时要么BeanDefinitionStoreException要么出现两个同类型 Bean 导致注入报错。这个条件是所有通用 starter 的底线。3. 手写一个操作日志 Starter 完整实操3.1 先把需求定清楚我打算写一个操作日志 starter目标是这样的给任意 Service 方法或 Controller 接口加上一个自定义注解方法执行时自动记录操作类型、描述、方法签名、入参和耗时默认实现把日志打印到控制台方便快速验证支持配置开关和自定义实现替换业务方引入依赖就能用不写任何手动 Bean 注册代码。为什么用 AOP 而不是拦截器拦截器只能拦截 HTTP 请求路由规则绑定在 Web 层而 Spring AOP 能切任意 Bean 方法注解往 Service 方法上一放也能记录使用面广得多。代价是引入 AOP 后有一点运行时开销但记录方法耗时这种场景完全能接受。3.2 工程结构和依赖配置整个工程结构如下audit-log-spring-boot-starter/ ├── pom.xml └── src/main/java/com/example/auditlog/ ├── AuditLog.java // 自定义注解 ├── AuditRecord.java // 日志记录对象 ├── AuditLogger.java // 日志输出接口 ├── ConsoleAuditLogger.java // 默认控制台实现 ├── AuditLogAspect.java // 切面类 ├── AuditLogProperties.java // 配置属性类 └── AuditLogAutoConfiguration.java // 自动配置类pom 的关键配置parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent groupIdcom.example/groupId artifactIdaudit-log-spring-boot-starter/artifactId version1.0.0/version dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-autoconfigure/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-aop/artifactId optionaltrue/optional /dependency /dependencies build plugins plugin groupIdorg.springframework.boot/groupId artifactIdspring-boot-maven-plugin/artifactId configuration skiptrue/skip /configuration /plugin /plugins /build这里有个很值得注意的坑starter 是给别人当依赖用的所以构建时必须跳过spring-boot-maven-plugin的 repackage。可执行 jar 的内部结构是BOOT-INF/classes普通工程引进去根本加载不到类会报一堆 ClassNotFound。跳过后打出来的就是普通 jar能被正常依赖。3.3 先定义注解和日志接口自定义注解Target(ElementType.METHOD) Retention(RetentionPolicy.RUNTIME) public interface AuditLog { String type() default system; String desc() default ; }Target限定只能标在方法上防止有人把注解放在类上造成误用。Retention必须设成RUNTIME因为切面在运行时要通过反射拿到注解对象。日志记录对象public class AuditRecord { private String type; private String desc; private String method; private Object[] args; private long costMs; // 省略 getter/setter 和 toString }日志输出接口和默认实现public interface AuditLogger { void record(AuditRecord record); } public class ConsoleAuditLogger implements AuditLogger { Override public void record(AuditRecord record) { System.out.println([audit-log] record); } }接口的意义是让业务方可以替换实现。比如后续接数据库、接消息队列只需要在业务工程里声明一个实现了AuditLogger的 Bean自动配置里的默认控制台实现就会自动让位。3.4 切面实现与耗时统计Aspect public class AuditLogAspect { private final AuditLogger auditLogger; private final AuditLogProperties properties; public AuditLogAspect(AuditLogger auditLogger, AuditLogProperties properties) { this.auditLogger auditLogger; this.properties properties; } Around(annotation(auditLog)) public Object record(ProceedingJoinPoint joinPoint, AuditLog auditLog) throws Throwable { long start System.currentTimeMillis(); try { return joinPoint.proceed(); } finally { long costMs System.currentTimeMillis() - start; auditLogger.record(buildRecord(joinPoint, auditLog, costMs)); } } private AuditRecord buildRecord(ProceedingJoinPoint joinPoint, AuditLog auditLog, long costMs) { AuditRecord record new AuditRecord(); record.setType(auditLog.type()); record.setDesc(auditLog.desc()); record.setMethod(joinPoint.getSignature().toLongString()); record.setArgs(joinPoint.getArgs()); record.setCostMs(costMs); return record; } }切面表达式annotation(auditLog)的意思是匹配所有带AuditLog注解的方法同时把注解对象绑定到切面方法的参数auditLog上。这样不需要额外写 pointcut 方法注解里的元信息直接能用。耗时统计放在finally块里是为了保证业务方法抛异常时也能记录日志。这里不吞异常joinPoint.proceed()原样往上抛业务行为不受影响。如果你只想记录成功调用把finally改成正常返回路径即可但我实测下来记录异常场景更有价值。3.5 配置属性类与自动配置类配置属性类ConfigurationProperties(prefix audit.log) public class AuditLogProperties { private boolean enabled true; private String operatorHeader X-User-Id; }ConfigurationProperties负责把application.yml里audit.log前缀配置绑定到这个类。绑定成功的关键是属性名必须和配置文件里的 kebab-case 对应比如 Java 里的operatorHeader对应配置里的operator-header。自动配置类Configuration(proxyBeanMethods false) EnableConfigurationProperties(AuditLogProperties.class) public class AuditLogAutoConfiguration { Bean ConditionalOnMissingBean(AuditLogger.class) ConditionalOnProperty(prefix audit.log, name enabled, havingValue true, matchIfMissing true) public AuditLogger auditLogger() { return new ConsoleAuditLogger(); } Configuration(proxyBeanMethods false) ConditionalOnClass(name org.aspectj.lang.annotation.Aspect) public static class AuditLogAspectConfiguration { Bean ConditionalOnMissingBean ConditionalOnProperty(prefix audit.log, name enabled, havingValue true, matchIfMissing true) public AuditLogAspect auditLogAspect(AuditLogger auditLogger, AuditLogProperties properties) { return new AuditLogAspect(auditLogger, properties); } } }这里有几个细节要展开讲。Configuration(proxyBeanMethods false)表示不生成 CGLIB 代理。这个优化对普通Bean方法足够用能减少启动阶段的开销。如果你在配置类里用Bean方法互相调用并想拿同一实例那不能关但我们这种场景没有关掉更干净。ConditionalOnMissingBean(AuditLogger.class)放在默认日志实现的Bean方法上意思是容器里已经有AuditLogger类型的 Bean 时默认控制台实现就不注册。这样业务方自定义实现不会和默认实现打架。切面配置单独抽成一个静态内部配置类套上ConditionalOnClass(name org.aspectj.lang.annotation.Aspect)。为什么用name而不是直接写Aspect.class因为如果业务工程没引 AOP用value Aspect.class时容器在解析条件注解时尝试加载该类反而会先把 ClassNotFound 抛出来。用字符串形式的全限定名只做类名判断不会主动触发类加载这是规避依赖缺失的一种标准做法。ConditionalOnProperty配合matchIfMissing true表示配置里没写audit.log.enabled时默认当作开启写false就能整体关掉。这是给业务方的一个开关。3.6 注册文件与安装验证在src/main/resources/META-INF/spring/目录下新建一个文件名字必须是org.springframework.boot.autoconfigure.AutoConfiguration.imports内容写一行com.example.auditlog.AuditLogAutoConfiguration文件路径、文件名都不能改这是 Boot 扫描候选配置类时约定的固定资源位置。接下来在工程根目录执行mvn clean install然后新建一个 demo 应用引入这个依赖dependency groupIdcom.example/groupId artifactIdaudit-log-spring-boot-starter/artifactId version1.0.0/version /dependency业务代码只需要加注解RestController public class OrderController { AuditLog(type order, desc 创建订单) PostMapping(/orders) public String create(RequestBody String request) { return created; } }启动应用调用接口后控制台会出现类似输出[audit-log] AuditRecord{typeorder, desc创建订单, methodcom.example...OrderController.create(..), args[...], costMs23}到这里一个可用的 starter 就跑通了。整个过程没有任何Import、手动new的痕迹业务方引入依赖、加注解、完事。4. 实战复盘配置不生效、装配冲突的排查实录4.1 自动配置类根本没有被加载我在实际使用中踩过不少坑第一个就是加了依赖但完全没动静。这时先别急着怀疑代码启动时加--debug看自动配置报告。如果报告里连AuditLogAutoConfiguration都找不到说明自动配置类压根没进候选列表。常见原因有三个AutoConfiguration.imports文件名或路径写错注意目录层级是META-INF/spring/不是META-INF/类全限定名和文件里写得不一致本地依赖没mvn installdemo 工程引用的是旧 jar。我遇到过最隐蔽的一次是改了 imports 文件但没重新执行 installdemo 工程里的依赖还是旧版本。排查到最后发现target/classes下面的 imports 文件根本没更新。这种问题要靠看实际 jar 里的文件来判断别只看源码。4.2 条件注解的类加载坑第二个常踩的坑是条件注解本身引发的 ClassNotFound。比如我一开始在切面配置上写的是ConditionalOnClass(Aspect.class)在业务工程没引 AOP 的情况下容器解析这个条件时会尝试把Aspect这个类型加载进来。因为 classpath 里没有这个类解析直接失败启动报错。这看起来很讽刺本来是想没有 AOP 就不加载切面结果因为条件判断方式不对反而把项目搞崩了。正确的写法就是用name属性ConditionalOnClass(name org.aspectj.lang.annotation.Aspect)字符串判断只查类名不触发类加载安全得多。这也是为什么很多框架在内部做条件判断时会把判断类单独抽到一个不受依赖影响的地方。4.3 用户覆盖不了默认实现第三种典型问题是我明明自定义了AuditLogger默认控制台实现还是生效。这通常是装配顺序造成的。自动配置类一般处理得比较靠后用户在普通配置类里声明 Bean 会优先被看到ConditionalOnMissingBean也就能正确判断。但如果业务方把自定义 Bean 放在了会被自动配置类覆盖顺序的位置或者自定义 Bean 和默认 Bean 的方法名都不一致就可能出现两个 Bean 并存的情况。解决思路有两个方向自动配置侧尽量用ConditionalOnMissingBean(类型)而不是按 Bean 名字判断业务方自定义实现时声明成Bean方法而不是依赖类路径扫描顺序更可控。还有一个相关的隐藏问题如果业务工程的ComponentScan恰好扫到了com.example.auditlog包自动配置类会被当成普通配置提前加载条件注解的行为就不可控了。所以 starter 的包名要尽量设计成业务方不容易扫描到的独立命名空间比如com.xxx.auditlog.core。这也是大组件的常见做法。4.4 配置属性全部是默认值第四个问题集中在配置绑定上。业务方在application.yml里写了audit: log: output: file结果代码里读到output还是console。这八成是 prefix 或者属性名对不上。ConfigurationProperties(prefix audit.log)对应的是audit.log前缀写成audit.log没错但属性名operatorHeader对应的是operator-header如果你写operatorHeader部分版本也能转但最好统一用 kebab-case。另一个很实际的排查手段在工程里加spring-boot-configuration-processor依赖这个依赖会在编译期生成配置元数据IDE 在application.yml里写配置时会有自动提示。如果业务方代码里没出现你要的属性提示大概率就是拼写或者前缀不对。4.5 常见问题速查表现象可能原因排查方法引入依赖后无任何日志自动配置类未加载启动加--debug看自动配置报告里有没有该配置类自动配置类不在报告中imports 文件路径/文件名错误检查META-INF/spring/下文件名和内容启动报ClassNotFound条件注解用了value Class形式改用ConditionalOnClass(name ...)用户自定义实现不生效装配顺序或 Bean 名字冲突确认是否被ComponentScan误扫改用ConditionalOnMissingBean(类型)配置属性读不到prefix、属性名、setter 问题加配置处理器用 IDE 提示验证业务方要求同时兼容 2.x 和 3.x注册文件版本不兼容同时保留spring.factories和 imports 文件5. 这个套路还能玩出什么花样5.1 用条件注解做智能降级明白了条件注解以后可以做出很优雅的降级策略。比如给同一套 starter 提供两个输出实现classpath 里有KafkaTemplate就注册 Kafka 实现的AuditLogger没有就退回控制台实现。这只要在自动配置里加两个Bean方法各自套ConditionalOnClass再用ConditionalOnMissingBean保证用户自定义优先级最高。这种能高级就高级不能就保底的设计正是 Spring Boot 内部众多自动配置类的通用思路。5.2 配置元数据、健康检查与优雅失败想让 starter 有更好的工程体验可以继续做三件事加spring-boot-configuration-processor生成配置元数据业务方在 IDE 里写配置时有自动补全减少拼写错误实现一个HealthIndicator把自己的状态挂到 actuator 的/actuator/health上方便运维侧看到组件是正常还是降级实现FailureAnalyzer在启动失败时输出一段针对性的中文提示告诉业务方缺了某个依赖、加哪个坐标而不是抛一坨堆栈让人猜。这些都不是必须的但对一个要给别人用的 starter 来说体验差距很大。5.3 最后补一点我自己的使用体会写这个 starter 之前我觉得自动配置像魔术写完之后我发现它更像一套按条件装配的规则引擎只要你把条件写清楚、顺序排对Spring 会按部就班地把 Bean 组装好。我个人实际项目里最常用到的还是那份--debug报告排查别人写的 starter 时先看 Positive 和 Negative问题基本一目了然。如果你也要在团队里沉淀公共组件建议从最小的注解切面条件装配开始先把链路跑通再逐步加健康检查、配置提示这些外围能力。越是看起来自动的东西越值得亲手把它拆开做一遍。
阅读完成 · 觉得有帮助?
咨询建站