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

@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";
    }
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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,首先第一行我们可以看到在查找候选者名称。
image-20220810015803016

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

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

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

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

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

总结

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

所以要解决文章开始出现的问题,有两个办法:
1.在查找处规避,注入的时候指定是Demo1还是Demo2
image-20220810022926244

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

posted @ 2022-08-10 02:45  Carol淋  阅读(1237)  评论(1编辑  收藏  举报