ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Spring Boot自动装配与Condition机制:条件注解原理与实战

Spring Boot自动装配与Condition机制:条件注解原理与实战 1. 先搞清楚自动装配这件事在哪里发生我见过不少刚接触 Spring Boot 的同事天真地以为SpringBootApplication只是启动入口注解直到某天引入一个第三方 Starter功能莫名其妙就能用了才开始好奇背后的原理。这里面的关键一环就是自动装配配合 Condition 机制在做判断。先说一个最简单的场景你的项目里引入了spring-boot-starter-data-redisSpring Boot 启动的时候会自动把RedisTemplate、LettuceConnectionFactory这些 Bean 创建出来。但如果你没引这个依赖这些 Bean 也不会创建项目也不会报错。名字叫自动装配但事实上它一点都不盲目——它在装配之前会做一道又一道的检查而这套检查机制的载体就是Conditional及其衍生注解。这篇文章就围绕Spring Boot 自动装配 Condition这个话题展开适合三类人看第一写业务代码但想理解 Spring Boot 底层逻辑的开发者第二打算自己封装 Starter 或做组件复用的中间件开发者第三遇到为什么我加了依赖却没生效 / 为什么没加依赖反而报错这类问题时需要排查思路的人。我会从自动装配的入口讲起再看 Condition 家族的实现原理然后手写一个带条件判断的自动配置最后分享几个实战中高频踩坑的排错姿势。2. 自动装配的真正入口不看不知道一看全是套路很多人以为自动装配是扫描主类所在包下面的所有配置类这理解其实只对了一半。2.1 EnableAutoConfiguration 是怎么被激活的SpringBootApplication是一个组合注解里面包含SpringBootConfiguration、ComponentScan和EnableAutoConfiguration。真正启动自动装配的就是这三个中的最后一个。EnableAutoConfiguration注解上用Import(AutoConfigurationImportSelector.class)引入了一个ImportSelector实现。AutoConfigurationImportSelector做的事情可以简单概括为把候选的自动配置类全部找出来然后交给 Spring 容器按条件筛选、按顺序注册。Candidate 从哪来在 Spring Boot 2.7 之前所有自动配置类的全限定名都写在META-INF/spring.factories文件里key 是org.springframework.boot.autoconfigure.EnableAutoConfiguration。从 2.7 开始Spring Boot 引入了新的注册文件META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports每个类名占一行。到了 Spring Boot 2.7 的AutoConfigurationImportSelector里两者还是兼容的——它会先读新机制的 imports 文件再读旧的 spring.factories 文件合在一起去重后返回候选列表。2.2 候选配置类多不代表全部都会生效我手头有个 Spring Boot 3.2 的空项目仅引入 Web 和 ActuatorAutoConfigurationImportSelector加载出来的候选配置类有上百个包括DataSourceAutoConfiguration、RedisAutoConfiguration、MongoAutoConfiguration等。但最终容器里实际创建了多少相关 Bean很少。原因就是候选列表拿到之后还要过三关第一关排除exclude。SpringBootApplication(exclude DataSourceAutoConfiguration.class)或在配置文件中用spring.autoconfigure.exclude能把指定类直接剔除。第二关过滤filter。Spring Boot 内部有一组AutoConfigurationImportFilter典型代表是OnClassCondition。它会分析候选配置类上的ConditionalOnClass注解扫描当前应用的 ClassLoader 中是否存在指定类把明显不满足条件的配置类提前筛掉。第三关配置类解析阶段的 Condition 评估。即便前两关过了真正加载配置类定义时Spring 会再次评估类上和方法上的全部Conditional注解。如果评估结果为 false对应 Bean 仍然不会注册。这里有个非常容易误解的点很多人以为Condition只评估一次实际上在候选筛选阶段和 BeanDefinition 解析阶段都会参与两者粒度不一样。前者是粗筛后者是细判。我第一次追源码时发现ConditionEvaluator里有shouldSkip这类方法本质上是给ConfigurationClassParser用的容器判断某个配置类或Bean方法是否要跳过时都会调它。3. Condition 注解家族一张表看清判断依据Conditional是 Spring Framework 自带的注解Spring Boot 在它之上做了一批开箱即用的衍生注解。这批注解定义在spring-boot-autoconfigure模块的org.springframework.boot.autoconfigure.condition包下。注解判断依据对应 Condition 实现典型使用场景ConditionalOnClassclasspath 中是否存在指定类OnClassCondition有某个 SDK 依赖才启用对应能力ConditionalOnMissingClassclasspath 中是否不存在指定类OnClassCondition类缺失时走降级逻辑ConditionalOnBean容器中是否已注册指定 BeanDefinitionOnBeanCondition用户已定义 Bean 时才组合ConditionalOnMissingBean容器中是否没有指定 BeanDefinitionOnBeanCondition提供默认 Bean 实现用户可覆盖ConditionalOnProperty配置项是否满足指定值OnPropertyCondition功能开关最常用ConditionalOnExpressionSpEL 表达式最终值是否为 trueOnExpressionCondition复杂组合条件ConditionalOnResource指定资源是否存在OnResourceCondition依赖某个资源文件才生效ConditionalOnWebApplication应用类型是否为 WebServlet/ReactiveOnWebApplicationConditionWeb 场景专用配置ConditionalOnJavaJVM 版本是否处于指定区间OnJavaCondition不同 Java 版本的差异化逻辑拿高频的ConditionalOnMissingBean举例。DataSource 自动配置里面DataSourceAutoConfiguration上标了ConditionalOnMissingBean(type io.r2dbc.spi.ConnectionFactory)。这就是给开发者留的口子——你只要自己声明一个DataSource自动配置的默认实现就会被跳过。这也是 Spring Boot 按需覆盖、默认兜底的设计哲学框架不强占你的选择但你没选的时候它会帮你选一个最合理的。这里我想强调一个容易看走的细节ConditionalOnBean/ConditionalOnMissingBean判断的是BeanDefinition 是否存在而不是运行期容器里有没有实例对象。因为自动配置类在容器刷新早期就会被解析Bean 实例可能还没创建完。如果你纯粹想判断运行期有没有某个实例ConditionalOnBean不一定可靠这也是官方文档明确提示过要慎用的原因。4. 手写一个自定义 Condition从接口到注解的完整链路4.1 实现 Condition 接口最小的判断逻辑Spring 的Condition接口只有两个方法matches(ConditionContext context, AnnotatedTypeMetadata metadata)和matches(ConditionContext context, AnnotatedTypeMetadata metadata, BeanDefinitionRegistry registry)后者是 Spring Framework 6.x 之后提供的变体带 BeanDefinitionRegistry 参数。自己写 Condition 最核心的就是实现matches返回 true 则继续注册返回 false 则跳过。来看一个实际场景我希望在配置文件里加一个功能开关custom.cache.enabledtrue并且 classpath 里存在com.github.benmanes.caffeine.cache.Cache这个类时才创建一个本地缓存 Bean。常规做法是直接用ConditionalOnProperty和ConditionalOnClass组合但为了演示自定义 Condition我写成这样public class CacheCondition implements Condition { Override public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { Environment environment context.getEnvironment(); String enabled environment.getProperty(custom.cache.enabled); boolean flag Boolean.parseBoolean(enabled); if (!flag) { return false; } ClassLoader classLoader context.getClassLoader(); if (classLoader null) { classLoader Thread.currentThread().getContextClassLoader(); } try { Class.forName(com.github.benmanes.caffeine.cache.Cache, false, classLoader); return true; } catch (ClassNotFoundException e) { return false; } } }ConditionContext能拿到的东西比想象中多getEnvironment()拿配置属性getClassLoader()拿类加载器getResourceLoader()拿资源加载器getRegistry()拿 BeanDefinitionRegistry。这意味着你的条件判断可以非常灵活——甚至能检查容器里是否已经存在某个 Bean 的名字前缀再决定装配策略。4.2 配合自定义注解把参数从代码里抽离出来直接在代码里写死属性名和类名扩展性差。更好的做法是定义一个带属性的注解再通过metadata读取注解属性。这里用Conditional引用自定义ConditionalOnCustomCacheTarget({ElementType.TYPE, ElementType.METHOD}) Retention(RetentionPolicy.RUNTIME) Conditional(CustomCacheCondition.class) public interface ConditionalOnCustomCache { String prefix() default custom.cache; boolean enabled() default true; String requiredClassName() default ; }Condition 实现里通过AnnotatedTypeMetadata拿到注解属性public class CustomCacheCondition implements Condition { Override public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { MultiValueMapString, Object attributes metadata .getAllAnnotationAttributes(ConditionalOnCustomCache.class.getName()); if (attributes null) { return false; } String prefix (String) attributes.getFirst(prefix); Boolean enabled (Boolean) attributes.getFirst(enabled); String requiredClassName (String) attributes.getFirst(requiredClassName); String property context.getEnvironment().getProperty(prefix .enabled); boolean propertyMatched property null ? enabled : Boolean.parseBoolean(property); if (!propertyMatched) { return false; } return loadClass(context.getClassLoader(), requiredClassName); } private boolean loadClass(ClassLoader classLoader, String className) { if (className null || className.isEmpty()) { return true; } try { Class.forName(className, false, classLoader); return true; } catch (ClassNotFoundException e) { return false; } } }用起来就变成了Configuration public class CacheAutoConfiguration { Bean ConditionalOnCustomCache(prefix custom.cache, requiredClassName com.github.benmanes.caffeine.cache.Cache) public CacheManager cacheManager() { return new CaffeineCacheManager(); } }这种做法最大的价值在于把是否装配的决策权交给条件而不是交给某个 if 判断。以后别人用你的组件时可以通过配置项或依赖组合来控制装配行为不用改你的代码。4.3 什么时候用 ConfigurationConditionConfigurationCondition是Condition的子接口多了一个getConfigurationPhase()方法返回PARSE_CONFIGURATION或REGISTER_BEAN。前者表示在配置类解析阶段BeanDefinition 还没注册评估后者表示在 BeanDefinition 注册阶段评估。默认的Condition接口在ConfigurationClassParser解析配置类时就执行了整体偏早期。如果你的条件要依赖容器中是否已注册某个 Bean那就必须考虑阶段问题——太早评估看到的 BeanDefinition 集合不完整判断结果可能和你预想的不一样。Spring Boot 内置的OnBeanCondition就实现了ConfigurationCondition把评估阶段放在了REGISTER_BEAN确保它能拿到尽量完整的候选 Bean 信息。5. 把条件塞进自动装配一个迷你 Starter 的实战过程讲了这么多接口和注解直接落到一个能跑的例子开发一个短信发送自动配置。需求是只有当配置文件中sms.enabledtrue且 classpath 中存在某个短信 SDK 的类时才注册SmsSender同时允许用户自定义SmsSender用户定义了就跳过默认实现。5.1 定义属性绑定类新建SmsProperties用ConfigurationProperties绑定sms前缀的配置ConfigurationProperties(prefix sms) public class SmsProperties { private boolean enabled false; private String accessKeyId ; private String accessKeySecret ; // getter / setter 省略 }5.2 核心自动配置类AutoConfiguration ConditionalOnClass(SmsSender.class) ConditionalOnProperty(prefix sms, name enabled, havingValue true) EnableConfigurationProperties(SmsProperties.class) public class SmsAutoConfiguration { Bean ConditionalOnMissingBean public SmsSender smsSender(SmsProperties properties) { return new DefaultSmsSender(properties.getAccessKeyId(), properties.getAccessKeySecret()); } }这里注意两点。第一我用了AutoConfiguration而不是Configuration。AutoConfiguration是 Spring Boot 2.7 之后推荐的标注配合自动配置导入机制更规范而且它会把自动配置类上的一些条件过滤逻辑纳入专门处理减少不必要的配置类解析。第二ConditionalOnProperty放在类级别ConditionalOnMissingBean放在方法级别组合使用效果最自然——类级别不满足条件时整个配置类直接跳过类级别满足但用户已有自定义 SmsSender 时只跳过默认 Bean。5.3 注册自动配置类在src/main/resources/META-INF/spring/目录下新建org.springframework.boot.autoconfigure.AutoConfiguration.imports内容一行com.example.sms.autoconfigure.SmsAutoConfiguration如果是 Spring Boot 2.7 之前的老项目则要写在META-INF/spring.factoriesorg.springframework.boot.autoconfigure.EnableAutoConfiguration\ com.example.sms.autoconfigure.SmsAutoConfiguration两种方式我都实际用过迁移期的项目往往两处都写了但要注意两边不要重复声明同一个类否则启动时会有重复过滤和排序的副作用。Spring Boot 3.x 已经完全移除 spring.factories 对自动配置的支持了新项目一律用 imports 文件。5.4 为什么要设计成组合条件而不是单一条件很多初级封装只用一个ConditionalOnProperty做开关这不够稳健。短信 SDK 的类有没有引入决定代码能不能编译层次上走到这个配置类而开关决定要不要启用。两者是且关系任何一个不满足都不能装配。其实做组件封装有个通用经验条件越具体装配行为越可预期。多条件组合还有一个好处排查问题时更清晰是依赖没引还是开关没开一眼就能分清。6. 自动装配阶段发生了什么源码层面的求值链路条件生效不是魔法我有段时间追到了ConfigurationClassParser把链路捋顺后发现逻辑非常清晰。启动时AutoConfigurationImportSelector从 imports 文件得到候选类列表。AutoConfigurationImportSelector内部会收集AutoConfigurationImportFilter其中OnClassCondition是默认注册的过滤器。这一步通过读取配置类注解中的类名元数据和当前 ClassLoader 中的实际类做匹配提前过滤掉不可能会通过的配置类。剩下的候选类变成配置类解析目标传入ConfigurationClassParser。解析到某个配置类时ConditionEvaluator会执行该类上所有Conditional注解的matches方法。如果类级条件全部通过再继续解析内部的Bean方法每个Bean方法上的Conditional也会单独评估。自动配置类的加载顺序由AutoConfigureOrder、AutoConfigureBefore、AutoConfigureAfter控制。这个顺序影响条件判断的视野——排在前面的配置类先被解析先注册 BeanDefinition后面的ConditionalOnMissingBean才有可能提前看到已有 BeanDefinition。我印象里最容易出问题的是第 3 步。ConditionEvaluator.shouldSkip()里面有一段逻辑判断当前注解属于哪种注册阶段如果条件在PARSE_CONFIGURATION阶段要求评估而当前阶段不匹配就会跳过该条件的计算。这也就是为什么后来引入了ConfigurationCondition去精细控制。真正定位到这一层后你会发现很多奇怪的问题其实都是阶段错位导致的。启动时还有一份非常有价值的报告——ConditionEvaluationReport。在配置文件里设debugtrue启动日志会打印Positive matches条件命中的自动配置和Negative matches条件未命中的自动配置。例如Negative matches: ----------------- SmsAutoConfiguration Did not match: - ConditionalOnProperty (sms.enabled) found different value in property sms.enabled这一手在排查为什么我的自动配置没生效时极其好用比瞎猜高效得多。如果是生产环境不便开 debug可以用 actuator 的/actuator/conditions端点查看同样的信息。7. 实战踩坑总结条件不生效多半是这些问题7.1 ConditionalOnClass 与类加载导致NoClassDefFoundErrorOnClassCondition的实现并不直接Class.forName加载类而是读取配置类注解上的类名做元数据匹配。但你的业务代码如果直接引用了那个缺失的类启动时仍会因类加载问题报错。比如一个自动配置类方法签名里写着CaffeineCacheManager参数classpath 没有 caffeine 依赖时即便条件判定不通过类加载时因为方法签名里的类型引用也可能会炸。解决办法是把这类方法的参数类型弱化成 Object 或通过反射创建或者干脆用ConditionalOnClass(name ...)以字符串方式指定类名尽量避免编译期依赖。7.2 多个自动配置类都用了 ConditionalOnMissingBean导致默认 Bean 冲突我自己就碰到过一次两个 Starter 都提供了默认的ObjectMapperBean都标注了ConditionalOnMissingBean。用户本以为只引入一个 Starter 会各自独立结果因为AutoConfigureOrder的先后顺序先加载的先注册了 Bean后加载的ConditionalOnMissingBean看到已有默认 Bean就不会再注册。但两个 Starter 的默认 Bean 语义并不一致最终容器里用的是先加载的那一个另一个组件的功能表现就不对。这个问题靠AutoConfigureBefore/AutoConfigureAfter可以缓解但根本思路是不要在多个自动配置里抢同一个 Bean 类型尽量让默认 Bean 的类型或名称唯一或者把默认实现收敛到单一配置类里统一提供。7.3 配置文件里的 Condition 相关属性没被绑定ConditionalOnProperty默认要求配置项存在且值为 true但如果属性类ConfigurationProperties和条件注解使用的 prefix 不一致写配置时很容易漏。我习惯在配置类里统一维护一份属性常量避免手写字符串出错。另外havingValue默认值是true如果你配置的是sms.enabledfalse条件会失效。有一种容易看错的情况是环境变量注入Spring Boot 会把环境变量SMS_ENABLEDfalse映射到sms.enabled此时Boolean.parseBoolean(false)结果符合预期但要小心环境变量名拼写和特殊字符。7.4 测试环境不加载自动配置类SpringBootTest基于主类启动时会扫描主类所在包及其子包如果你把自动配置类放在独立 jar 包里且 jar 包的AutoConfiguration.imports没有打进最终产物测试环境不会触发自动装配。这种问题通常表现为本地单独跑组件示例没问题一集成进其他项目就失效。排查时先确认jar tf看 imports 文件是否存在再开 debug 日志看候选配置类列表。有一个额外技巧测试类上可以手动Import(SmsAutoConfiguration.class)强制加载但这不是治本方案只适合临时验证条件逻辑。7.5 条件注解放在普通业务类上而不是配置类或 Bean 方法上Spring Boot 的ConditionalOnXxx设计目标就是服务自动配置放在Service、Component这类业务组件上也有效但不推荐。原因有三业务组件扫描阶段和自动配置类的加载阶段不同条件评估时 BeanDefinition 的可见范围不一样其次业务代码里塞一批条件注解会让团队排查问题时误以为每个组件都有独立开关维护负担很大。我的经验是只有两种位置放 Condition 最合适自动配置类类级别和 Bean 方法方法级别。8. 关于条件机制的一段个人使用心得绕了一圈回到开头那个点自动装配不靠运气Condition 就是它的安检员。我在实际项目里养成的习惯是用 debug 日志看Negative matches定位为什么没生效用 actuator 的 conditions 端点看运行期的最终结果用 exclude 列表管理我明确不需要的自动配置。做组件封装时再结合ConditionalOnProperty ConditionalOnMissingBean ConfigurationProperties三件套基本能覆盖 90% 的装配控制需求。最后一个建议是如果你在写 Starter 或者公共配置模块每次新增一个条件注解时都在注释里写清楚满足什么条件才装配、不满足时用户应该去哪里配置——这个习惯帮我省掉了无数后续答疑的时间。
返回列表