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 {@AutowiredDataService 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{@Overridepublic void deleteStudent(int id) {log.info("delete student info maintained by oracle");}
    }
    
  • 截止目前,运行并测试程序是毫无问题的。但是需求往往是源源不断的,某天我们可能接到节约成本的需求,希望把一些部分非核心的业务从 Oracle 迁移到社区版 Cassandra,所以我们自然会先添加上一个新的 DataService 实现,代码如下:
    @Repository
    @Slf4j
    public class CassandraDataService implements DataService{@Overridepublic 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"的DependencyDescriptorvalue = 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;}// Fallbackfor (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.mzph.cn/bicheng/65824.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

李宏毅机器学习笔记-Transformer

目录 1. Seq2seq 2. encoder Transformer 中的 Block 结构 3. Decoder 4.Encoder和Decoder间的信息传递 5.Training 6.Tips 1. Seq2seq Transformer 是一个seq2seq的model。Seq2seq指的是input是一个序列&#xff0c;输出也是一个序列&#xff0c;输出的长度是由机器自己…

es 3期 第20节-运用指标聚合快速统计数值

#### 1.Elasticsearch是数据库&#xff0c;不是普通的Java应用程序&#xff0c;传统数据库需要的硬件资源同样需要&#xff0c;提升性能最有效的就是升级硬件。 #### 2.Elasticsearch是文档型数据库&#xff0c;不是关系型数据库&#xff0c;不具备严格的ACID事务特性&#xff…

基于Docker的ETCD分布式集群

目录 1. 说明 2. 配置表 3. 步骤 3.1 放行端口 3.2 docker-compose 文件 3.3 部署到3台服务器 3.4 相关命令 4. 参考 1. 说明 - 以docker容器方式实现ETCD分布式集群&#xff0c;为其他项目提供支持&#xff0c;经过反复试验成功部署(网上资料大都过期或部署失败)。 -…

LeetCode 力扣 热题 100道(二十七)除自身以外数组的乘积(C++)

给你一个整数数组 nums&#xff0c;返回 数组 answer &#xff0c;其中 answer[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积 。 题目数据 保证 数组 nums之中任意元素的全部前缀元素和后缀的乘积都在 32 位 整数范围内。 请 不要使用除法&#xff0c;且在 O(n) 时间复杂…

DSMM数据安全能力成熟度评估--学习笔记(1)

目录 DSMM是什么&#xff1f; 1、能力成熟度等级评估流程 2、DSMM评估方式&#xff1f; 3、能力成熟度模型使用方法 4、 安全能力维度 4.1 能力构成 4.2 组织建设 4.3 制度流程 4.4 技术工具 4.5 人员能力 5、 能力成熟度等级维度 6、 数据安全过程维度 数据安全能…

【Go】:Sentinel 动态数据源配置指南

前言 在现代微服务架构中&#xff0c;流量控制是确保系统高可用性和稳定性的关键。Sentinel 是一款由阿里巴巴开源的流量控制组件&#xff0c;它不仅支持熔断降级和流量整形&#xff0c;还能通过动态数据源&#xff08;如本地文件或 Nacos&#xff09;加载规则&#xff0c;从而…

STM32学习之通信协议1 USART

时钟特性&#xff0c;比如发送一个波形高电平&#xff0c;然后低电平&#xff0c;接收方怎么知道是1、0还是1、1、0、0 &#xff1f; 这就需要有一个时钟信号来告诉接收方什么时候需要采集数据。时钟特性分为同步和异步。 I2C和SPI有单独的时钟线&#xff0c;所以它们是同步的&…

Arduino 小白的 DIY 空气质量检测仪(3)- TVOC模块、CO2模块

接上回 逐个分析 m_TVOC.h #include <Arduino.h>#include <SoftI2C.h>#include "DFRobot_SGP40.h"// TVOC指数 // 型号&#xff1a;sgp_40 // 接口&#xff1a;VCC->VCC(5V)、GND->GND、SDA->D6、SCL->D7、WAK->GND // 协议&#xff1…

主数据驱动的数据治理框架

目录 数据资产分类 数据治理的重要性 数据质量框架的4个方面 数据治理的核心是加强对数据资产的管控,通过深化数据服务,持续创造价值。数据治 理是在数据资产价值创造的过程中,治理团队对数据资产管理的评价、指导、控制,如下图所示。 数据资产分类 数据资产划分成为主数据…

【大模型实战篇】LLaMA Factory微调ChatGLM-4-9B模型

1. 背景介绍 虽然现在大模型微调的文章很多&#xff0c;但纸上得来终觉浅&#xff0c;大模型微调的体感还是需要自己亲自上手实操过&#xff0c;才能有一些自己的感悟和直觉。这次我们选择使用llama_factory来微调chatglm-4-9B大模型。 之前微调我们是用两块3090GPU显卡&…

AnnotationConfigApplicationContext流程看实例化的beanPostProcessor

接上一篇https://blog.csdn.net/qq_26437925/article/details/144865082 探究一下beanPostProcessors怎么加进来的 目录 首先register(annotatedClasses);后添加了重要的beanDefinition: ConfigurationClassPostProcessorinvokeBeanFactoryPostProcessors(beanFactory);执行后…

pip 下载安装时使用国内源配置

pip 是 Python 的包管理工具&#xff0c;用于安装和管理第三方库。然而&#xff0c;在某些情况下&#xff0c;默认的 PyPI&#xff08;Python Package Index&#xff09;源可能由于网络原因导致下载速度慢或者连接不稳定。幸运的是&#xff0c;我们可以轻松地配置 pip 使用国内…

LinuxC高级day4

作业: 1.思维导图 2.终端输入一个C源文件名(.c结尾)判断文件是否有内容&#xff0c;如果没有内容删除文件&#xff0c;如果有内容编译并执行改文件。 3.终端输入两个文件名&#xff0c;判断哪个文件的时间戳更新

RuoYi-Vue从http升级为https(Jar+Nginx)

一、前提条件 1.已通过数字证书管理服务控制台签发证书。 2.SSL证书绑定的域名已完成DNS解析,即域名与主机IP地址相互映射。 附:阿里云网站运维检测平台 3.已在Web服务器开放443端口(HTTPS通信的标准端口)。 如果使用的是阿里云ECS服务器,请确保已经在安全组规则入方向…

OpenCV相机标定与3D重建(39)计算校正畸变后的新相机内参矩阵函数getOptimalNewCameraMatrix()的使用

操作系统&#xff1a;ubuntu22.04 OpenCV版本&#xff1a;OpenCV4.9 IDE:Visual Studio Code 编程语言&#xff1a;C11 算法描述 根据自由缩放参数返回新的相机内参矩阵。 cv::getOptimalNewCameraMatrix 是 OpenCV 库中的一个函数&#xff0c;用于计算校正畸变后的新相机内…

OkHttp深度解析:请求流程、分发器机制、拦截器工作及TCP连接复用

目录 一、OkHttp请求的请求流程是怎样的? 二、OkHttp分发器是怎样工作的? 2.1 请求队列与线程池维护 2.2 异步请求处理 2.3 同步请求处理 2.4 请求执行 2.5 请求完成处理&#xff1a; 2.6 线程池设计 三、OkHttp拦截器是如何工作的? 四、应用拦截器和网络拦截器的…

云计算课程报告实验-WordCount算法实验 过程记录

内容描述 本实验指导书通过在华为鲲鹏上&#xff0c;编译运行WordCount程序。完成实验操作后&#xff0c;读者会掌握简单的程序编写&#xff0c;如WordCount中的getWords、countWords、treeMerge。 实验环境 华为鲲鹏云主机、openEuler 20.03操作系统&#xff1b;安装mpich-3…

2018年全国硕士研究生入学统一考试管理类专业学位联考英语(二)试题-纯享题目版

&#x1f3e0;个人主页&#xff1a;fo安方的博客✨ &#x1f482;个人简历&#xff1a;大家好&#xff0c;我是fo安方&#xff0c;目前中南大学MBA在读&#xff0c;也考取过HCIE Cloud Computing、CCIE Security、PMP、CISP、RHCE、CCNP RS、PEST 3等证书。&#x1f433; &…

kafka使用常见问题

连接不上kafka,报下边的错 org.apache.kafka.common.KafkaException: Producer is closed forcefully.at org.apache.kafka.clients.producer.internals.RecordAccumulator.abortBatches(RecordAccumulator.java:760) [kafka-clients-3.0.2.jar:na]at org.apache.kafka.client…

Jenkins(持续集成与自动化部署)

Jenkins 是一个开源软件项目&#xff0c;是基于Java开发的一种持续集成工具。 官网&#xff1a;https://www.jenkins.io/ GitLab安装使用 安装前提&#xff1a;内存至少需要4G 官方网站&#xff1a;https://about.gitlab.com/ 安装文档&#xff1a;https://docs.gitlab.c…