当前位置: 首页 > article >正文

Spring Bean required a single bean, but 2 were found,发现多个 Bean

问题复现

  • 在使用 @Autowired 时,不管你是菜鸟级还是专家级的 Spring 使用者,都应该制造或者遭遇过类似的错误:

required a single bean, but 2 were found

  • 顾名思义,我们仅需要一个 Bean,但实际却提供了 2 个(这里的“2”在实际错误中可能是其它大于 1 的任何数字)。
  • 为了重现这个错误,我们可以先写一个案例来模拟下。假设我们在开发一个学籍管理系统案例,需要提供一个 API 根据学生的学号(ID)来移除学生,学生的信息维护肯定需要一个数据库来支撑,所以大体上可以实现如下:
    @RestController
    @Slf4j
    @Validated
    public class StudentController {
        @Autowired
        DataService dataService;
    
        @RequestMapping(path = "students/{id}", method = RequestMethod.DELETE)
        public void deleteStudent(@PathVariable("id") @Range(min = 1,max = 100) int id){
            dataService.deleteStudent(id);
        };
    }
    
  • 其中 DataService 是一个接口,其实现依托于 Oracle,代码示意如下:
    public interface DataService {
        void deleteStudent(int id);
    }
    
    @Repository
    @Slf4j
    public class OracleDataService implements DataService{
        @Override
        public void deleteStudent(int id) {
            log.info("delete student info maintained by oracle");
        }
    }
    
  • 截止目前,运行并测试程序是毫无问题的。但是需求往往是源源不断的,某天我们可能接到节约成本的需求,希望把一些部分非核心的业务从 Oracle 迁移到社区版 Cassandra,所以我们自然会先添加上一个新的 DataService 实现,代码如下:
    @Repository
    @Slf4j
    public class CassandraDataService implements DataService{
        @Override
        public void deleteStudent(int id) {
            log.info("delete student info maintained by cassandra");
        }
    }
    
  • 实际上,当我们完成支持多个数据库的准备工作时,程序就已经无法启动了,报错如下:
    在这里插入图片描述
  • 很显然,上述报错信息正是我们这一小节讨论的错误,那么这个错误到底是怎么产生的呢?接下来我们具体分析下。

案例分析

  • 要找到这个问题的根源,我们就需要对 @Autowired 实现的依赖注入的原理有一定的了解。首先,我们先来了解下 @Autowired 发生的位置和核心过程。
  • 当一个 Bean 被构建时,核心包括两个基本步骤:
    • 执行 AbstractAutowireCapableBeanFactory#createBeanInstance 方法:通过构造器反射构造出这个 Bean,在此案例中相当于构建出 StudentController 的实例;
    • 执行 AbstractAutowireCapableBeanFactory#populate 方法:填充(即设置)这个 Bean,在本案例中,相当于设置 StudentController 实例中被 @Autowired 标记的 dataService 属性成员。
  • 在步骤 2 中,“填充”过程的关键就是执行各种 BeanPostProcessor 处理器,关键代码如下:
    protected void populateBean(String beanName, RootBeanDefinition mbd, @Nullable BeanWrapper bw) {
          //省略非关键代码
          for (BeanPostProcessor bp : getBeanPostProcessors()) {
             if (bp instanceof InstantiationAwareBeanPostProcessor) {
                InstantiationAwareBeanPostProcessor ibp = (InstantiationAwareBeanPostProcessor) bp;
                PropertyValues pvsToUse = ibp.postProcessProperties(pvs, bw.getWrappedInstance(), beanName);
              //省略非关键代码
             }
          }
       }   
    }
    
  • 在上述代码执行过程中,因为 StudentController 含有标记为 Autowired 的成员属性 dataService,所以会使用到 AutowiredAnnotationBeanPostProcessor(BeanPostProcessor 中的一种)来完成“装配”过程:找出合适的 DataService 的 bean 并设置给 StudentController#dataService。如果深究这个装配过程,又可以细分为两个步骤:
    • 寻找出所有需要依赖注入的字段和方法,参考 AutowiredAnnotationBeanPostProcessor#postProcessProperties 中的代码行:
      InjectionMetadata metadata = findAutowiringMetadata(beanName, bean.getClass(), pvs);
      
    • 根据依赖信息寻找出依赖并完成注入,以字段注入为例,参考 AutowiredFieldElement#inject 方法:
      @Override
      protected void inject(Object bean, @Nullable String beanName, @Nullable PropertyValues pvs) throws Throwable {
         Field field = (Field) this.member;
         Object value;
         //省略非关键代码
            try {
                DependencyDescriptor desc = new DependencyDescriptor(field, this.required);
               //寻找“依赖”,desc为"dataService"的DependencyDescriptor
               value = beanFactory.resolveDependency(desc, beanName, autowiredBeanNames, typeConverter);
            }
            
         }
         //省略非关键代码
         if (value != null) {
            ReflectionUtils.makeAccessible(field);
            //装配“依赖”
            field.set(bean, value);
         }
      }
      
  • 说到这里,我们基本了解了 @Autowired 过程发生的位置和过程。而且很明显,我们案例中的错误就发生在上述“寻找依赖”的过程中(上述代码的第 9 行),那么到底是怎么发生的呢?我们可以继续刨根问底。
  • 为了更清晰地展示错误发生的位置,我们可以采用调试的视角展示其位置(即 DefaultListableBeanFactory#doResolveDependency 中代码片段),参考下图:
    在这里插入图片描述
  • 如上图所示,当我们根据 DataService 这个类型来找出依赖时,我们会找出 2 个依赖,分别为 CassandraDataService 和 OracleDataService。在这样的情况下,如果同时满足以下两个条件则会抛出本案例的错误:
    • 调用 determineAutowireCandidate 方法来选出优先级最高的依赖,但是发现并没有优先级可依据。具体选择过程可参考 DefaultListableBeanFactory#determineAutowireCandidate
      protected String determineAutowireCandidate(Map<String, Object> candidates, DependencyDescriptor descriptor) {
         Class<?> requiredType = descriptor.getDependencyType();
         String primaryCandidate = determinePrimaryCandidate(candidates, requiredType);
         if (primaryCandidate != null) {
            return primaryCandidate;
         }
         String priorityCandidate = determineHighestPriorityCandidate(candidates, requiredType);
         if (priorityCandidate != null) {
            return priorityCandidate;
         }
         // Fallback
         for (Map.Entry<String, Object> entry : candidates.entrySet()) {
            String candidateName = entry.getKey();
            Object beanInstance = entry.getValue();
            if ((beanInstance != null && this.resolvableDependencies.containsValue(beanInstance)) ||
                  matchesBeanName(candidateName, descriptor.getDependencyName())) {
               return candidateName;
            }
         }
         return null;
      }
      
      • 如代码所示,优先级的决策是先根据 @Primary 来决策,其次是 @Priority 决策,最后是根据 Bean 名字的严格匹配来决策。如果这些帮助决策优先级的注解都没有被使用,名字也不精确匹配,则返回 null,告知无法决策出哪种最合适。
    • @Autowired 要求是必须注入的(即 required 保持默认值为 true),或者注解的属性类型并不是可以接受多个 Bean 的类型,例如数组、Map、集合。这点可以参考 DefaultListableBeanFactory#indicatesMultipleBeans 的实现:
      private boolean indicatesMultipleBeans(Class<?> type) {
         return (type.isArray() || (type.isInterface() &&
               (Collection.class.isAssignableFrom(type) || Map.class.isAssignableFrom(type))));
      }
      
  • 对比上述两个条件和我们的案例,很明显,案例程序能满足这些条件,所以报错并不奇怪。而如果我们把这些条件想得简单点,或许更容易帮助我们去理解这个设计。就像我们遭遇多个无法比较优劣的选择,却必须选择其一时,与其偷偷地随便选择一种,还不如直接报错,起码可以避免更严重的问题发生。

问题修正

  • 针对这个案例,有了源码的剖析,我们可以很快找到解决问题的方法:打破上述两个条件中的任何一个即可,即让候选项具有优先级或压根可以不去选择。不过需要你注意的是,不是每一种条件的打破都满足实际需求,例如我们可以通过使用标记 @Primary 的方式来让被标记的候选者有更高优先级,从而避免报错,但是它并不一定符合业务需求,这就好比我们本身需要两种数据库都能使用,而不是顾此失彼。
    @Repository
    @Primary
    @Slf4j
    public class OracleDataService implements DataService{
        //省略非关键代码
    }
    
  • 现在,请你仔细研读上述的两个条件,要同时支持多种 DataService,且能在不同业务情景下精确匹配到要选择到的 DataService,我们可以使用下面的方式去修改:
    @Autowired
    DataService oracleDataService;
    
  • 如代码所示,修改方式的精髓在于将属性名和 Bean 名字精确匹配,这样就可以让注入选择不犯难:需要 Oracle 时指定属性名为 oracleDataService,需要 Cassandra 时则指定属性名为 cassandraDataService。

http://www.kler.cn/a/461792.html

相关文章:

  • 在Ubuntu 18.04.6 LTS安装OpenFace流程
  • 自学记录鸿蒙API 13:实现多目标识别Object Detection
  • 智联视频超融合平台:电力行业的智能守护者
  • 游泳溺水识别数据集,对25729张图片进行YOLO,COCO JSON, VOC XML 格式的标注,溺水平均识别率在89.9%
  • CentOS7安装配置JDK保姆级教程(图文详解)
  • 库伦值自动化功耗测试工具
  • 深入浅出:事件监听中的适配器模式
  • 微信小程序调用 WebAssembly 烹饪指南
  • 25年开篇之作---动态规划系列<七> 01背包问题
  • Python机器学习笔记(十六、数据表示与特征工程-分类变量)
  • Linux隐藏登录和清除历史命令以及其他相关安全操作示例
  • 20241231 机器学习ML -(2)KNN(scikitlearn)
  • Selenium和WebDriver的安装与配置
  • TCP 链接与 HTTP 链接的区别
  • 二十三种设计模式-抽象工厂模式
  • 最大连续和(POJ2750)
  • Three.js教程006:物体的缩放与旋转
  • 创建flutter项目遇到无法连接源的问题
  • 计算机毕设-基于springboot的考研学习分享平台的设计与实现(附源码+lw+ppt+开题报告)
  • linux系统安装搭建chrony(ntp)时间同步服务器
  • 2024年终总结
  • 《Xsens动捕与人型机器人训练》讲座距离开讲仅剩9天
  • MongoDB的安装、启停和常用命令(五分钟入门)
  • 三、GIT与Github推送(上传)和克隆(下载)
  • 2024年度总结-考研-就业-其他可能-NEXT--..2025
  • 动手学深度学习-深度学习计算-2参数管理