ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Spring Boot自动配置原理:从@SpringBootApplication到条件装配

Spring Boot自动配置原理:从@SpringBootApplication到条件装配 1. 一个注解如何撬动整个Spring Boot先搞懂组合注解的关系在Spring Boot项目里启动类上那一行SpringBootApplication大概是所有Java开发者最熟悉又最陌生的注解。说熟悉是因为每个项目都要写说陌生是因为绝大多数人连它到底做了什么、为什么一个注解就能把整个应用自动跑起来都讲不清楚。先说结论SpringBootApplication本身不干活它是一个组合注解。把它拆开来看你会发现它真正的作用是召集了另外三个注解——SpringBootConfiguration、EnableAutoConfiguration、ComponentScan。这就好比一个人同时挂了三个头衔每个头衔背后都有一整套完整的处理逻辑。而其中最关键、也最复杂的是EnableAutoConfiguration也就是自动配置的入口。1.1 先看源码SpringBootApplication的本体打开Spring Boot源码SpringBootApplication在org.springframework.boot.autoconfigure包下它的定义长这样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 { // ... 部分属性的定义 }这里有两个容易忽略的细节。第一个是ComponentScan上带了一组excludeFilters排除了两个特殊的过滤器。TypeExcludeFilter是为了让开发者能通过自定义TypeExcludeFilter实现来排除某些类被扫描到AutoConfigurationExcludeFilter则负责排除自动配置类。为什么要排除自动配置类因为自动配置类通常在META-INF的配置文件里被注册它们默认是被ComponentScan扫描范围内的很多自动配置类都放在org.springframework.boot.autoconfigure包下如果不排除这些配置类会被重复处理导致意料之外的Bean加载。第二个细节是Inherited。这个注解表示SpringBootApplication可以被继承但实际上Java的Inherited只对类继承生效对接口实现不生效。所以如果你写了自定义注解去代理SpringBootApplication是可行的但如果你在一个接口上标注了它那实现类是不会获取到这个注解的。1.2 SpringBootConfiguration其实它就是Configuration接着看SpringBootConfiguration的定义Target(ElementType.TYPE) Retention(RetentionPolicy.RUNTIME) Documented Configuration public interface SpringBootConfiguration { }没错它只是Configuration的马甲版。也就是说启动类本身就是一个配置类这也是为什么我们可以直接在启动类上写Bean方法——因为Spring容器在启动时会把它当作配置类来处理其中的Bean方法会被拦截并纳入容器管理。1.3 ComponentScan扫描的边界问题ComponentScan没有额外属性时默认扫描启动类所在包及其子包下的所有Component。这一点很容易踩坑如果你的启动类放在com.example.demo而某些Bean放在com.example.other那这些Bean默认扫描不到Spring容器启动后就会报NoSuchBeanDefinitionException。所以一个常见的规范是启动类一定要放在所有业务代码包的顶层。现在三个注解都清楚了但真正核心的EnableAutoConfiguration还没展开。接下来我们要深入它的内部看它究竟如何做到自动。2. EnableAutoConfiguration的核心机密AutoConfigurationImportSelector的完整走读EnableAutoConfiguration这个注解本身只有一行有效代码Target(ElementType.TYPE) Retention(RetentionPolicy.RUNTIME) Documented Inherited AutoConfigurationPackage Import(AutoConfigurationImportSelector.class) public interface EnableAutoConfiguration { String ENABLED_OVERRIDE_PROPERTY spring.boot.enableautoconfiguration; Class?[] exclude() default {}; String[] excludeName() default {}; }它干了两件事第一通过AutoConfigurationPackage向容器注册启动类所在的包第二通过Import引入AutoConfigurationImportSelector这个类才是自动配置的真正总指挥。2.1 什么是DeferredImportSelector为什么必须延迟导入AutoConfigurationImportSelector不是普通的ImportSelector它实现了DeferredImportSelector接口。这个Deferred延迟非常关键。普通的ImportSelector会在当前配置类解析时立刻执行selectImports并导入选中的类而DeferredImportSelector要等到所有Configuration类都处理完之后再执行。换句话说自动配置类的导入被推迟到了最后。为什么要这样设计因为自动配置类大量依赖条件装配比如ConditionalOnMissingBean如果它导入得太早开发者自己定义的Bean还没来得及注册那很多自动配置类就会因为发现已有Bean而不会生效或者反过来误判缺失Bean而错误地创建重复的Bean。延迟导入给了开发者配置和业务Bean先注册的机会自动配置类再根据实际情况做决策这是一个非常精巧的设计。2.2 源码走读从selectImports到getCandidateConfigurationsAutoConfigurationImportSelector的执行入口是selectImports方法但实际逻辑在getAutoConfigurationEntry里。我摘录核心代码省略掉一些校验逻辑Override public String[] selectImports(AnnotationMetadata annotationMetadata) { if (!isEnabled(annotationMetadata)) { return NO_IMPORTS; } AutoConfigurationMetadata autoConfigurationMetadata AutoConfigurationMetadataLoader .loadMetadata(annotationMetadata.getClassLoader()); AnnotationAttributes attributes getAttributes(annotationMetadata); ListString configurations getCandidateConfigurations(annotationMetadata, attributes); configurations removeDuplicates(configurations); SetString exclusions getExclusions(annotationMetadata, attributes); checkExcludedClasses(configurations, exclusions); configurations.removeAll(exclusions); configurations filter(configurations, autoConfigurationMetadata); fireAutoConfigurationImportEvents(configurations, exclusions); return StringUtils.toStringArray(configurations); }流程可以拆解成五步loadMetadata从classpath加载META-INF/spring-autoconfigure-metadata.properties这个文件记录了自动配置类的各种元数据如条件注解、排序顺序等用于后续的快速过滤。getCandidateConfigurations获取所有候选自动配置类的类名。removeDuplicates去重然后根据exclude/excludeName排除指定类。filter利用元数据对所有候选类做最终过滤去掉不满足条件的配置类。fireAutoConfigurationImportEvents发布事件用户可以实现AutoConfigurationImportListener监听这些事件做一些统计或调试工作。2.3 配置文件的双重加载机制spring.factories和AutoConfiguration.importsgetCandidateConfigurations的源码揭示了自动配置类从哪里来protected ListString getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) { ListString configurations SpringFactoriesLoader.loadFactoryNames( getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader()); Assert.notEmpty(configurations, No auto configuration classes found in META-INF/spring.factories. If you are using a custom packaging, make sure that file is correct.); return configurations; }在Spring Boot 2.7之前自动配置类清单全部来自META-INF/spring.factories文件里的org.springframework.boot.autoconfigure.EnableAutoConfiguration键。而从Spring Boot 2.7开始官方引入了一个新的注册文件META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.importsspring.factories里只保留了一小部分兼容项。Spring Boot 3.0之后spring.factories里已经不再支持自动配置类的注册了统一走AutoConfiguration.imports。我在项目里看过不少老源代码很多人的自定义starter还在spring.factories里写EnableAutoConfiguration如果升级到Spring Boot 3.x这些配置会静默失效排查起来非常痛苦。所以如果你现在新建starter直接使用AutoConfiguration.imports每行写一个自动配置类的全限定名com.example.autoconfigure.MyAutoConfiguration com.example.autoconfigure.AnotherAutoConfiguration2.4 自动配置是如何排名的AutoConfiguration.imports支持通过AutoConfigureBefore、AutoConfigureAfter、AutoConfigureOrder来调整自动配置类之间的先后顺序。这三者处理时机不同AutoConfigureOrder控制的是整个自动配置类的加载顺序数值越小越先加载。AutoConfigureBefore和AutoConfigureAfter则用于描述配置类之间的直接依赖关系Spring Boot会在排序阶段解析这些关系。举个例子数据源相关的自动配置类上通常会有类似AutoConfigureBefore(DataSourceAutoConfiguration.class)的标注表示要在某个核心配置之前执行确保连接池、事务管理等组件按正确顺序就绪。排序是由AutoConfigurationSorter类在内部完成的它会把类名按依赖关系构建成一张DAG再拓扑排序。如果出现了循环依赖Spring Boot会忽略相关顺序声明并打印一条warn日志。3. 条件装配机制自动配置真正懂事的关键光有一堆自动配置类还不够如果一个Web项目把所有候选配置类全部生效那启动过程会创建大量无用的Bean内存和启动速度都扛不住。Spring Boot解决这个问题的核心是条件装配Conditional。3.1 常用的条件注解家族这部分是自动配置原理中最容易囫囵吞枣的地方我挑几个最常用的展开说。ConditionalOnClass/ConditionalOnMissingClass判断classpath中是否存在某个类。这是自动配置最常用的条件。比如RedisAutoConfiguration上标注了ConditionalOnClass(RedisOperations.class)只有当类路径里存在RedisOperations这个类时Redis的自动配置才会参与后续处理。ConditionalOnBean/ConditionalOnMissingBean判断容器中是否已经注册了某个Bean。这是自动配置让位给用户自定义Bean的关键。ConditionalOnProperty判断配置项是否存在且是否等于某个值。很多功能的开关都靠它比如ConditionalOnProperty(name spring.redis.enabled, havingValue true)。ConditionalOnWebApplication判断当前应用是不是Web应用分为SERVLET、REACTIVE、ANY三种类型。ConditionalOnExpression基于SpEL表达式判断。3.2 条件注解的执行顺序为什么要分配置条件和Bean条件条件注解并不是在所有阶段都会执行。Spring Boot内部把条件分成两类一类是配置类级别的条件一类是Bean方法级别的条件。配置类级别的条件在配置类解析阶段就会判断如果不满足这个配置类的所有Bean方法都不会被处理。Bean方法级别的条件则要等到Bean方法真正执行时才判断。这里有个经典坑ConditionalOnBean放在自动配置类级别是不可靠的。因为自动配置类本身是延迟导入的而当容器开始处理这些配置类时很多用户定义的Bean还没有注册完成ConditionalOnBean可能会误判为没有这个Bean。Spring Boot官方文档也明确指出ConditionalOnBean和ConditionalOnMissingBean应该放在Bean方法上而不是放在配置类级别。打个比方条件注解的执行顺序就像面试配置类级别的条件相当于简历初筛Bean方法级别相当于现场技能测试。你可以在简历上写精通XX但如果现场不会做题照样淘汰。ConditionalOnBean放在类级别相当于只看简历就下结论很容易翻车。3.3 数据源自动配置的条件分析一个完整的案例以最容易理解的DataSourceAutoConfiguration为例看条件注解如何组合。AutoConfiguration(before SqlInitializationAutoConfiguration.class) ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class }) ConditionalOnMissingBean(type io.r2dbc.spi.ConnectionFactory) EnableConfigurationProperties(DataSourceProperties.class) Import({ DataSourcePoolMetadataProvidersConfiguration.class, DataSourceCheckpointRestoreConfiguration.class }) public class DataSourceAutoConfiguration { // ... }这个配置类的生效条件有三个classpath中同时存在DataSource和EmbeddedDatabaseType类也就是说至少有一个数据源相关的依赖。容器中不存在ConnectionFactory类型的Bean避免和响应式数据源冲突。通过EnableConfigurationProperties绑定以spring.datasource开头的配置属性到DataSourceProperties。再往下走连接池的自动装配是通过DataSourceConfiguration的内部类实现的Configuration(proxyBeanMethods false) ConditionalOnProperty(prefix spring.datasource, name type) static class Generic { Bean DataSource dataSource(DataSourceProperties properties) { return properties.initializeDataSourceBuilder().build(); } } Configuration(proxyBeanMethods false) ConditionalOnClass(HikariDataSource.class) ConditionalOnMissingBean(DataSource.class) ConditionalOnProperty(name spring.datasource.type, havingValue com.zaxxer.hikari.HikariDataSource, matchIfMissing true) static class Hikari { Bean DataSource dataSource(DataSourceProperties properties) { HikariDataSource dataSource createDataSource(properties, HikariDataSource.class); // ... return dataSource; } }这里最值得注意的是ConditionalOnClass(HikariDataSource.class)配合matchIfMissing true意味着只要classpath中有HikariCP且没有显示指定其他连接池Spring Boot就会默认使用HikariCP。这解释了为什么很多项目只是引入了spring-boot-starter-jdbc就自动用上了Hikari连接池——一切都是条件判断的结果。3.4 配置属性的绑定机制ConfigurationProperties在自动配置中的角色自动配置类普遍会和ConfigurationProperties配合把application.yml里的配置项映射成类型安全的属性对象。还是以数据源为例DataSourceProperties上有ConfigurationProperties(prefix spring.datasource)所以配置spring.datasource.url、spring.datasource.username会自动绑定到这个类的对应字段。EnableConfigurationProperties注解确保了属性类被注册为Bean然后Bean方法可以通过方法参数自动注入这些属性对象。这里有一个值得养成的习惯当你想查看一个自动配置类支持哪些配置项时直接去看它绑定的Properties类源码即可这是最准确的文档。4. 自定义一个属于自己的自动配置starter含完整的实战代码理解了原理之后最好的验证方式是自己动手写一个自动配置starter。考虑到很多人问过Spring Boot MyBatis实现数据库字段级加密后怎么做查询这类问题我以一个简化版的字段加密自动配置作为示例把前面提到的知识点全部串起来。4.1 需求场景假设我们有一个Spring Boot项目希望给某些敏感字段如手机号、身份证号自动加密存储。我们的starter需要做到引入后自动注册一个FieldCipher的Bean提供encrypt和decrypt方法。默认使用AES算法密钥通过配置my.security.cipher.key指定。如果用户已经自己定义了FieldCipher我们的自动配置不再生效。4.2 创建starter模块新建一个Maven模块命名为field-cipher-spring-boot-starter。注意这个模块不需要包含业务代码只放依赖声明真正的自动配置类放在一个独立的field-cipher-spring-boot-autoconfigure模块中。这是我的个人习惯分工清晰也方便以后拆发布。在autoconfigure模块中先定义一个属性绑定类ConfigurationProperties(prefix my.security.cipher) public class CipherProperties { private String key default-key-123456; private String algorithm AES; public String getKey() { return key; } public void setKey(String key) { this.key key; } public String getAlgorithm() { return algorithm; } public void setAlgorithm(String algorithm) { this.algorithm algorithm; } }注意默认值。一个自动配置类如果没有提供合理的默认值用户在引入依赖后什么都不配置就会启动报错。默认值的设置建议遵循保守但可用的原则——这里给了一个demo级别的默认密钥生产环境务必覆盖。4.3 写自动配置类AutoConfiguration EnableConfigurationProperties(CipherProperties.class) ConditionalOnClass(Cipher.class) public class FieldCipherAutoConfiguration { Bean ConditionalOnMissingBean public FieldCipher fieldCipher(CipherProperties properties) throws Exception { return new FieldCipher(properties.getKey(), properties.getAlgorithm()); } }这段代码里有几个关键点AutoConfiguration是Spring Boot 2.7之后推荐使用的注解底层组合了Configuration并标记了这是一个自动配置类。ConditionalOnClass(Cipher.class)只有classpath中存在javax.crypto.Cipher时才生效这个条件是几乎永远成立的但它展示了条件注解的写法。ConditionalOnMissingBean如果用户自定义了FieldCipher则自动配置创建的Bean会被忽略。这是自动配置让位规则的经典表达。方法参数CipherProperties properties会从容器中获取绑定了配置属性的Bean因为用了EnableConfigurationProperties注册。4.4 注册自动配置类在src/main/resources/META-INF/spring/目录下创建文件org.springframework.boot.autoconfigure.AutoConfiguration.imports内容为com.example.cipher.autoconfigure.FieldCipherAutoConfiguration注意这个文件名很特殊是接口的全限定名路径是META-INF/spring。Spring Boot 2.7之前的写法是放在META-INF/spring.factories里org.springframework.boot.autoconfigure.EnableAutoConfiguration\ com.example.cipher.autoconfigure.FieldCipherAutoConfiguration两种格式我都经历过Spring Boot 3.x开始只认新的.imports格式。4.5 实测结果与踩坑记录在测试工程中引入这个starter后启动日志里如果开启了debugtrue你会看到类似这样的内容FieldCipherAutoConfiguration matched: - ConditionalOnClass found required class javax.crypto.Cipher; ConditionalOnMissingBean does not find bean type com.example.cipher.FieldCipher (OnBeanCondition)这说明自动配置的条件判断通过了Bean被创建。如果用户自己定义了一个FieldCipher日志会变成FieldCipherAutoConfiguration: Did not match: - ConditionalOnMissingBean found bean customFieldCipher (OnBeanCondition)这里的found bean意味着容器已经存在用户自定义的Bean所以自动配置静默退出。这种日志排查方式是理解自动配置行为的第一手资料。踩过的坑也一并说出来如果你在FieldCipher类上标注了Component那它会被ComponentScan扫到再配合自动配置里的ConditionalOnMissingBean就会导致自动配置永远不生效因为Bean已经被扫描注册了。所以自定义的Bean要确保不要被包扫描误注册否则条件判断会失效。5. 改了为什么没生效自动配置失效的排查链路与实战复盘很多开发者在实际工作中遇到的问题是配置写好了、starter也引了但自动配置就是没生效。这种问题往往比如何实现功能更让人头疼因为Spring Boot的自动配置过程在默认情况下像一个黑盒。5.1 第一板斧开启debug模式查看条件评估报告在application.yml中加一行debug: true启动后控制台会打印出一个超大的CONDITIONS EVALUATION REPORT分为两大部分Positive matches条件匹配生效的自动配置和Negative matches条件未匹配的自动配置。Negative matches里会明确告诉你每个自动配置类没有匹配的原因例如RedisAutoConfiguration: Did not match: - ConditionalOnClass did not find required class org.springframework.data.redis.core.RedisOperations这就直说了classpath里根本没有Redis相关类所以Redis的自动配置直接跳过。看到这种日志你应该先检查依赖有没有引入。Negative matches是我排查问题时的第一站。它把你的猜测范围从几十个自动配置类快速缩小到两三个候选。5.2 第二板斧查看AutoConfigurationImportEvent如果你引入了自定义starter且条件报告中完全没有出现你的自动配置类那问题大概率出在注册环节。重点检查三个地方.imports文件是否真的放在了classpath的META-INF/spring目录下。自动配置类的全限定名是否和.imports里写的完全一致大小写、包名都不能错。有没有被spring.autoconfigure.exclude排除掉。一个很典型的错误是多人开发时有人在自己模块里写了spring.autoconfigure.excludecom.example.*导致某个范围内的自动配置类全部被排除。这种问题从条件报告里是看不出来的因为被主动排除的类不会进入Negative matches而是显示为Exclusions。所以在排查时要习惯性地看一眼报告最开头的Exclusions部分。5.3 第三板斧ConditionEvaluationReport的编程式获取有时候日志太长或者你想在测试中动态断言某个自动配置是否生效可以通过ConditionEvaluationReport来获得结构化的评估结果Autowired ApplicationContext context; Test void checkAutoConfiguration() { ConditionEvaluationReport report context.getBean(ConditionEvaluationReport.class); report.getConditionAndOutcomesBySource().forEach((source, outcomes) - { outcomes.forEach(outcome - { System.out.println(source - outcome.getOutcome() : outcome.getMessage()); }); }); }这比肉眼翻日志高效得多也适合写进自动化测试里作为回归断言。5.4 一个实际案例设了静态IP但仍然被自动配置覆盖标题相关的热搜词里恰好出现设了静态ip但还是有自动配置这类问题在配置管理里很常见。虽然这通常不是Spring Boot自动配置的问题更多是操作系统网络管理服务与静态IP配置的冲突但它的排查思路和Spring Boot自动配置的原理完全一致——系统里同时存在手动配置和自动配置两套机制自动机制总是先扫描一遍如果没有显式的覆盖条件就会用自己的默认值。套用到Spring Boot里等价的场景是你在配置文件里明明写了某个Bean的配置结果启动后却发现自动配置创建了一个默认值Bean你自己的配置好像没起作用。这种情况十有八九是配置绑定前缀写错了。比如DataSourceProperties绑定的是spring.datasource前缀你写成了datasource.url那Spring Boot根本读取不到自动配置就会使用默认值。排查方法很简单在配置类里加一个ConfigurationProperties的测试Bean或者用Environment接口打印一下实际绑定的值。5.5 避免失效的几个日常习惯根据我自己的经验以下习惯能显著减少自动配置失效类问题的发生率优先级理解显式配置Bean 自动配置Bean。想覆盖自动配置时优先用自定义Bean而不是试图去改自动配置类的源码。排除策略能用exclude属性排除就尽量不要用spring.autoconfigure.exclude。因为前者是注解级别的局部作用后者是全局的很容易误伤。命名规范自定义自动配置类建议以AutoConfiguration结尾这不仅是风格问题Spring Boot的AutoConfigurationSorter在排序时会根据类名做启发式处理。最小依赖starter模块不要引入任何业务组件的依赖只放自动配置逻辑。业务依赖应该由使用方自行引入否则条件注解的判断会被你的starter的传递依赖干扰。6. 从源码到实战个人回顾与一些调试技巧回到最初的问题理解了SpringBootApplication和自动配置原理之后对你的日常开发到底有什么实际帮助我的体会是最大的帮助不是能看懂源码这件事本身而是在遇到诡异问题的时候你的排查思路会变得有章法。以前遇到为什么这个配置没生效只能瞎试改配置、清缓存、重启运气好解决了但根本原因不清楚。现在遇到问题我会自动按照条件报告 → 排除列表 → 注册文件 → 排序关系这条链路走一遍大部分问题都能在十分钟内定位。这种判断问题的能力正是从源码阅读中获得的。另外分享一个调试小技巧在Spring Boot 2.4以上的版本中可以直接在启动参数里加-Ddebug效果和配置文件里debug: true一样。如果你是IDEA用户还可以在启动类的main方法里临时加一段代码打印BeanDefinition的源码来源ConfigurableListableBeanFactory beanFactory context.getBeanFactory(); BeanDefinition bd beanFactory.getBeanDefinition(dataSource); System.out.println(bd.getResourceDescription());通过getResourceDescription()可以看到dataSource这个Bean是在哪个配置文件或哪个Bean方法中定义的。当你不确定某个Bean到底是自动配置创建的、还是自己代码创建的时候这个方法特别管用。最后一个想说的点是版本差异。Spring Boot 2.7、2.6、3.0之间的自动配置实现有不少变化比如spring.factories与.imports的迁移、AutoConfiguration注解的引入、proxyBeanMethods的默认值调整。如果你在网上搜到一篇很老的自动配置源码分析文章对照着当前版本的源码去读会发现很多细节对不上这不是你理解错了而是版本演进导致的。我建议直接阅读当前Spring Boot版本对应的源码以官方文档为准别拿三年前的博客当成金科玉律。我自己是把Spring Boot的三个大版本都读过一遍每次升级后重新梳理一遍源码每次都有新的理解。
返回列表