ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Spring Boot条件装配:用@ConditionalOnResource按资源文件决定Bean是否加载

Spring Boot条件装配:用@ConditionalOnResource按资源文件决定Bean是否加载 你有没有遇到过这种需求同一个组件包被好几个项目接进去某个能力是否启用取决于classpath下是否存在一个外部配置文件——比如签名公钥、license文件、规则脚本。有文件就加载一组Bean没文件就静默跳过连异常都不能抛。这种场景用ConditionalOnResource来处理最合适。它是Spring Boot条件装配家族里专门用来“判断资源是否存在”的注解。通过它可以把组件能力的开关从代码依赖里剥离开让同一个jar在不同项目里表现出不同的装配行为。这篇文章我会把它的工作原理、源码触发时机、和同类条件注解的取舍讲清楚最后把我在项目里踩过的坑列表出来。适合正在写starter、二次封装公共组件或者被“可有可无的配置文件”折磨过的人阅读。1. 没有这个注解之前条件装配是怎么写的1.1 一个真实场景组件包里带不带资源行为必须不同我在封装内部的一个调用链追踪组件时遇到过一个很典型的问题不同接入方是否开启参数脱敏取决于classpath下是否放了mask-rules.json这份文件。有的项目业务方放了有的项目没放。最朴素的做法是让各业务方自己用ConditionalOnProperty控制但这样就把内部组件的细节暴露给接入方了对方还得去理解“脱敏开关”这个配置项维护成本不低。更常见的场景是license/授权文件SDK在classpath下找license.key找到就加载授权模块找不到就按免费版模式运行。这种情况下“文件在不在”是一个客观事实比人工配置的开关值更可靠。配置项可能被运维误改文件缺失则是最直接的环境状态。所以Spring Boot才会专门提供一个注解把“资源存在性”这个判断固化成内置能力。1.2 旧方案手动判断资源加自定义Condition在Spring Boot条件注解体系成熟之前我一般会写一个自定义的Condition实现类public class OnMaskRuleFileCondition implements Condition { Override public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { ResourceLoader resourceLoader context.getResourceLoader(); Resource resource resourceLoader.getResource(classpath:mask-rules.json); return resource.exists(); } }然后在配置类上写Conditional(OnMaskRuleFileCondition.class)。这套方案能用但缺点非常明显每一个资源判断都要单独写一个Condition类还要记得处理ResourceLoader的获取方式代码散落在各处后来接手的人看到自定义Condition根本不知道它在判断什么全靠类名和注释猜测。一个项目里如果出现三五个这样的类维护起来就想骂人。1.3 从Conditional到Spring Boot的条件注解家族Spring Framework本身提供了Conditional允许用户自定义装配条件。Spring Boot在这个基础上沉淀了一整套开箱即用的条件注解ConditionalOnClass、ConditionalOnProperty、ConditionalOnBean、ConditionalOnExpression、ConditionalOnResource等等。我个人的理解是条件注解本质上是“装配决策的信息来源”。有的条件看classpath里有没有某个类有的条件看配置项的值有的条件看容器里有没有某个BeanConditionalOnResource则是看classpath或文件系统里有没有某个Resource。选哪个取决于功能开关到底是跟着代码依赖走、跟着配置走还是跟着物理文件走。2. ConditionalOnResource到底怎么用2.1 注解定义与属性语义ConditionalOnResource可以放在类上也可以放在Bean方法上。它和Spring Boot其他条件注解一样是被Conditional(OnResourceCondition.class)元标注的组合注解Target({ ElementType.TYPE, ElementType.METHOD }) Retention(RetentionPolicy.RUNTIME) Documented Conditional(OnResourceCondition.class) public interface ConditionalOnResource { String[] resources() default {}; }注意较新版本的Spring Boot中属性名是resources早期资料里也有写成value的看版本。判断时Spring会把resources数组里的每个路径交给ResourceLoader去加载核心逻辑很简单for (String location : locations) { if (loader.getResource(cleanLocation(location)).exists()) { found.add(location); } else { missing.add(location); } }路径支持classpath:前缀、file:前缀也可以不写前缀直接使用相对classpath路径。例如Configuration ConditionalOnResource(resources { classpath:signature/rsa-public.key, classpath:signature/local-pub.key }) public class SignatureConfiguration { // 配置类内容 }2.2 判断逻辑是“任一存在”还是“全部存在”这是必须说清楚的关键点。我翻过OnResourceCondition源码它的匹配逻辑是遍历resources数组里的每个路径只要发现任意一个资源存在整个条件就返回匹配。也就是说你在一个注解里写多个资源它们之间是“逻辑或”的关系不是“逻辑与”。如果你需要所有资源都存在才装配有两个思路一是写一个自定义Condition把“全部满足”的判断逻辑写进去二是把多个ConditionalOnResource拆到不同的配置类上用组合注解或自定义注解去聚合。直白地说一个注解里堆多个值本意大多是“这些文件有任何一个就行”。2.3 最简单的入门示例Configuration ConditionalOnResource(resources classpath:report-template.xlsx) public class ReportExportAutoConfiguration { Bean public ReportExportService reportExportService() { return new ReportExportService(); } }当classpath下存在report-template.xlsx时ReportExportService才会被注册不存在时整个配置类被跳过。启动日志里会看到类似Condition OnResourceCondition did not find resource ...的DEBUG信息。实际项目中我更推荐把注解和自定义starter结合使用公共配置类打包进starter接入方提供对应的资源文件组件就自动开启增强功能不提供就退回基础功能。这是一种比配置项更“自动”的开关方式。3. 条件评估的时机与源码执行链路3.1 条件注解在配置类解析阶段如何被触发理解ConditionalOnResource不能停在“会用”最好知道它什么时候生效。Spring Boot解析配置类的入口是ConfigurationClassParser。在处理每个Configuration类时它会先调用ConditionEvaluator.shouldSkip()来判断这个类要不要被跳过。大致流程是这样的扫描候选配置类ConfigurationClassParser创建ConditionEvaluator对每个配置类检查类上的Conditional注解包括ConditionalOnResource这种被Conditional元注解标注的组合注解如果判断结果是skip整个类就不进入后续解析如果判断结果是保留继续解析类里的Bean方法解析每个Bean方法时也会再次走条件评估判断方法级条件。所以这个注解不管放在类上还是方法上都会在BeanDefinition生成之前就被执行不会出现“Bean先注册了再回头删掉”的情况。条件评估是比较前置的这一点对排查问题非常关键。3.2 为什么在Configuration和Bean方法上都能生效因为ConfigurationClassParser在解析阶段和注册Bean定义阶段分别都会做条件评估。类级条件在parse阶段就决定了是否处理这个类的成员方法级条件则是在把Bean方法转成BeanDefinition时校验。两者的评估时机不同但都发生在Bean实例化之前。这带来的实际好处是你可以把公共条件放在类上把局部差异条件放在某个Bean方法上。这样能实现“同一个配置类里一部分Bean受资源条件控制另一部分不受影响”。比如类上判断主配置存在某个Bean方法上再判断增强配置存在有的项目只启动基础能力有的项目基础能力加增强能力一起上。3.3 资源判断使用的类加载器与常见混淆点OnResourceCondition最终依赖ConditionContext提供的ResourceLoader。在Spring Boot自动配置场景下这个ResourceLoader持有的ClassLoader通常是应用类加载器AppClassLoader或Web应用环境下的WebAppClassLoader能看见classpath下所有打好的类和资源。一个容易混淆的点如果你在工具类的静态代码块里自己写ClassLoader.getSystemResource()去判断资源由于系统类加载器和应用类加载器的视野不同结果可能与ConditionalOnResource不一致。官方注解用ResourceLoader判断不代表你手写判断就能得到同样的结论。另外注解判断的是“资源是否能被ResourceLoader加载”。对于classpath内资源通常会命中但如果你写的是file:绝对路径ResourceLoader会去文件系统找这种用法在多环境部署时不推荐。我自己只在本地联调时用过file:路径上到容器环境就换掉了。4. 和ConditionalOnClass、ConditionalOnProperty放在一起怎么选4.1 条件注解对照表注解判断依据典型场景备注ConditionalOnClass类加载器能否加载到指定类依赖某个可选jar时才启用功能自动配置类的标配ConditionalOnMissingBean容器中是否缺少某类Bean允许用户覆盖默认Bean对顺序敏感ConditionalOnProperty配置项的值通过配置开关功能需要约定属性名ConditionalOnExpressionSpEL表达式结果复杂条件组合表达式写多了可读性差ConditionalOnResource资源文件是否存在外部文件决定能力开关与classpath/文件系统强相关ConditionalOnWebApplication是否Web环境MVC专用配置按应用类型分装配选型思路我建议按“信息的来源”来判断功能依赖某个类是否在classpath用ConditionalOnClass依赖配置文件里的开关值用ConditionalOnProperty依赖某个物理文件是否存在用ConditionalOnResource。三者边界其实很明显。4.2 组合使用的推荐姿势我见过比较规范的做法是一个自动配置类同时使用ConditionalOnClass和ConditionalOnResource限定两个条件都必须满足。例如Configuration(proxyBeanMethods false) ConditionalOnClass(name com.example.sdk.sensitive.SensitiveUtil) ConditionalOnResource(resources classpath:sensitive/mask-rules.json) public class SensitiveFilterAutoConfiguration { // 配置类内容 }类条件保证业务方引入了SensitiveUtil依赖代码调用不会出现NoClassDefFoundError资源条件保证mask-rules.json一定存在过滤器初始化时不会因为读不到文件而抛异常。在自定义starter里这种“类条件加资源条件”的组合用起来非常稳。4.3 一个我经历过的误用教训早期我写一个短信发送组件时错误地用ConditionalOnProperty去控制“是否加载签名校验器”。结果有一台服务器配置项被运维改掉了功能静默失效线上排查了很久。最后发现根源在于配置开关依赖人为维护而文件是否存在是客观事实。后来把这个条件改成ConditionalOnResource(resources classpath:signature/sms-sign.key)问题就消失了。从那以后我给自己定了一条规矩凡是“文件决定能力”的场景优先用资源条件判断而不是依赖配置项。配置项很容易被各种环境差异覆盖文件存在性则稳定得多。5. 两个可直接抄走的实战配置案例5.1 案例一签名资源存在才开启接口加解密过滤器假设我在做一个公共starter要求业务方classpath下存在encrypt-key.pub时自动注册加解密过滤器没有时不注册也不抛错。自动配置类可以这样写AutoConfiguration ConditionalOnResource(resources classpath:security/encrypt-key.pub) ConditionalOnWebApplication(type ConditionalOnWebApplication.Type.SERVLET) public class CryptoFilterAutoConfiguration { Bean public FilterRegistrationBeanCryptoFilter cryptoFilterRegistration() { FilterRegistrationBeanCryptoFilter registration new FilterRegistrationBean(); registration.setFilter(new CryptoFilter()); registration.addUrlPatterns(/api/*); registration.setOrder(Ordered.HIGHEST_PRECEDENCE 10); return registration; } }当不存在公钥文件时整个自动配置类会被跳过接入方连配置都不用动默认不启用加解密。后面想开启把公钥文件放到classpath下重启即可。这里我额外加了一个ConditionalOnWebApplication避免在非Web环境做无意义的过滤器注册。5.2 案例二规则脚本目录存在才装配规则引擎另一个我实际做过的场景是规则引擎集成resources/rules目录下可以放若干规则脚本有目录就启动规则管理器没有就跳过规则服务避免日志里刷一堆file not found。Configuration(proxyBeanMethods false) ConditionalOnResource(resources classpath:rules/) public class RuleEngineAutoConfiguration { Bean public RuleManager ruleManager() { return new RuleManager(new ClassPathResource(rules/)); } }这里我踩过一个小坑用目录路径做判断在IDE里正常打包成jar之后行为可能不同。因为jar包里目录不一定有显式的ZipEntryClassPathResource.exists()对目录的判断在不同容器下表现不一样。后面我会专门讲这个问题更稳妥的做法是判断目录下的代表文件而不是目录本身。6. 用这个注解最容易踩的五个坑6.1 路径前缀和打包格式不一致ConditionalOnResource对路径前缀处理有自己的一套逻辑它会清理掉开头的/和classpath*:前缀再判断。但如果你在代码里写的是file:/absolute/path那么判断结果就依赖文件系统环境了。我遇到过一种情况本地IDE启动时资源在target/classes下一切正常打包成可执行jar后同样是classpath资源判断也正常但有些直接把war包丢进Tomcat的场景资源在WEB-INF/classes下的可见性会和IDE里不一样。排查时先确认你判断的资源是否真的打进了最终产物再看具体容器下路径是否可被ResourceLoader发现。6.2 目录判断的“假阳性”很多人在注解里写classpath:some-dir/这样的目录路径以为能判断“目录存在”。这在Spring Boot IDE环境里通常没问题但打包成jar后就不一定了。原因很简单jar文件里目录不一定有对应的目录条目ZipFile里往往只有文件条目没有目录条目。所以我会明确建议不要用目录路径作为ConditionalOnResource的判断依据。要么判断目录下某个固定文件比如classpath:rules/rule-index.json要么约定一个标识文件比如classpath:rules/.enable。标识文件方案在分布式配置和打包场景里都稳定。6.3 多个resources是或逻辑别当成且这是最容易踩的坑。前面已经说过ConditionalOnResource(resources {a.txt, b.txt})只要其中一个存在就匹配不是两个都要有。如果业务上确实需要“所有资源都存在”不能靠一个注解堆多个值。我一般建议写一个自定义Condition里面用循环判断每个资源全部exists()才返回true。这样语义清晰后面也不会有人误改成或逻辑。6.4 静态块和工具类里的类加载器与注解不一致有时候你想在某个Bean初始化时再次读取同一个资源会顺手在静态代码块里写getClass().getClassLoader().getResource(...)。注意这个结果和ConditionalOnResource的判断结果可能不一致。原因在于OnResourceCondition走的是ResourceLoader底层用到的ClassLoader在Web容器里可能经过多层委派而静态代码块的ClassLoader可能是应用启动早期的类加载器两者视野不同。我之前排查过一起“条件判断通过了但Bean内部读资源却返回null”的诡异问题最后定位就是静态代码块里的类加载器拿不到WebAppClassLoader才能看见的路径。6.5 自动配置顺序导致的“判断成功但Bean仍缺失”资源条件判断成功只是说明配置类可以注册Bean。但如果这个配置类里依赖了另一个配置类注册的Bean而另一个配置类因为资源条件不满足压根没注册那么依赖方就会启动报错。在自定义starter里我会用AutoConfigureAfter显式声明顺序让被依赖的配置类先评估先注册。同时业务方如果有自定义同名Bean也要考虑ConditionalOnMissingBean的配合使用。顺序问题看似和资源无关实际上在多个条件注解叠加时非常容易出现。写在最后的实操体会我自己用下来最大的感受是ConditionalOnResource的真正价值不是省几行代码而是把“环境事实”挡在装配决策前面。资源文件在不在不该由业务方手动配置开关去保证应当由组件自己根据真实情况决定行为。如果你在维护公共starter或组件包遇到“某个功能依赖外部文件才能启动”的需求优先考虑它但一定要记住它判断的是存在性不是内容合法性文件内容为空它也照常放行。我现在的习惯是在资源条件背后再加一层Bean初始化时的内容校验条件负责“能不能加载”业务校验负责“加载内容对不对”两层配合基本就不会出幺蛾子了。
返回列表