@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;
}
探究SpringBootCondition
的matches
函数执行流程。
@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">
* @Configuration
* public class MyAutoConfiguration {
*
* @ConditionalOnMissingBean
* @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之间创建的顺序关系。