为什么@Conditional会失效?

一、背景描述

在项目中通过cat上报java对redis相关操作,从而监控redis命令操作的监控指标,在基础组件中写了如下配置:

 
 

java

复制代码

@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中常见的条件注解有:

  • @ConditionalOnClass:当类路径中存在指定的类时,条件才会成立。
  • @ConditionalOnMissingClass:当类路径中不存在指定的类时,条件才会成立。
  • @ConditionalOnBean:当容器中存在指定的 Bean 时,条件才会成立。
  • @ConditionalOnMissingBean:当容器中不存在指定的 Bean 时,条件才会成立。
  • @ConditionalOnProperty:当指定的属性在配置文件中被设置为特定的值时,条件才会成立。
  • @ConditionalOnExpression:通过 SpEL 表达式来判断条件是否成立。
  • @ConditionalOnWebApplication:当是一个 Web 应用程序时,条件才会成立。
  • @ConditionalOnNotWebApplication:当不是一个 Web 应用程序时,条件才会成立。

这些条件注解也都是基于@Conditional实现,@Conditional 注解用于根据特定的条件来决定是否启用或禁用某个组件或配置。它可以应用于类、方法或配置类上。当条件不满足时,被 @Conditional 注解标记的组件或配置将被忽略,不会被加载到 Spring 容器中。以下常见情况下,@Conditional注解可能会失效:

  • 条件表达式始终返回 false:如果条件表达式的逻辑判断始终返回 false,那么被 @Conditional 注解标记的组件或配置将不会生效,无论条件是否满足。
  • 条件依赖的Bean未被正确注入:在定义条件注解时,如果条件依赖某个 Bean 的存在或属性值,但这个 Bean 在运行时未被正确注入,那么条件判断可能会失效。
  • 条件依赖的class未被加载:在条件注解依赖的class,未被引入或者由于版本冲突未被正确加载,也会导致条件注解失效。
  • 条件不存在或配置错误:如果自定义的条件类或条件判断方法存在问题,或者配置了不存在的条件类,那么条件判断也可能失效。
  • 条件不在正确的上下文中生效:有些条件注解只在特定的上下文环境下才会生效,例如 @ConditionalOnWebApplication 只在 Web 应用上下文中生效。如果将这样的条件注解应用在非对应的上下文环境中,条件判断也会失效。
  • Bean注入顺序问题:条件注解依赖的bean在条件注解生效判断时,还没有被注册成BeanDefination,但是最终会被注册进来,导致条件注解失效。

三、聊一聊条件注解实现原理

从之前的两篇文章《ConfigurationClassPostProcessor原理详解》《springboot自动装配原理》中可以了解到配置类的解析和加载成BeanDefination都是由ConfigurationClassPostProcessor完成。 我们选择@ConditionalOnBean为例,分析一下springboot条件注解的视线原理,看一下@ConditionalOnBean实现:

 
 

java

复制代码

@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.png

OnBeanCondition本质是是一个Condition,并且继承了SpringBootCondition拥有一些条件注解的通用能力,并且拥有其他一些工具能力。它的核心方法是实现SpringBootCondition定义的getMatchOutcome�方法,看一下方法实现:

 
 

java

复制代码

@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方法实现:

 
 

java

复制代码

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�方法实现:

 
 

java

复制代码

@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�方法加载:

 
 

java

复制代码

public void loadBeanDefinitions(Set<ConfigurationClass> configurationModel) { TrackedConditionEvaluator trackedConditionEvaluator = new TrackedConditionEvaluator(); for (ConfigurationClass configClass : configurationModel) { loadBeanDefinitionsForConfigurationClass(configClass, trackedConditionEvaluator); } }

此处创建了TrackedConditionEvaluator类型的ConditionEvaluator,持有ConditionEvaluator实例,然后调用loadBeanDefinitionsForConfigurationClass方法加载@Configuration注解类。

 
 

java

复制代码

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,看一下实现:

 
 

java

复制代码

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的实现:

 
 

java

复制代码

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; }

该方法做了以下事情:

  • 如果元数据为空,或者没有被@Conditional注解,则返回false,不跳过注册@Bean
  • 如果配置阶段为空,则重新提取调用,否则配置阶段默认为REGISTER_BEAN
  • 从元数据解析出来@Conditional中的依赖类,比如@ConditionalOnBean使用@Conditional(OnBeanCondition.class),那么此处提取出来的Condition类就是OnBeanCondition
  • 实例化Condition类并添加到conditions备用,之所以这里是列表,是因为可能@Bean标注的方法上除了@ConditionalOnBean还有@ConditionalOnMissingBean等多个条件注解
  • 对条件注解支持类Condition列表进行排序,然后遍历判断是否满足所有条件,如果是返回正常注册,否则跳过注册

这里的关键点是condition.matches方法,前边我们使用的是@ConditionalOnBean,所以此处的Condition是OnBeanCondition,我们看一下它的matches方法实现,前边从继承关系中看到OnBeanCondition继承了SpringBootCondition�,matches方法的定义和实现在SpringBootCondition中:

 
 

java

复制代码

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,后分析的条件注解调用,不太好理解,梳理了一下,整体流程大致如下:

image.png

另外一些基于@Conditional实现的条件注解,运行原理也基本类似,区别在于其依托的实现类不同:

  • @ConditionalOnBean->OnBeanCondition
  • @ConditionalOnClass -> OnClassCondition�
  • @ConditionalOnProperty -> OnPropertyCondition�
  • @ConditionalOnExpression -> OnExpressionCondition�
  • @ConditionalOnWebApplication -> OnWebApplicationCondition�

四、问题定位

从前边的分析中,我们了解了条件注解工作原理,以及失效的常见原因,结合篇头配置代码,发现我们写的配置类是@Configuration注解的普通引导类,而依赖的bean是通过starter注入进来的自动装配类,通过代码debug,可以看到:

image.png

此段代码位置是ConfigurationClassPostProcessor�的processConfigBeanDefinitions�方法,解析到的配置类顺序是,@Configuration注解的普通配置类优先于自动装配类,BeanDefination注册顺序也是按照这个顺序,那么也就出现了,我们前边条件注解失效,导致@Bean对应的Bean没有注册进来,原因就是执行普通@Configuration注解标注类以及内部@Bean的时候,执行条件注解逻辑,从容器中没有找到@ConditionalOnBean依赖类的BeanDefination定义,所以就出现目标类没有正常注入的问题。

五、解决方案

想要解决上述问题,要保证配置类的解析和加载在依赖类之后,也就是使用@ConditionalOnBean注解的类的条件判定和注册必须要在依赖的类之后,可以参考一下方案。

1.确保自动装配类的优先级高于配置类

在自动装配类上使用 @AutoConfigureBefore 或 @AutoConfigureAfter 注解,显式指定自动装配类的加载顺序。确保自动装配类在配置类之前被加载和处理。

 
 

java

复制代码

@AutoConfigureBefore(CatAutoConfiguration.class) @Configuration public class SomeAutoConfiguration { // ... }

2.将@Bean方法移动到自动装配类中

将有 @ConditionalOnBean 注解的 @Bean 方法移到自动装配类中,这样就可以保证自动装配类中的 Bean 先被加载和注册,满足 @ConditionalOnBean 的条件要求。

 
 

java

复制代码

@Configuration public class CatAutoConfiguration { // ... } @Configuration @ConditionalOnClass({Cat.class}) @Slf4j public class SomeAutoConfiguration { @Bean @ConditionalOnBean(RedisConnectionFactory.class) public RedisAopService redisAopService() { // ... } }

3.使用@DependsOn注解

在需要等待自动装配类中某个 Bean 加载完毕后再初始化 @Bean 的情况下,可以在 @Bean 方法上使用 @DependsOn 注解,指定依赖的 Bean 的名称。

 
 

java

复制代码

@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() { // ... } }

猜你喜欢

转载自blog.csdn.net/Trouvailless/article/details/132022526