@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. 个人冲刺(三)——体温上报app(二阶段)

    冲刺任务:完成用户类.温度数据和第二页面类的编写 User.java package com.example.helloworld; class User { private String usern ...

  2. SQL语言DDL

    MySQL数据库基本操作-DDL -- ctrl+/和# :注释 -- SQL语言不区分大小写: DDL:数据定义语言: 对数据库的常用操作: -- 查看所有的数据库: show databases; ...

  3. React中使用react-player 播放视频或直播

    业务中需要播放视频,寻来寻去,找到了react-player 初次点击,甚是眼熟,思来想去,竟是钉钉同款 如果使用react框架 先安装 npm install --save video-react ...

  4. CabloyJS一站式助力微信、企业微信、钉钉开发 - 钉钉篇

    前言 现在软件开发不仅要面对前端碎片化,还要面对后端碎片化.针对前端碎片化,CabloyJS提供了pc=mobile+pad的跨端自适应方案,参见:自适应布局:pc = mobile + pad 在这 ...

  5. Docker-Compose实现Mysql主从

    1. 简介 通过使用docker-compose 搭建一个主从数据库,本示例为了解耦 将两个server拆分到了两个compose文件中,当然也可以放到一个compose文件中 演示mysql版本:5 ...

  6. html关键字大全

    html标签属性大全 html标签属性大全从网上搜集整理的常用html标签,供朋友们交流学习html用. html标签<marquee> <marquee>...</ma ...

  7. MySQL-3-DML

    DML 数据操作语言 插入insert 语法一:insert into 表名(列名,...)values(值1,...): 语法二:insert into 表名 set 列名=值,列名=值,... 插 ...

  8. Spring框架系列(8) - Spring IOC实现原理详解之Bean实例化(生命周期,循环依赖等)

    上文,我们看了IOC设计要点和设计结构:以及Spring如何实现将资源配置(以xml配置为例)通过加载,解析,生成BeanDefination并注册到IoC容器中的:容器中存放的是Bean的定义即Be ...

  9. window下Redis快速启动,以及闪退问题解决

    我下载的是免安装版的window版redis,解压后如下: 第一步:在解压的redis文件夹下新建一个redis-start.bat(window启动一般都是xx.bat) 第二步:打开redis.w ...

  10. p_b_p_b 杂题选讲

    [ARC119F] AtCoder Express 3 [ARC117F] Gateau 考虑二分答案,对前缀和建差分约束 \(\text{check}\) ,但是用 \(\text{spfa}\) ...