0
点赞
收藏
分享

微信扫一扫

玩转 @ConditionalOnMissingBean

Gaaidou 2021-09-28 阅读 50
Blog

原文地址:https://alphahinex.github.io/2021/06/27/conditional-on-missing-bean/


description: "学废了吗"
date: 2021.06.27 10:26
categories:
- Spring
tags: [Spring Boot, Spring, Java]
keywords: ConditionalOnMissingBean, K8s


男人,不能说不行! 中,留了两个问题:

  1. 为什么名为 testServiceImpl 的 Bean 会被注册?
  2. 为什么无法注入 TestService 的实例?

先从可运行环境 https://github.com/AlphaHinex/conditional-on-missing-bean-demo 看下结果。

在测试用例中,通过 @Autowired 注解注入了 TestService,但调用时,报了空指针异常,说明容器中没有 TestService 类型的实例。

那么是 @ConditionalOnMissingBean 不行吗?

看下 bean 的定义:

@Service
@ConditionalOnMissingBean(TestService.class)
public class TestServiceImpl implements TestService

debug

打开 org.springframeworkdebug 级别日志,可以看到:

   TestServiceImpl:
      Did not match:
         - @ConditionalOnMissingBean (types: com.example.demo.service.TestService; SearchStrategy: all) found beans of type 'com.example.demo.service.TestService' testServiceImpl (OnBeanCondition)

@ConditionalOnMissingBean 没有生效的原因,是因为其条件没有满足,即 found beans of type 'com.example.demo.service.TestService' testServiceImpl

这就矛盾了啊:

  1. 因为找到了 TestService 类型的 bean,所以没有注册 TestServiceImpl 这个 bean,但找到的那个 bean 的名称,是 testServiceImpl ,也就是 —— TestServiceImpl 自己!
  2. 既然 @ConditionalOnMissingBean 没有生效,说明有这个 bean,但注入的时候还注入了个 null!

一探究竟

先来看下 ConditionalOnMissingBean 中的注释文字:

可以得出几个要点:

  1. 强烈建议仅在自动配置类上使用此注解
  2. 这个条件仅能匹配已经被当前的应用上下文处理过的 bean 定义
  3. 如果候选 bean 是被其他配置类创建的,需确保这个条件在其后运行

所以一般我们很少见到本例中这样直接在 @Service 类上使用 @ConditionalOnMissingBean 的情况,大多数都是如源码中建议所示:

@Configuration
public class MyAutoConfiguration {

    @ConditionalOnMissingBean
    @Bean
    public MyService myService() {
        ...
    }

}

虽说不建议,但也不是不行。从日志中也可以看出,这个条件确实被运行了,只不过并未满足条件。

继续往下看,条件进行计算时,会从当时已经处理过的 bean 定义中进行匹配,也就是说,TestServiceImpl 这个 bean 在进行条件判断时已经注册到 Spring 容器中了。

在 Spring 中,Bean 被抽象为 BeanDefinition,注册到 BeanDefinitionRegistry 中。

注册时,通过 BeanDefinitionRegistry 中的 registerBeanDefinition 方法,将 bean 以 beanName 为 key,beanDefinition 为 value,注册到 BeanDefinitionRegistry 具体实现类的一个 Map 中。

void registerBeanDefinition(String beanName, BeanDefinition beanDefinition)
        throws BeanDefinitionStoreException;

BeanDefinitionReaderUtils.java#L164 打断点,可以观察到 bean 被注册的情况。

可以看到 testServiceImpl 这个 bean,被根据启动类获得到的 basePackages 扫描到了,进而注册到了容器中。

之后会从 ConfigurationClassParser 中获得所有用户自定义的配置类:

Set<ConfigurationClass> configClasses = new LinkedHashSet<>(parser.getConfigurationClasses());

再由 ConfigurationClassBeanDefinitionReader 读取配置类中的 bean 定义:

this.reader.loadBeanDefinitions(configClasses);

ConfigurationClassBeanDefinitionReader 在读取 bean 定义时,会使用 TrackedConditionEvaluator 先进行是否要跳过这个 bean 的判断,如果需要跳过,则从 registry 中将这个 bean 移除:

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

TrackedConditionEvaluator 的 shouldSkip 方法,会使用 ConditionEvaluator 进行条件计算:

skip = conditionEvaluator.shouldSkip(configClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN);

而 ConditionEvaluator 是使用 BeanDefinitionRegistry 构造的:

public ConditionEvaluator(@Nullable BeanDefinitionRegistry registry,
            @Nullable Environment environment, @Nullable ResourceLoader resourceLoader)

所以在进行条件计算时,registry 中有 testServiceImpl 的定义,此时 @ConditionalOnMissingBean(TestService.class) 条件不满足,故 skip 为 true,testServiceImpl 结束了其短暂的生命周期,被从 registry 中移除掉了,这也就解释了在测试用例中,无法再 @Autowired 进来 TestService 实例的原因。

总结

再回顾一下 @ConditionalOnMissingBean 的三个要点:

强烈建议仅在自动配置类上使用此注解

本例中,如果是以如下方式定义这个 bean,则不会出现本例中条件失效,无法从容器中获取此 bean 的情况:

@Bean
@ConditionalOnMissingBean(TestService.class)
public TestService testService() {
    return new TestService() {
        @Override
        public String helloWorld() {
            return this.getClass().getName() + " says hello world";
        }
    };
}

这个条件仅能匹配已经被当前的应用上下文处理过的 bean 定义

因为会先扫描 basePackages 中的 bean,再读取配置类中的 bean,条件的计算是在二者之间,所以上面两种定义 TestService bean 的方式,会得到两个不同的结果。

如果候选 bean 是被其他配置类创建的,需确保这个条件在其后运行

如果已经在 A 配置类中定义了 TestService bean,在 B 配置类中要使用 TestService 进行条件判断,则需保证 B 的配置类在 A 之后被处理,此时可以使用 @AutoConfigureBefore@AutoConfigureOrder 进行配置类先后顺序的控制。

举报

相关推荐

0 条评论