@Autowired注解是spring用来支持依赖注入的核心利器之一,但是我们或多或少都会遇到required a single bean, but 2 were found(2可能是其他数字)的问题,接下来我们从源码的角度去看为什么会出现这个问题,以及这个问题的解法是什么?

首先我们写一个demo来复现一下这个问题。首先我们有一个抽象类AbstractAutowiredDemo,两个实现类AutowiredDemo1,AutowiredDemo2。然后我们在AutowiredDemoController中通过@Autowired依赖注入AbstractAutowiredDemo。

@RestController
public class AutowiredDemoController { @Autowired
private AbstractAutowiredDemo abstractAutowiredDemo;
} @Component
public abstract class AbstractAutowiredDemo {
public abstract String print();
} @Component
public class AutowiredDemo2 extends AbstractAutowiredDemo {
@Override
public String print() {
return "AutowiredDemo2";
}
} @Component
public class AutowiredDemo1 extends AbstractAutowiredDemo {
@Override
public String print() {
return "AutowiredDemo1";
}
}

此时我们启动项目就会出现如下报错,找到了两个,并且列出了找到的两个其实就是抽象类的实现类。

接下来,我们从源码的角度来看看,spring是如何查找依赖并注入的。

与之前查看@Component注解方法一致,我们全局搜索Autowired,会找到一个叫做AutowiredAnnotationBeanPostProcessor,根据命名AutowiredAnnotationXXX我们可以大概知道这个类是用来处理注解@Autowired的。

进入AutowiredAnnotationBeanPostProcessor,从注释上我们可以知道这个类可以处理注解@Autowired,@Value以及如果支持的话还有@Inject,这里我们就只用关注@Autowired就行了,其他的以后再看,并且在无参构造器中有设置支持这些类型。

然后开始进入正题,我们开始真正去看,spring是如何处理,如何查找依赖并注入的,但是我们的主线任务是为什么会出现上面的错误,这样有目的的看,先抛开其他细节,要相对容易一些。

这里可以是一个看源代码的技巧,之前的ComponentScanAnnotationParser很简单,里面只有一个parse方法,我们知道就看它,但是在AutowiredAnnotationBeanPostProcessor这个里面,这么多方法,我们应该看什么呢?首先我们要的是处理注解的方法,应该是提供出去的方法,所以应该是个pubilic方法,(我们平时编码的时候也应该是这个习惯,往外提供的public方法应该放在前面,protect,peivate这种往后面放,因为作用域越小通用性越低,用到的概率越小)。而前面的几个public方法都是在set属性值,所以排除掉,然后跟着两个看命名是跟bean定义有关的,一个是合并,一个是重置,可以暂时排出掉,然后跟着是个决定使用哪个构造器的,应该是找到bean然后实例化时候用的,接下来就是一个后置处理属性的,而我们的@Autowired就是注解在属性字段上,这里我们多看一步,看看方法的实现,有Injection of autowired dependencies字样,并且根据命名有先查元数据,再注入的过程,猜测是这个方法。

接下来着重看AutowiredAnnotationBeanPostProcessor.postProcessProperties这个方法。

首先第一行,看方法名是在查找要注入的元数据。

进入方法AutowiredAnnotationBeanPostProcessor.findAutowiringMetadata,我们可以看到这段代码是先判断cache中是否已经有了,并且是否需要刷新(刷新其实就是为空或者类型不是clazz,可以自行点进去查看),不需要直接返回,需要就开始加锁(加锁之后又进行了一次校验,双重校验,小知识点,避免在加锁的过程中,已经put进去),再进行构建元数据buildAutowiringMetadata

进入方法AutowiredAnnotationBeanPostProcessor.buildAutowiringMetadata,看第一个判断,记不记得刚开始讲的,这个类可以处理的注解类型,这里就在判断,我们的@Autowired是肯定在其中的,然后中间又个do...while循环,将当前类以及它的父类被注解了字段,方法放入elements中,最终返回一个InjectionMetadata对象,并且设置了它的targetClass为clazz,injectedElements为elements。现在相当于我们需要进行依赖注入的元数据找到了。

接下来开始注入过程,我们回到postProcessProperties方法,查看注入方法。

进入InjectionMetadata.inject方法,我们在上面找到的元数据这里就用到的,我们不管checkedElements,至少我们的injectedElements肯定是有的,在上一步查找元数据的时候,我们已经set进去了,接下来我们就继续往下走。

当我们继续进入inject方法的时候,我们发现注释上有一句话,this和getResourceToInject都需要覆盖这个方法,所以这个方法并不是我们需要的注入方法的实现。

点击左边向下箭头,我们可以发现两个实现方法,根据命名,一个处理field的,一个处理method的,显然我们这里需要的是处理field的。

进入AutowiredFieldElement.inject方法,我们看到他先判断了是否有缓存,我们这里假设就是第一次,没有缓存(缓存肯定也是之前加载进去的),这样我们就应该走的是else分支。

进入AutowiredFieldElement.inject.resolveFieldValue方法,我们可以看到,开头是在做一些准备工作,可以忽略,最后是在将查找到的缓存起来,我们也可以不看,重点就是try中的内容,解决依赖。

进入方法AutowireCapableBeanFactory.resolveDependency,我们需要找它的实现方法,点击左边向下箭头,可以看到两个实现方法,同样根据命名,红框内的很显然是用来处理bean的。

进入DefaultListableBeanFactory.resolveDependency方法,大概扫一眼,前面都是在判断descriptor.getDependencyType()这个的值是不是那些类的类型,很显然是我们自己定义的类,都不是这些类型,所以我们直接到最后一个else,else中第一句是如果是懒加载,就先不加载了,所以真正的逻辑在下面。(其实我们就是要找到解决依赖的方法,而spring方法命名都是见文知意的,所以我们可以先直接定位到下面,发现不对再说,这是看源码时候的一个思路)

进入DefaultListableBeanFactory.doResolveDependency方法,这里就是真正的查找依赖的核心了,接下来我们仔细分析一下。

Step1:通过descriptor.resolveShortcut(this)返回shortcut,我们点进这个方法查看注释可以发现,这是用来做一些预先解析的,一般是spring自用的,我们如果没有特殊设置,一般不会用到,所以这个shortcut应该为null,方法不会返回。

Step2:通过getAutowireCandidateResolver().getSuggestedValue(descriptor)返回value,点进方法查看,根据注释看,这个是给给定依赖建议默认值的,应该处理的是@Value。所以这里value为null,方法不返回。

Step3:通过resolveMultipleBeans返回multipleBeans,可以看到里面是在判断我们当前查找的依赖的类型是否符合哪些条件(stream或者集合类型,所以这个叫multi),而我们当前的type就是我们定义的抽象类,所以这里multipleBeans也为null,方法不返回。

Step4:通过findAutowireCandidates返回matchingBeans(其实看这个方法名,就是处理Autowired注解,查找候选者的),点进方法查看。

进入方法DefaultListableBeanFactory.findAutowireCandidates,首先第一行我们可以看到在查找候选者名称。

进入方法BeanFactoryUtils.beanNamesForTypeIncludingAncestors,我们可以看到这里又调用了一个方法,通过type获取beanNames,点进去看注释可以看到这里会获取当前类型的bean的名称(会排除抽象类,不再深入进去,可以自己点进去看),包括子类,其实看到这里应该大概猜出来了,我们通过上面的抽象类AbstractAutowiredDemo拿到了它的子类,所以报错里面出现的是子类AutowiredDemo1和AutowiredDemo2。接着中间一段可能会查出更多的,但是这里我们不关心了,现在我们直接返回,此时String[]应该包含两个元素。

回到方法DefaultListableBeanFactory.findAutowireCandidates,我们可以发现,result中至少有两个元素,下面的for都是在里面继续add,这里我们不再看,继续往外走。

回到方法DefaultListableBeanFactory.doResolveDependency,matchingBeans中至少会有两个元素,则会进入下面一个if,而在if里面第一个代码就是在决定到底选用哪个候选bean,这里也是我们解决这个问题的一个切入点。

进入DefaultListableBeanFactory.determineAutowireCandidate会发现它先找了是否设置primary,priority,都没有的话就循环,查看有没有已经加载了的或者就是当前,这个最终目的就是要决定一个候选作为依赖注入,但是我们的这个案例,很显然决定不了。

回到外面方法之后,因为@Aurowired的required默认就是为true,所以一定会进入这个if,返回一个找到不唯一的异常。

总结

@Autowired注解字段查找并注入依赖的过程可以概括为:找到需要依赖注入的字段,通过class类型查找可以注入的类(包括子类),决定注入类,注入。

所以要解决文章开始出现的问题,有两个办法:

1.在查找处规避,注入的时候指定是Demo1还是Demo2

2.在决定注入类处规避,通过注解@Primary或者@Priority

@Autowired注解 --required a single bean, but 2 were found出现的原因以及解决方法的更多相关文章

  1. Field amqpTemplate in * required a single bean, but 3 were found:

    Field amqpTemplate in * required a single bean, but 3 were found: Spring Boot 启动的时候报的错 使用Spring Boot ...

  2. 【记录】Field required a single bean, but 2 were found:

    重构遇到个小问题,记录下: 错误信息: *************************** APPLICATION FAILED TO START ************************ ...

  3. Field in required a single bean, but 2 were found:

    我在其他类注入的时候出现以下错误 @Autowired NodeAgentService nodeAgentService; 异常 Description: Field mibService in c ...

  4. Parameter 0 of method sqlSessionTemplate in org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration required a single bean, but 2 were found:

    Parameter 0 of method orderSqlSessionFactory in com.config.MultipleDBConfig required a single bean, ...

  5. Field baseMapper in com.baomidou.mybatisplus.extension.service.impl.ServiceImpl required a single bean, but xx were found:

    在学习使用 mybatis-plus 时,遇到一个奇怪的异常 如 代码一: 代码一: Error starting ApplicationContext. To display the conditi ...

  6. springboot多数据源启动报错:required a single bean, but 6 were found:

    技术群: 816227112 参考:https://stackoverflow.com/questions/43455869/could-not-autowire-there-is-more-than ...

  7. Spring多个数据源问题:DataSourceAutoConfiguration required a single bean, but * were found

    原因: @EnableAutoConfiguration 这个注解会把配置文件号中的数据源全部都自动注入,不会默认注入一个,当使用其他数据源时再调用另外的数据源. 解决方法: 1.注释掉这个注解 2. ...

  8. 在同一个类中,一个方法调用另外一个有注解(比如@Async,@Transational)的方法,注解失效的原因和解决方法

    参考原贴地址:https://blog.csdn.net/clementad/article/details/47339519 在同一个类中,一个方法调用另外一个有注解(比如@Async,@Trans ...

  9. 【转】在同一个类中,一个方法调用另外一个有注解(比如@Async,@Transational)的方法,注解失效的原因和解决方法

    参考 原文链接 @Transactional does not work on method level 描述 在同一个类中,一个方法调用另外一个有注解(比如@Async,@Transational) ...

随机推荐

  1. QTP——功能测试

    一.前言(课设目的及内容) QTP是quicktest Professional的简称,是一种自动测试工具.使用QTP的目的是想用它来执行重复的手动测试,主要是用于回归测试和测试同一软件的新版本.因此 ...

  2. 02-C高级编程

    Day01 笔记 1 typedef使用 1.1 起别名 - 简化struct关键字 1.2 区分数据类型 1.3 提高代码移植性 2 void使用 2.1 不可以利用void创建变量 无法给无类型变 ...

  3. ASP.NET MVC 处理管线模型

    MVC管道整体处理模型 1.在ASP.NET MVC处理管线中的第一站就是路由模块.当请求到达路由模块后,MVC框架就会根据Route Table中配置的路由模板来匹配当前请求以获得对应的contro ...

  4. Lucene开发实例:Lucene中文分词(转载)

    1.准备工作下载lucene 3.6.1 : http://lucene.apache.org/下载中文分词IK Analyzer: http://code.google.com/p/ik-analy ...

  5. 《HALCON数字图像处理》第五章笔记

    目录 第五章 图像运算 图像的代数运算 加法运算 图像减法 图像乘法 图像除法 图像逻辑运算(位操作) 图像的几何变换 图像几何变换的一般表达式 仿射变换 投影变换 灰度插值 图像校正 我在Gitee ...

  6. 技术分享 | app测试中常用的Android模拟器

    原文链接 Emulator Emualor 是 Android Studio 自带的模拟器,是官方提供的工具,Android 开发最常使用的就是这一款. 它功能非常齐全,电话本.通话等功能都可正常使用 ...

  7. java标识符 identifier

    1,标识符 --> 类名 方法名  变量名 常量名 接口名   为程序员自己命名的内容 main也是标识符但是不能修改 2, 命名规则 只能以   数字 字母(中文) 下划线 美元符号      ...

  8. 敲了几万行源码后,我给Mybatis画了张“全地图”

    作者:小傅哥 博客:https://bugstack.cn 沉淀.分享.成长,让自己和他人都能有所收获! 一.说说:"产"后感受 有人跟我说,手写Spring难,手写Mybatis ...

  9. 在VMware Workstation 16上安装Windows7虚拟机以及VMware tools安装失败解决方法

    安装VMware Workstation 16 搜素"VMware Workstation下载" 下载 VMware Workstation Pro 下载Windows7系统镜像 ...

  10. 简单ELK配置实现生产级别的日志采集和查询实践

    概述 生产问题 集群规模如何规划? 集群中节点角色如何规划? 集群之脑裂问题如何避免? 索引分片如何规划? 分片副本如何规划? 集群规划 准备条件 先估算当前系统的数据量和数据增长趋势情况. 现有服务 ...