关于ConditionalOnMissingBean失效问题的追踪

目录
  • 现场回放
    • services
    • 操作类
    • configuration
    • 抛出异常
  • 问题定位
    • 工作原理
    • 问题出在哪?
    • 解决问题
    • 结论

遇到一个@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.

以上为个人经验,希望能给大家一个参考,也希望大家多多支持我们。

(0)

相关推荐

  • SpringBoot 自动配置失效的解决方法

    目录 问题描述 @EnableConfigurationProperties 注解行为 配置有效,AutoTestConfiguration 未刷新 prefix-type @ConditionalOnProperty @ConditionalOnProperty match 逻辑 @ConditionalOnProperty skip 逻辑 总结 本文源自近期项目中遇到的问题, bug 总是出现在你自以为是的地方... 问题描述 下面是一个简单复现的代码片段,在你没有阅读完本文时,如果能做出正

  • springboot @ConditionalOnMissingBean注解的作用详解

    @ConditionalOnMissingBean,它是修饰bean的一个注解,主要实现的是,当你的bean被注册之后,如果而注册相同类型的bean,就不会成功,它会保证你的bean只有一个,即你的实例只有一个,当你注册多个相同的bean时,会出现异常,以此来告诉开发人员. 代码演示 @Component public class AutoConfig { @Bean public AConfig aConfig() { return new AConfig("lind"); } @B

  • Spring条件注解@Conditional示例详解

    前言 @Conditional是Spring4新提供的注解,它的作用是根据某个条件创建特定的Bean,通过实现Condition接口,并重写matches接口来构造判断条件.总的来说,就是根据特定条件来控制Bean的创建行为,这样我们可以利用这个特性进行一些自动的配置. 本文将分为三大部分,@Conditional源码的介绍.Condition的使用示例和@Conditional的扩展注解的介绍. 一.@Conditional的源码 @Target({ElementType.TYPE, Elem

  • 关于ConditionalOnMissingBean失效问题的追踪

    目录 现场回放 services 操作类 configuration 抛出异常 问题定位 工作原理 问题出在哪? 解决问题 结论 遇到一个@ConditionalOnMissingBean失效的问题,今天花点时间来分析一下. 现场回放 services 首先介绍下代码结构:有RunService,以及它的两个实现类:TrainRunServiceImpl和CarRunServiceImpl RunService public interface RunService {     void run

  • 常用的Git便捷操作合集

    目录 1.Fork出来的Git仓库同步代码 2.合并多个提交 3.代码回退 4.使用worktree 5.其它 1.Fork出来的Git仓库同步代码 背景:有的时候从原仓库fork出了一个新仓库,这个新仓库做了自己的修改.可是原仓库也进行了更新,比如修复了bug,增加了新特性之类的.这个时候想要把原仓库代码同步过来. 原理:把原仓库的代码拉到本地,然后通过git merge把原仓库分支代码合到自己的分支代码. 1.先拉取原仓库代码到本地 git remote add upstream (填写你仓

  • 全网最深分析SpringBoot MVC自动配置失效的原因

    前言 本来没有计划这一篇文章的,只是在看完SpringBoot核心原理后,突然想到之前开发中遇到的MVC自动失效的问题,虽然网上有很多文章以及官方文档都说明了原因,但还是想亲自看一看,本以为很简单的事情,没想到却引发出一个较复杂的问题,请教了很多人都没有得到结果,网上文章也没有写清楚的,最后还是自己搞了很久才弄明白的,此篇主要记录自己的一个分析过程. 正文 引出问题 上面是SpringBoot MVC的自动配置,问题是这样的,当我们需要自己配置MVC时,有三种选择: 实现WebMvcConfig

  • 如何追踪入侵者

    在区域网路上可能你听过所谓「广播模式」的资料发送方法,此种方法不指定收信站,只要和此网路连结的所有网路设备皆为收信对象.但是这仅仅在区域网路上能够实行,因为区域网路上的机器不多(和Internet比起来).如果想是Internet上有数千万的主机,根本就不可能实施资料广播(至于IP  Multi cast算是一种限定式广播Restricted  Broadcast,唯有被指定的机器会收到,Internet上其他电脑还是不会收到).假设Internet上可以实施非限定广播,那随便一个人发出广播讯息

  • springboot配置多数据源后mybatis拦截器失效的解决

    目录 1. 解析配置文件初始化数据源 2. 定义数据源枚举类型 3. TheadLocal保存数据源类型 4. 自定义sqlSessionProxy 5. 自定义路由 6. 定义切面,dao层定义切面 7. 最后在写库增加事务管理 8. 在配置文件中增加数据源配置 配置文件是通过springcloudconfig远程分布式配置.采用阿里Druid数据源.并支持一主多从的读写分离.分页组件通过拦截器拦截带有page后缀的方法名,动态的设置total总数. 1. 解析配置文件初始化数据源 @Conf

  • mybatis的坑-integer类型为0的数据if test失效问题

    integer类型为0的数据if test失效 mybatis的where动态判断语句if test 遇到tinyint类型为0的数据失效 发现一个mybatis的坑,有个支付表,通过状态去筛选已支付/未支付的数据,支付状态用status字段表示,status=0表示未支付,status=1表示已支付,且status类型为Integer.当选择已支付即status=1时,可以筛选成功已支付的数据列表,但是当选择未支付即status=0时,查出来的数据是未支付和已支付的所有数据. 此时就有点懵逼了

  • SpringBoot自定义/error路径失效的解决

    目录 SpringBoot自定义/error路径失效 背景 配置信息 解决思路 SpringBoot的/error的自定义处理 它的具体定义如下 他有三个方法 SpringBoot自定义/error路径失效 背景 最近使用SpringBoot做controller统一异常处理的时候,配置好映射路径(/error),使用SpringBoot自带的异常通知注解@ControllerAdvice配置好异常处理类,按理说在Controller发生异常的时候重定向到自定义错误页面(这里是重定向到Sprin

  • 解决mybatis分页插件PageHelper导致自定义拦截器失效

    目录 问题背景 mybatis拦截器使用 使用方法: 注解参数介绍: setProperties方法 bug内容: 自定义拦截器部分代码 PageInterceptor源码: 解决方法: 解决方案一 调整执行顺序 解决方案二 修改拦截器注解定义 问题背景 在最近的项目开发中遇到一个需求 需要对mysql做一些慢查询.大结果集等异常指标进行收集监控,从运维角度并没有对mysql进行统一的指标搜集,所以需要通过代码层面对指标进行收集,我采用的方法是通过mybatis的Interceptor拦截器进行

  • Java Session会话追踪原理深入分析

    目录 一.会话技术 二.Session 1.原理 2.特点 3.获得一个会话对象 4.Session常见方法 三.Cookie和Session的区别 一.会话技术 客户端和服务器通信的过程中,自然而然的会产生一些数据交互.比如,A用户登录了邮箱,那么web服务器该怎么知道C一段时间后的登录状态呢?虽然HttpServletRequest对象和ServletContext对象都可以保存数据,但是不适用于这种情况. 客户端的每次请求,服务器都会产生一个HttpServletRequest对象,该对象

随机推荐