内容目录
一、背景描述二、常见条件注解失效场景三、聊一聊条件注解实现原理四、问题定位五、解决方案六、参考
在项目中通过cat上报java对redis相关操作,从而监控redis命令操作的监控指标,在基础组件中写了如下配置:
@Configuration
@ConditionalOnClass({Cat.class,RedisOperations.class})
@Slf4j
public class CatRedisAutoConfiguration {
/**
* redis拦截上报
*/
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
@ConditionalOnBean(RedisConnectionFactory.class)
@ConditionalOnMissingBean
public RedisAopService redisAopService(){
log.info("CatAutoConfiguration init RedisAopService...");
return new RedisAopService();
}
}
业务项目添加该配置后,启动项目发现RedisAopService并没有注入进去,redis相关操作并没有上报,怀疑是条件注解失效导致的问题。
springboot中常见的条件注解有:
这些条件注解也都是基于@Conditional实现,@Conditional 注解用于根据特定的条件来决定是否启用或禁用某个组件或配置。它可以应用于类、方法或配置类上。当条件不满足时,被 @Conditional 注解标记的组件或配置将被忽略,不会被加载到 Spring 容器中。以下常见情况下,@Conditional注解可能会失效:
从之前的两篇文章《ConfigurationClassPostProcessor原理详解》和《springboot自动装配原理》中可以了解到配置类的解析和加载成BeanDefination都是由ConfigurationClassPostProcessor完成。
我们选择@ConditionalOnBean为例,分析一下springboot条件注解的视线原理,看一下@ConditionalOnBean实现:
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(OnBeanCondition.class)
public @interface ConditionalOnBean {
Class<?>[] value() default {};
String[] type() default {};
Class<? extends Annotation>[] annotation() default {};
String[] name() default {};
SearchStrategy search() default SearchStrategy.ALL;
Class<?>[] parameterizedContainer() default {};
}
该注解依赖@Conditional注解,并且依赖OnBeanCondition.class,一般常用到的是value属性,也就是依赖的bean。
@ConditionalOnBean的生效依赖OnBeanCondition,看一下其继承关系:
OnBeanCondition本质是是一个Condition,并且继承了SpringBootCondition拥有一些条件注解的通用能力,并且拥有其他一些工具能力。它的核心方法是实现SpringBootCondition定义的getMatchOutcome方法,看一下方法实现:
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
ConditionMessage matchMessage = ConditionMessage.empty();
MergedAnnotations annotations = metadata.getAnnotations();
if (annotations.isPresent(ConditionalOnBean.class)) {
Spec<ConditionalOnBean> spec = new Spec<>(context, metadata, annotations, ConditionalOnBean.class);
MatchResult matchResult = getMatchingBeans(context, spec);
if (!matchResult.isAllMatched()) {
String reason = createOnBeanNoMatchReason(matchResult);
return ConditionOutcome.noMatch(spec.message().because(reason));
}
matchMessage = spec.message(matchMessage).found("bean", "beans").items(Style.QUOTE,
matchResult.getNamesOfAllMatches());
}
if (metadata.isAnnotated(ConditionalOnSingleCandidate.class.getName())) {
Spec<ConditionalOnSingleCandidate> spec = new SingleCandidateSpec(context, metadata, annotations);
MatchResult matchResult = getMatchingBeans(context, spec);
if (!matchResult.isAllMatched()) {
return ConditionOutcome.noMatch(spec.message().didNotFind("any beans").atAll());
}
else if (!hasSingleAutowireCandidate(context.getBeanFactory(), matchResult.getNamesOfAllMatches(),
spec.getStrategy() == SearchStrategy.ALL)) {
return ConditionOutcome.noMatch(spec.message().didNotFind("a primary bean from beans")
.items(Style.QUOTE, matchResult.getNamesOfAllMatches()));
}
matchMessage = spec.message(matchMessage).found("a primary bean from beans").items(Style.QUOTE,
matchResult.getNamesOfAllMatches());
}
if (metadata.isAnnotated(ConditionalOnMissingBean.class.getName())) {
Spec<ConditionalOnMissingBean> spec = new Spec<>(context, metadata, annotations,
ConditionalOnMissingBean.class);
MatchResult matchResult = getMatchingBeans(context, spec);
if (matchResult.isAnyMatched()) {
String reason = createOnMissingBeanNoMatchReason(matchResult);
return ConditionOutcome.noMatch(spec.message().because(reason));
}
matchMessage = spec.message(matchMessage).didNotFind("any beans").atAll();
}
return ConditionOutcome.match(matchMessage);
}
该方法分别支持@ConditionalOnBean、@ConditionalOnSingleCandidate和@ConditionalOnMissingBean三个条件注解的逻辑判定,继续分析@ConditionalOnBean,就是检查容器中是否有符合条件的bean。会继续调用getMatchingBeans方法实现:
protected final MatchResult getMatchingBeans(ConditionContext context, Spec<?> spec) {
ClassLoader classLoader = context.getClassLoader();
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
boolean considerHierarchy = spec.getStrategy() != SearchStrategy.CURRENT;
Set<Class<?>> parameterizedContainers = spec.getParameterizedContainers();
if (spec.getStrategy() == SearchStrategy.ANCESTORS) {
BeanFactory parent = beanFactory.getParentBeanFactory();
Assert.isInstanceOf(ConfigurableListableBeanFactory.class, parent,
"Unable to use SearchStrategy.ANCESTORS");
beanFactory = (ConfigurableListableBeanFactory) parent;
}
MatchResult result = new MatchResult();
Set<String> beansIgnoredByType = getNamesOfBeansIgnoredByType(classLoader, beanFactory, considerHierarchy,
spec.getIgnoredTypes(), parameterizedContainers);
for (String type : spec.getTypes()) {
Collection<String> typeMatches = getBeanNamesForType(classLoader, considerHierarchy, beanFactory, type,
parameterizedContainers);
Iterator<String> iterator = typeMatches.iterator();
while (iterator.hasNext()) {
String match = iterator.next();
if (beansIgnoredByType.contains(match) || ScopedProxyUtils.isScopedTarget(match)) {
iterator.remove();
}
}
if (typeMatches.isEmpty()) {
result.recordUnmatchedType(type);
}
else {
result.recordMatchedType(type, typeMatches);
}
}
for (String annotation : spec.getAnnotations()) {
Set<String> annotationMatches = getBeanNamesForAnnotation(classLoader, beanFactory, annotation,
considerHierarchy);
annotationMatches.removeAll(beansIgnoredByType);
if (annotationMatches.isEmpty()) {
result.recordUnmatchedAnnotation(annotation);
}
else {
result.recordMatchedAnnotation(annotation, annotationMatches);
}
}
for (String beanName : spec.getNames()) {
if (!beansIgnoredByType.contains(beanName) && containsBean(beanFactory, beanName, considerHierarchy)) {
result.recordMatchedName(beanName);
}
else {
result.recordUnmatchedName(beanName);
}
}
return result;
}
此方法的逻辑是,从目标注解中解析出来value、type、name以及annotation属性,从beanFactory中检查是否存在符合条件的bean,并且在结果中标记是否匹配。
然后我们再看一下springboot启动时,解析加载BeanDefination的逻辑,对于引导类的BeanDefination注册由ConfigurationClassPostProcessor的postProcessBeanDefinitionRegistry方法实现:
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
int registryId = System.identityHashCode(registry);
if (this.registriesPostProcessed.contains(registryId)) {
throw new IllegalStateException(
"postProcessBeanDefinitionRegistry already called on this post-processor against " + registry);
}
if (this.factoriesPostProcessed.contains(registryId)) {
throw new IllegalStateException(
"postProcessBeanFactory already called on this post-processor against " + registry);
}
this.registriesPostProcessed.add(registryId);
processConfigBeanDefinitions(registry);
}
在通过ConfigurationClassParser类解析后,会通过ConfigurationClassBeanDefinitionReader类的loadBeanDefinitions方法加载:
public void loadBeanDefinitions(Set<ConfigurationClass> configurationModel) {
TrackedConditionEvaluator trackedConditionEvaluator = new TrackedConditionEvaluator();
for (ConfigurationClass configClass : configurationModel) {
loadBeanDefinitionsForConfigurationClass(configClass, trackedConditionEvaluator);
}
}
此处创建了TrackedConditionEvaluator类型的ConditionEvaluator,持有ConditionEvaluator实例,然后调用loadBeanDefinitionsForConfigurationClass方法加载@Configuration注解类。
private void loadBeanDefinitionsForConfigurationClass(
ConfigurationClass configClass, TrackedConditionEvaluator trackedConditionEvaluator) {
if (trackedConditionEvaluator.shouldSkip(configClass)) {
String beanName = configClass.getBeanName();
if (StringUtils.hasLength(beanName) && this.registry.containsBeanDefinition(beanName)) {
this.registry.removeBeanDefinition(beanName);
}
this.importRegistry.removeImportingClass(configClass.getMetadata().getClassName());
return;
}
if (configClass.isImported()) {
registerBeanDefinitionForImportedConfigurationClass(configClass);
}
for (BeanMethod beanMethod : configClass.getBeanMethods()) {
loadBeanDefinitionsForBeanMethod(beanMethod);
}
//省略...
}
该方法先检查外层@Configuration注解的类是否需要跳过加载,如果跳过就不加载,如果不跳过就继续解析加载里边的内容,TrackedConditionEvaluator的shouldSkip逻辑会委托给ConditionEvaluator处理,此处暂不展开分析,在@Configuration类里边@Bean和@ConditionalOnBean注解的方法解析时一起分析。我们在@Configuration注解的类里边定义了@Bean注解方法注册bean,然后遍历并调用loadBeanDefinitionsForBeanMethod方法加载注册BeanDefination,看一下实现:
private void loadBeanDefinitionsForBeanMethod(BeanMethod beanMethod) {
ConfigurationClass configClass = beanMethod.getConfigurationClass();
MethodMetadata metadata = beanMethod.getMetadata();
String methodName = metadata.getMethodName();
// Do we need to mark the bean as skipped by its condition?
if (this.conditionEvaluator.shouldSkip(metadata, ConfigurationPhase.REGISTER_BEAN)) {
configClass.skippedBeanMethods.add(methodName);
return;
}
if (configClass.skippedBeanMethods.contains(methodName)) {
return;
}
AnnotationAttributes bean = AnnotationConfigUtils.attributesFor(metadata, Bean.class);
Assert.state(bean != null, "No @Bean annotation attributes");
//省略...
this.registry.registerBeanDefinition(beanName, beanDefToRegister);
}
上述方法省略掉了中间组装需要注册的bean的BeanDefination相关内容,整体逻辑大概是,先检查是否需要跳过注册,如果跳过则直接返回,不注册BeanDefination,否则组装BeanDefination并注册到容器中。我们主要看一下conditionEvaluator.shouldSkip的实现:
public boolean shouldSkip(@Nullable AnnotatedTypeMetadata metadata, @Nullable ConfigurationPhase phase) {
if (metadata == null || !metadata.isAnnotated(Conditional.class.getName())) {
return false;
}
if (phase == null) {
if (metadata instanceof AnnotationMetadata &&
ConfigurationClassUtils.isConfigurationCandidate((AnnotationMetadata) metadata)) {
return shouldSkip(metadata, ConfigurationPhase.PARSE_CONFIGURATION);
}
return shouldSkip(metadata, ConfigurationPhase.REGISTER_BEAN);
}
List<Condition> conditions = new ArrayList<>();
for (String[] conditionClasses : getConditionClasses(metadata)) {
for (String conditionClass : conditionClasses) {
Condition condition = getCondition(conditionClass, this.context.getClassLoader());
conditions.add(condition);
}
}
AnnotationAwareOrderComparator.sort(conditions);
for (Condition condition : conditions) {
ConfigurationPhase requiredPhase = null;
if (condition instanceof ConfigurationCondition) {
requiredPhase = ((ConfigurationCondition) condition).getConfigurationPhase();
}
if ((requiredPhase == null || requiredPhase == phase) && !condition.matches(this.context, metadata)) {
return true;
}
}
return false;
}
该方法做了以下事情:
这里的关键点是condition.matches方法,前边我们使用的是@ConditionalOnBean,所以此处的Condition是OnBeanCondition,我们看一下它的matches方法实现,前边从继承关系中看到OnBeanCondition继承了SpringBootCondition,matches方法的定义和实现在SpringBootCondition中:
public final boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
String classOrMethodName = getClassOrMethodName(metadata);
try {
ConditionOutcome outcome = getMatchOutcome(context, metadata);
logOutcome(classOrMethodName, outcome);
recordEvaluation(context, classOrMethodName, outcome);
return outcome.isMatch();
}
catch (NoClassDefFoundError ex) {
throw new IllegalStateException(ex);
}
catch (RuntimeException ex) {
throw new IllegalStateException("Error processing condition on " + getName(metadata), ex);
}
}
此方法调用getMatchOutcome方法,并通过返回结果的isMatch决定是否找到匹配,该类的getMatchOutcome方法是抽象,交给子类实现,这里就是我们前边分析的OnBeanCondition类的getMatchOutcome方法。
由于我们先分析的OnBeanCondition,后分析的条件注解调用,不太好理解,梳理了一下,整体流程大致如下:
另外一些基于@Conditional实现的条件注解,运行原理也基本类似,区别在于其依托的实现类不同:
从前边的分析中,我们了解了条件注解工作原理,以及失效的常见原因,结合篇头配置代码,发现我们写的配置类是@Configuration注解的普通引导类,而依赖的bean是通过starter注入进来的自动装配类,通过代码debug,可以看到:
此段代码位置是ConfigurationClassPostProcessor的processConfigBeanDefinitions方法,解析到的配置类顺序是,@Configuration注解的普通配置类优先于自动装配类,BeanDefination注册顺序也是按照这个顺序,那么也就出现了,我们前边条件注解失效,导致@Bean对应的Bean没有注册进来,原因就是执行普通@Configuration注解标注类以及内部@Bean的时候,执行条件注解逻辑,从容器中没有找到@ConditionalOnBean依赖类的BeanDefination定义,所以就出现目标类没有正常注入的问题。
想要解决上述问题,要保证配置类的解析和加载在依赖类之后,也就是使用@ConditionalOnBean注解的类的条件判定和注册必须要在依赖的类之后,可以参考一下方案。
在自动装配类上使用 @AutoConfigureBefore 或 @AutoConfigureAfter 注解,显式指定自动装配类的加载顺序。确保自动装配类在配置类之前被加载和处理。
@AutoConfigureBefore(CatAutoConfiguration.class)
@Configuration
public class SomeAutoConfiguration {
// ...
}
将有 @ConditionalOnBean 注解的 @Bean 方法移到自动装配类中,这样就可以保证自动装配类中的 Bean 先被加载和注册,满足 @ConditionalOnBean 的条件要求。
@Configuration
public class CatAutoConfiguration {
// ...
}
@Configuration
@ConditionalOnClass({Cat.class})
@Slf4j
public class SomeAutoConfiguration {
@Bean
@ConditionalOnBean(RedisConnectionFactory.class)
public RedisAopService redisAopService() {
// ...
}
}
在需要等待自动装配类中某个 Bean 加载完毕后再初始化 @Bean 的情况下,可以在 @Bean 方法上使用 @DependsOn 注解,指定依赖的 Bean 的名称。
@Configuration
public class CatAutoConfiguration {
@Bean
public RedisConnectionFactory redisConnectionFactory() {
// ...
}
}
@Configuration
@ConditionalOnClass({Cat.class})
@Slf4j
public class SomeAutoConfiguration {
@Bean
@ConditionalOnBean(name = "redisConnectionFactory")
@DependsOn("redisConnectionFactory") // 等待 redisConnectionFactory 初始化完毕
public RedisAopService redisAopService() {
// ...
}
}
https://view.inews.qq.com/k/20220709A012O800?web_channel=wap&openApp=false&f=newdc
https://blog.csdn.net/qq_41737716/article/details/98028976
https://dmsupine.com/2021/04/27/springboot-qi-dong-yuan-li/
本文分享自 PersistentCoder 微信公众号,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。
本文参与 腾讯云自媒体同步曝光计划 ,欢迎热爱写作的你一起参与!
扫码关注腾讯云开发者
领取腾讯云代金券
Copyright © 2013 - 2025 Tencent Cloud. All Rights Reserved. 腾讯云 版权所有
深圳市腾讯计算机系统有限公司 ICP备案/许可证号:粤B2-20090059 深公网安备号 44030502008569
腾讯云计算(北京)有限责任公司 京ICP证150476号 | 京ICP备11018762号 | 京公网安备号11010802020287
Copyright © 2013 - 2025 Tencent Cloud.
All Rights Reserved. 腾讯云 版权所有