一、背景描绘

在项目中经过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中常见的条件注解有:

  • @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完成:

@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,看一下其承继联系

为什么@Conditional会失效?
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;
}

该办法做了以下工作:

  • 假如元数据为空,或许没有被@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中:

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会失效?
另外一些依据@Conditional完成的条件注解,运行原理也根本类似,区别在于其依托的完成类不同:

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

四、问题定位

从前边的剖析中,咱们了解了条件注解工作原理,以及失效的常见原因,结合篇头装备代码,发现咱们写的装备类是@Configuration注解的普通引导类,而依靠的bean是经过starter注入进来的主动安装类,经过代码debug,能够看到:

为什么@Conditional会失效?
此段代码方位是ConfigurationClassPostProcessor�的processConfigBeanDefinitions�办法,解析到的装备类次序是,@Configuration注解的普通装备类优先于主动安装类,BeanDefination注册次序也是依照这个次序,那么也就呈现了,咱们前边条件注解失效,导致@Bean对应的Bean没有注册进来,原因便是执行普通@Configuration注解标注类以及内部@Bean的时分,执行条件注解逻辑,从容器中没有找到@ConditionalOnBean依靠类的BeanDefination界说,所以就呈现目标类没有正常注入的问题。

五、解决方案

想要解决上述问题,要保证装备类的解析和加载在依靠类之后,也便是运用@ConditionalOnBean注解的类的条件判定和注册必须要在依靠的类之后,能够参阅一下方案。

1.确保主动安装类的优先级高于装备类

在主动安装类上运用 @AutoConfigureBefore 或 @AutoConfigureAfter 注解,显式指定主动安装类的加载次序。确保主动安装类在装备类之前被加载和处理。

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

2.将@Bean办法移动到主动安装类中

将有 @ConditionalOnBean 注解的 @Bean 办法移到主动安装类中,这样就能够保证主动安装类中的 Bean 先被加载和注册,满意 @ConditionalOnBean 的条件要求。

@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 的名称。

@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…