@ConditionalOnMissingBean失效场景解析

1 背景

项目为SpringBoot多模块项目,基础模块中已经创建了鉴权拦截器(基于HandlerInterceptor实现)。
然而有一个子模块需要定制化实现鉴权,其鉴权流程与基础模块中的鉴权流程不相匹配,因此构思通过@ConditionalOnMissingBean实现定制化加载。
具体实现思路如下:

// Common模块

// 定义鉴权接口(基于`HandlerInterceptor`)
public interface AuthInterceptor extends HandlerInterceptor {
}

// 定义默认的鉴权拦截器
@Slf4j
@Configuration
@ConditionalOnMissingBean(AuthInterceptor.class)
public class DefaultAuthInterceptor implements AuthInterceptor {
}
// 业务模块

@Slf4j
@Configuration
public class BusinessAuthInterceptor implements AuthInterceptor {
}

此处,基本思路是:

  • 当业务模块中存在自定义的鉴权拦截器(实现AuthInterceptor),则默认的鉴权拦截器会因为@ConditionalOnMissingBean注解不再创建;
  • 当业务模块中不存在自定义鉴权拦截器,则默认的鉴权拦截器(DefaultAuthInterceptor)将创建并注入指IOC容器中。

2 BUG分析

提交完代码后,存在自定义鉴权拦截器的模块运行正常,然而期望使用默认鉴权拦截器的模块无法正常运行,具体表现为IOC容器未创建DefaultAuthInterceptor实例。因为代码中均是通过注解实现Bean的定义,因此重点关注ConfigurationClassPostProcessor的解析流程。

ConfigurationClassPostProcessor是一个BeanFactory的后置处理器,因此它的主要功能是参与BeanFactory的建造,在这个类中,会解析加了@Configuration的配置类,还会解析@ComponentScan、@ComponentScans注解扫描的包,以及解析@Import等注解。

主要处理流程在public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) 函数中,此函数负责解析以及校验registry中的配置类。以下通过注释解析processConfigBeanDefinitions的具体执行流程。

public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) {
		List<BeanDefinitionHolder> configCandidates = new ArrayList<>();
        // 获取registry中存储的已经解析的beanNames
		String[] candidateNames = registry.getBeanDefinitionNames();

		for (String beanName : candidateNames) {
            // 遍历beanNames,获取相应的BeanDefinition
			BeanDefinition beanDef = registry.getBeanDefinition(beanName);
            // 校验BeanDefinition中的Attribute,确定此配置类是否已经被解析过了
			if (beanDef.getAttribute(ConfigurationClassUtils.CONFIGURATION_CLASS_ATTRIBUTE) != null) {
				if (logger.isDebugEnabled()) {
					logger.debug("Bean definition has already been processed as a configuration class: " + beanDef);
				}
			}
			else if (ConfigurationClassUtils.checkConfigurationClassCandidate(beanDef, this.metadataReaderFactory)) {
				// 校验当前BeanDefinition是否满足配置类要求,如果是,则假如待处理配置类集合
                configCandidates.add(new BeanDefinitionHolder(beanDef, beanName));
			}
		}

		// Return immediately if no @Configuration classes were found
		if (configCandidates.isEmpty()) {
			return;
		}

		// Sort by previously determined @Order value, if applicable
		configCandidates.sort((bd1, bd2) -> {
			int i1 = ConfigurationClassUtils.getOrder(bd1.getBeanDefinition());
			int i2 = ConfigurationClassUtils.getOrder(bd2.getBeanDefinition());
			return Integer.compare(i1, i2);
		});

		// Detect any custom bean name generation strategy supplied through the enclosing application context
		SingletonBeanRegistry sbr = null;
		if (registry instanceof SingletonBeanRegistry) {
			sbr = (SingletonBeanRegistry) registry;
			if (!this.localBeanNameGeneratorSet) {
				BeanNameGenerator generator = (BeanNameGenerator) sbr.getSingleton(
						AnnotationConfigUtils.CONFIGURATION_BEAN_NAME_GENERATOR);
				if (generator != null) {
					this.componentScanBeanNameGenerator = generator;
					this.importBeanNameGenerator = generator;
				}
			}
		}

		if (this.environment == null) {
			this.environment = new StandardEnvironment();
		}

		// Parse each @Configuration class
		ConfigurationClassParser parser = new ConfigurationClassParser(
				this.metadataReaderFactory, this.problemReporter, this.environment,
				this.resourceLoader, this.componentScanBeanNameGenerator, registry);

        // 保存尚未解析的配置类
		Set<BeanDefinitionHolder> candidates = new LinkedHashSet<>(configCandidates);
        // 保存已经解析的配置类
		Set<ConfigurationClass> alreadyParsed = new HashSet<>(configCandidates.size());
		do {
			StartupStep processConfig = this.applicationStartup.start("spring.context.config-classes.parse");
			// 尝试对尚未解析的配置类集合进行解析(此处逻辑比较复杂,后续展开)
            parser.parse(candidates);
            // 此处的校验规则:
            // 如果配置类被@Configuration修饰,且proxyBeanMethods属性设置为true,则当前类不能被final修饰;
            // 并且被@Bean修饰的方法,必须允许被覆盖(@Override),因为proxyBeanMethods被设置为true,斯需要通过cglib进行代理
			parser.validate();

            // 需要注意,在未包含定制化鉴权拦截器的模块中,registry包含DefaultAuthInterceptor的BeanDefinition,beanName为defaultAuthInterceptor

            // 保存解析出来的配置类
			Set<ConfigurationClass> configClasses = new LinkedHashSet<>(parser.getConfigurationClasses());
			// 去除已经被解析的配置类,避免重复解析
            configClasses.removeAll(alreadyParsed);

			// Read the model and create bean definitions based on its content
			if (this.reader == null) {
				this.reader = new ConfigurationClassBeanDefinitionReader(
						registry, this.sourceExtractor, this.resourceLoader, this.environment,
						this.importBeanNameGenerator, parser.getImportRegistry());
			}
            // 根据解析出来的配置类,加载相应的BeanDefinition
            // 需要注意的是,此处实际上是对解析出来的BeanDefinition进行再次校验,其中就包含针对@Conditional相关注解的校验
            // 针对此函数,在下文会进行具体的解析
			this.reader.loadBeanDefinitions(configClasses);
			alreadyParsed.addAll(configClasses);
			processConfig.tag("classCount", () -> String.valueOf(configClasses.size())).end();

			candidates.clear();
			if (registry.getBeanDefinitionCount() > candidateNames.length) {
				String[] newCandidateNames = registry.getBeanDefinitionNames();
				Set<String> oldCandidateNames = new HashSet<>(Arrays.asList(candidateNames));
				Set<String> alreadyParsedClasses = new HashSet<>();
				for (ConfigurationClass configurationClass : alreadyParsed) {
					alreadyParsedClasses.add(configurationClass.getMetadata().getClassName());
				}
				for (String candidateName : newCandidateNames) {
					if (!oldCandidateNames.contains(candidateName)) {
						BeanDefinition bd = registry.getBeanDefinition(candidateName);
						if (ConfigurationClassUtils.checkConfigurationClassCandidate(bd, this.metadataReaderFactory) &&
								!alreadyParsedClasses.contains(bd.getBeanClassName())) {
							candidates.add(new BeanDefinitionHolder(bd, candidateName));
						}
					}
				}
				candidateNames = newCandidateNames;
			}
		}
		while (!candidates.isEmpty());

		// Register the ImportRegistry as a bean in order to support ImportAware @Configuration classes
		if (sbr != null && !sbr.containsSingleton(IMPORT_REGISTRY_BEAN_NAME)) {
			sbr.registerSingleton(IMPORT_REGISTRY_BEAN_NAME, parser.getImportRegistry());
		}

		if (this.metadataReaderFactory instanceof CachingMetadataReaderFactory) {
			// Clear cache in externally provided MetadataReaderFactory; this is a no-op
			// for a shared cache since it'll be cleared by the ApplicationContext.
			((CachingMetadataReaderFactory) this.metadataReaderFactory).clearCache();
		}
	}

此处重点解析ConfigurationClassBeanDefinitionReader.loadBeanDefinitions(Set<ConfigurationClass> configurationModel)函数执行流程的解析:

	public void loadBeanDefinitions(Set<ConfigurationClass> configurationModel) {
        // 创建TrackedConditionEvaluator实例,将由其判断是否
		TrackedConditionEvaluator trackedConditionEvaluator = new TrackedConditionEvaluator();
		for (ConfigurationClass configClass : configurationModel) {
			loadBeanDefinitionsForConfigurationClass(configClass, trackedConditionEvaluator);
		}
	}

    private class TrackedConditionEvaluator {

		private final Map<ConfigurationClass, Boolean> skipped = new HashMap<>();

		public boolean shouldSkip(ConfigurationClass configClass) {
            // 判断在当前循环中,当前配置类是否已经被校验过
			Boolean skip = this.skipped.get(configClass);
			if (skip == null) {
                // 当前配置类没有被校验,进入校验流程
                // Return whether this configuration class was registered via @Import or automatically registered due to being nested within another configuration class. (此处未研究过,掠过)
				if (configClass.isImported()) {
					boolean allSkipped = true;
					for (ConfigurationClass importedBy : configClass.getImportedBy()) {
						if (!shouldSkip(importedBy)) {
							allSkipped = false;
							break;
						}
					}
					if (allSkipped) {
						// The config classes that imported this one were all skipped, therefore we are skipped...
						skip = true;
					}
				}
				if (skip == null) {
                    // 此处根据配置类是否包含@Conditional注解,进行校验
                    // 此处在后续展开讲解
					skip = conditionEvaluator.shouldSkip(configClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN);
				}
				this.skipped.put(configClass, skip);
			}
			return skip;
		}
	}

    // 针对包含@Conditional注解的配置类,进行校验,确定是否需要将其从registry注册的beanDefinitions中进行移除
    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();
			}

            // 当前函数前序都是进行铺垫,此处进行真正的匹配工作
            // 如果匹配失败,则当前类需要从registry中进行移除,无法注册至最终的IOC容器中
            // matches的实现在SpringBootCondition类中,后续文章将对其进行解析
			if ((requiredPhase == null || requiredPhase == phase) && !condition.matches(this.context, metadata)) {
				return true;
			}
		}

		return false;
	}

探究SpringBootConditionmatches函数执行流程。

	@Override
	public final boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
		String classOrMethodName = getClassOrMethodName(metadata);
		try {
			// 根据Condition上下文,判断是否满足匹配要求,输出匹配结果
			ConditionOutcome outcome = getMatchOutcome(context, metadata);
			logOutcome(classOrMethodName, outcome);
			recordEvaluation(context, classOrMethodName, outcome);
			return outcome.isMatch();
		}
		catch (NoClassDefFoundError ex) {
			throw new IllegalStateException("Could not evaluate condition on " + classOrMethodName + " due to "
					+ ex.getMessage() + " not found. Make sure your own configuration does not rely on "
					+ "that class. This can also happen if you are "
					+ "@ComponentScanning a springframework package (e.g. if you "
					+ "put a @ComponentScan in the default package by mistake)", ex);
		}
		catch (RuntimeException ex) {
			throw new IllegalStateException("Error processing condition on " + getName(metadata), ex);
		}
	}

	// getMatchOutcome的具体实现详见OnBeanCondition类
	// class OnBeanCondition extends FilteringSpringBootCondition implements ConfigurationCondition
	@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());
			}
			Set<String> allBeans = matchResult.getNamesOfAllMatches();
			if (allBeans.size() == 1) {
				matchMessage = spec.message(matchMessage).found("a single bean").items(Style.QUOTE, allBeans);
			}
			else {
				List<String> primaryBeans = getPrimaryBeans(context.getBeanFactory(), allBeans,
						spec.getStrategy() == SearchStrategy.ALL);
				if (primaryBeans.isEmpty()) {
					return ConditionOutcome.noMatch(
							spec.message().didNotFind("a primary bean from beans").items(Style.QUOTE, allBeans));
				}
				if (primaryBeans.size() > 1) {
					return ConditionOutcome
							.noMatch(spec.message().found("multiple primary beans").items(Style.QUOTE, primaryBeans));
				}
				matchMessage = spec.message(matchMessage)
						.found("a single primary bean '" + primaryBeans.get(0) + "' from beans")
						.items(Style.QUOTE, allBeans);
			}
		}

		// 处理逻辑在此处
		if (metadata.isAnnotated(ConditionalOnMissingBean.class.getName())) {
			Spec<ConditionalOnMissingBean> spec = new Spec<>(context, metadata, annotations,
					ConditionalOnMissingBean.class);
			// BUG的原因根由在此处,前序解析BeanDefinition时将DefaultAuthInterceptor的BeanDefinition添加至registry中
			// 此处在匹配的时候,发现已经存在DefaultAuthInterceptor的BeanDefinition,因此不满足@ConditionalOnMissingBean的条件
			// 因此匹配的记过为false,执行完此流程之后,上层函数中将从registry中删除DefaultAuthInterceptor的BeanDefinition
			// 这就相应的导致在未实现自定义AuthInterceptor的模块中,报出AuthInterceptor相应Bean缺失的错误
			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);
	}

关于BUG出现的具体缘由,在上文的代码注释中已经给出。

3 复盘

出现此BUG,是因为对@ConditionalOnMissingBean的使用不清晰,在其对应的类型注释中已经给出了推荐的使用方式:

/**
 * {@link Conditional @Conditional} that only matches when no beans meeting the specified
 * requirements are already contained in the {@link BeanFactory}. None of the requirements
 * must be met for the condition to match and the requirements do not have to be met by
 * the same bean.
 * <p>
 * When placed on a {@code @Bean} method, the bean class defaults to the return type of
 * the factory method:
 *
 * <pre class="code">
 * &#064;Configuration
 * public class MyAutoConfiguration {
 *
 *     &#064;ConditionalOnMissingBean
 *     &#064;Bean
 *     public MyService myService() {
 *         ...
 *     }
 *
 * }</pre>
 * <p>
 * In the sample above the condition will match if no bean of type {@code MyService} is
 * already contained in the {@link BeanFactory}.
 * <p>
 * The condition can only match the bean definitions that have been processed by the
 * application context so far and, as such, it is strongly recommended to use this
 * condition on auto-configuration classes only. If a candidate bean may be created by
 * another auto-configuration, make sure that the one using this condition runs after.
 *
 * @author Phillip Webb
 * @author Andy Wilkinson
 * @since 1.0.0
 */
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(OnBeanCondition.class)
public @interface ConditionalOnMissingBean 

此处推荐的是在自动化配置类内部通过@Bean创建对应的bean实例,在@Bean修饰的方法上添加@ConditionalOnMissingBean注解。

4 解决

@Configuration
public class CommonAuthAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean(AuthInterceptor.class)
    public AuthInterceptor defaultAuthInterceptor() {
        return new DefaultAuthInterceptor();
    }

    @Bean
    public WebMvcConfigurer commonAuthMvcConfigurer(AuthInterceptor authInterceptor) {
        return new CommonAuthMvcConfigurer(authInterceptor);
    }
}

按照@ConditionalOnMissingBean注释推荐的方式,使用自动化配置类即可解决此问题。

5 拓展

按照@ConditionalOnMissingBean的注释,当有多个自动化配置类,均会创建某一类型的Bean时,如何按照顺序创建就是一个复杂的问题。

关于此问题,现在的思路是结合spring.factories指定Bean之间创建的顺序关系。

posted @ 2022-07-19 14:56  从此寂静无声  阅读(2823)  评论(0编辑  收藏  举报