为什么@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本质是是一个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,后分析的条件注解调用,不太好理解,梳理了一下,整体流程大致如下: 另外一些基于@Conditional实现的条件注解,运行原理也基本类似,区别在于其依托的实现类不同:

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

四、问题定位

从前边的分析中,我们了解了条件注解工作原理,以及失效的常见原因,结合篇头配置代码,发现我们写的配置类是@Configuration注解的普通引导类,而依赖的bean是通过starter注入进来的自动装配类,通过代码debug,可以看到: 此段代码位置是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() {
        // ...
    }
}

六、参考

view.inews.qq.com/k/20220709A... blog.csdn.net/qq_41737716... dmsupine.com/2021/04/27/... mp.weixin.qq.com/s/IA8P03Klz...

相关推荐
@yanyu6669 分钟前
springboot实现查询学生
java·spring boot·后端
酷爱码38 分钟前
Spring Boot项目中JSON解析库的深度解析与应用实践
spring boot·后端·json
java干货2 小时前
虚拟线程与消息队列:Spring Boot 3.5 中异步架构的演进与选择
spring boot·后端·架构
武昌库里写JAVA4 小时前
iview Switch Tabs TabPane 使用提示Maximum call stack size exceeded堆栈溢出
java·开发语言·spring boot·学习·课程设计
小白杨树树5 小时前
【WebSocket】SpringBoot项目中使用WebSocket
spring boot·websocket·网络协议
clk660711 小时前
Spring Boot
java·spring boot·后端
爱敲代码的TOM11 小时前
基于JWT+SpringSecurity整合一个单点认证授权机制
spring boot
loser.loser12 小时前
QQ邮箱发送验证码(Springboot)
java·spring boot·mybatis