关于ConditionalOnMissingBean失效问题的追踪
作者:伊布拉西莫
遇到一个@ConditionalOnMissingBean失效的问题,今天花点时间来分析一下。
现场回放
services
首先介绍下代码结构:有RunService,以及它的两个实现类:TrainRunServiceImpl和CarRunServiceImpl
RunService
public interface RunService { void run(); }
TrainRunServiceImpl
public class TrainRunServiceImpl implements RunService { @Override public void run() { System.out.println("开火车,wuwuwuwuwu"); } }
CarRunServiceImpl
public class CarRunServiceImpl implements RunService { @Override public void run() { System.out.println("汽车,dididi"); } }
操作类
操作类MyInitBean中,注入了RunService – byType
@Component public class MyInitBean implements InitializingBean { @Autowired private RunService runService; @Override public void afterPropertiesSet() throws Exception { runService.run(); } }
configuration
我们在配置类中,注入RunService的实现bean,并通过@ConditionalOnMissingBean来判断是否注入。
@Configuration public class MyConfiguration { @Bean @ConditionalOnMissingBean public RunService carRunServiceImpl() { return new CarRunServiceImpl(); } @Bean public RunService trainRunServiceImpl() { return new TrainRunServiceImpl(); } }
抛出异常
按照上述的代码,执行后,本以为会成功执行,但是却抛出了异常,异常信息如下:
在spring容器中存在了两个RunService实现类。
这导致了MyInitBean无法决定它到底该使用这两个中的哪一个。(默认是byType注入的)
按照上述的异常信息,它给出了两种解决方案:
@Qualifier
在注入bean时,指定bean的名称.
@Controller public class MyInitBean implements InitializingBean { @Autowired @Qualifier("carRunServiceImpl") private RunService runService; }
通过@Configuration配置类注入的bean,默认名称为方法名称
@Bean // `trainRunServiceImpl ` public RunService trainRunServiceImpl() { return new TrainRunServiceImpl(); }
直接在类头部申明注入的bean,默认名称为类名称
@Service // `trainRunServiceImpl` public class TrainRunServiceImpl implements RunService { }
@Primary
@Primary的作用是,在bean存在多个候选者且无法决定使用哪一个时,优先使用带有该注解的bean.
在配置类中Configuration添加
@Bean @Primary public RunService trainRunServiceImpl() { return new TrainRunServiceImpl(); }
在类申明中添加
@Primary public class TrainRunServiceImpl implements RunService { }
注意
在上述给出的两种方法中,无论是使用@Primary还是这里容器中仍然存在多个实现类,
这并不是我们想要的结果。
这里为什么@ConditionalOnMissingBean会失效呢?
问题定位
在进行问题定位前,我们先来回顾一下@ConditionalOnMissingBean的工作原理
工作原理
@ConditionalOnMissingBean
ConditionalOnMissingBean的注解定义如下:
@Target({ ElementType.TYPE, ElementType.METHOD }) @Retention(RetentionPolicy.RUNTIME) @Documented @Conditional(OnBeanCondition.class) public @interface ConditionalOnMissingBean { Class<?>[] value() default {}; String[] type() default {}; //略.... }
@ConditionalOnMissingBean通常可以有如下三种使用方式:
@Bean // @ConditionalOnMissingBean(type ="xxx.yyy.zzz.service") // @ConditionalOnMissingBean(value = RunService.class) @ConditionalOnMissingBean //无参数,表示按照返回值类型过滤 public RunService carRunServiceImpl() { return new CarRunServiceImpl(); }
在注解上看到了一个OnBeanCondition类,在@ConditionalOnBean,ConditionalOnSingleCandidate和ConditionalOnMissingBean都看到了它的身影。
OnBeanCondition
@Order(Ordered.LOWEST_PRECEDENCE) class OnBeanCondition extends SpringBootCondition implements ConfigurationCondition { @Override public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) { //ConditionalOnBean 略 //ConditionalOnSingleCandidate 略 if (metadata.isAnnotated(ConditionalOnMissingBean.class.getName())) { //寻找 @ConditionalOnMissingBean 匹配的 type; BeanSearchSpec spec = new BeanSearchSpec(context, metadata,ConditionalOnMissingBean.class); //从容器中寻找指定的type --- step1 MatchResult matchResult = getMatchingBeans(context, spec); if (matchResult.isAnyMatched()) { //如果存在指定的type //reason: found beans of type 'service.Service' AServiceImpl String reason = createOnMissingBeanNoMatchReason(matchResult); //创建 ConditionOutcome.noMatch: return new ConditionOutcome(false, message); return ConditionOutcome.noMatch(ConditionMessage .forCondition(ConditionalOnMissingBean.class, spec) .because(reason)); } matchMessage = matchMessage.andCondition(ConditionalOnMissingBean.class, spec) .didNotFind("any beans").atAll(); } //默认 创建 ConditionOutcome.match : return new ConditionOutcome(true, message); return ConditionOutcome.match(matchMessage); } }
ConditionOutcome 的用法:当match= true时,才注入容器.
若@ConditionalOnMissingBean找到了匹配项,则返回ConditionOutcome.notMatch,则不注入容器。
问题出在哪?
有了上面的一系列原理支撑,但是为什么没有执行到我们想要的结果呢?
debug执行后,发现问题出现在OnBeanCondition .getMatchingBeans(context, spec)这个方法中。
首先再次回顾下配置类:
在注入carRunServiceImpl时,执行OnBeanCondition .getMatchingBeans(context, spec)并没有找到下面定义的trainRunServiceImpl.
真相只有一个:
@Configuration 在初始化bean的时候,顺序出现了问题,那么如何控制初始化bean的顺序呢?
解决问题
一顿分析之后,我们发现只要控制了bean的加载顺序之后,上述的问题就可以解决了。
接下来我们来尝试控制bean初始化顺序:
Configuration中bean使用@Order ----------------- failure
@Configuration public class MyConfiguration { @Order(2) @Bean @ConditionalOnMissingBean public RunService carRunServiceImpl() { return new CarRunServiceImpl(); } @Order(1) @Bean public RunService trainRunServiceImpl() { return new TrainRunServiceImpl(); } }
Configuration 调整bean申明顺序----------------- success
将带有@ConditionalOnMissingBean注解的bean,申明在代码的末尾位置,操作成功:
@Configuration public class MyConfiguration { @Bean public RunService trainRunServiceImpl() { return new TrainRunServiceImpl(); } @Bean @ConditionalOnMissingBean public RunService carRunServiceImpl() { return new CarRunServiceImpl(); } }
配置多个Configuration类,并通过@Order指定顺序---------------- failure
@Configuration @Order(Ordered.LOWEST_PRECEDENCE) //最低优先级 public class MyConfiguration { @Bean @ConditionalOnMissingBean public RunService carRunServiceImpl() { return new CarRunServiceImpl(); } } @Configuration @Order(Ordered.HIGHEST_PRECEDENCE) //最高优先级 public class MyConfiguration2 { @Bean public RunService trainRunServiceImpl() { return new TrainRunServiceImpl(); } }
@Configuration并不能通过@Order指定顺序。
大胆猜测下: @Configuration通过配置类名的自然顺序来加载的。
@Configuration配置类加载顺序通过类名顺序来加载 ------- 验证success
将MyConfiguration2重命名为Configuration2,而它的加载顺序在MyConfiguration之前,执行程序成功。
这里貌似所有的问题似乎都解决了, 只需要我们自定义的配置类名称保证最优先加载就可以了。我们只需要注意配置类的命名规则即可.
但是,这种解决方案,似乎并不是那么令人信服。
@AutoConfigureBefore,@AutoConfigureAfter
经查文档,终于找到了需要的东西:我们可以通过@AutoConfigureBefore,@AutoConfigureAfter来控制配置类的加载顺序。
@Configuration public class MyConfiguration { @Bean @ConditionalOnMissingBean public RunService carRunServiceImpl() { return new CarRunServiceImpl(); } } @Configuration @AutoConfigureBefore(MyConfiguration.class) public class MyConfiguration2 { @Bean public RunService trainRunServiceImpl() { return new TrainRunServiceImpl(); } }
注意:
如果要开启@EnableAutoConfiguration需要在META-INF/spring.factories文件中添加如下内容:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ xxx.configuration.MyConfiguration2,\ xxx.configuration.MyConfiguration
结论
我们需要控制目标bean的加载顺序即可。
但是我们在实际的使用一些通用plugin过程中(如redis),并没有刻意的指定bean的加载顺序,这是为什么呢?
因为:在实际的应用过程中,我们使用第三方插件,他们的默认配置都会存在于插件的jar包中,而我们的个性化配置则存在于自身的应用中。
而容器会优先执行classes/,然后才执行jars/classes.
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。