@Autowired註解 --required a single bean, but 2 were found出現的原因以及解決方法

来源:https://www.cnblogs.com/lin0/archive/2022/08/10/16570856.html
-Advertisement-
Play Games

@Autowired註解是spring用來支持依賴註入的核心利器之一,但是我們或多或少都會遇到required a single bean, but 2 were found(2可能是其他數字)的問題,接下來我們從源碼的角度去看為什麼會出現這個問題,以及這個問題的解法是什麼? 首先我們寫一個demo ...


@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


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 一、誕生時間 1995年 二、JavaScript包含內容 ECMAScript:基本語法及相關對象es(包括我們瞭解的es3,es5,es6.es7...) DOM:文檔對象模型,用來操作HTML BOM:瀏覽器對象模型,用來操作瀏覽器 三、書寫方式 1、書寫在script標簽里(比較常用) 2、 ...
  • margin:auto為什麼不垂直居中 margin:auto是具有強烈計算意味的關鍵字,用來計算元素對應方向上應該獲得的剩餘空間大小。 行內元素margin:auto; 不能水平居中在一行的中央位置(行內元素不獨占一行)。 position定位屬性大家都不會陌生,添加position屬性的元素可以 ...
  • vivo 互聯網前端團隊-Yang Kun 一、背景 在團隊中,我們因業務發展,需要用到桌面端技術,如離線可用、調用桌面系統能力。什麼是桌面端開發?一句話概括就是:以 Windows 、macOS 和 Linux 為操作系統的軟體開發。對此我們做了詳細的技術調研,桌面端的開發方式主要有 Native ...
  • 蒼穹之邊,浩瀚之摯,眰恦之美; 悟心悟性,善始善終,惟善惟道! —— 朝槿《朝槿兮年說》 寫在開頭 作為一名Java Developer,我們都清楚地知道,主要從搭載Linux系統上的伺服器程式來說,使用Java編寫的是”單進程-多線程"程式,而用C++語言編寫的,可能是“單進程-多線程”程式,“多 ...
  • 技術 Leader 是一個對綜合素質要求非常高的崗位,不僅要有解具體技術問題的架構能力,還要具備團隊管理的能力,更需要引領方向帶領團隊/平臺穿越迷茫進階到下一個境界的能力。所以通常來說技術 Leader 的技能是虛實結合的居多,繁雜的工作偏多。為此我把自己在工作中經常用到的思考技巧也做了一個整理。 ...
  • 社交是一種永恆的需求,既有生存層面的必要,也有情感上的渴求。而隨著互聯網開始統治這個時代,社交被搬到了網上,並且越來越成為主流,社交也在發展成互聯網產品的一個重要賽道。本文將介紹Soul是如何破解Z世代社交密碼的。 文章目錄 01 年輕人的社交密碼 02 為什麼對年輕人來說,Soul是那個對的產品? ...
  • 統一術語(戰略設計) 我們將通過DDD完成業務與技術的完整落地 統一 領域模型術語 DDD模式名稱 技術 技術設計術語 技術術語 技術設計模式 業務 領域模型術語 DDD模式名稱 業務術語 設計無關的業務術語 清晰的事件流 DDD 領域驅動設計是一個有關軟體開發的方法論,它提出基於領域開發的開發模式 ...
  • 3、ElasticSearch搜索結果處理 3.1、排序 Elasticsearch預設是根據相關度算分(_score)來排序,但是也支持自定義方式對搜索結果排序,可以排序的欄位類型有如下幾種 keyword類型 數值類型 地理坐標類型 日期類型 ... 3.1.1、普通欄位排序 keyword、數 ...
一周排行
    -Advertisement-
    Play Games
  • 1、預覽地址:http://139.155.137.144:9012 2、qq群:801913255 一、前言 隨著網路的發展,企業對於信息系統數據的保密工作愈發重視,不同身份、角色對於數據的訪問許可權都應該大相徑庭。 列如 1、不同登錄人員對一個數據列表的可見度是不一樣的,如數據列、數據行、數據按鈕 ...
  • 前言 上一篇文章寫瞭如何使用RabbitMQ做個簡單的發送郵件項目,然後評論也是比較多,也是準備去學習一下如何確保RabbitMQ的消息可靠性,但是由於時間原因,先來說說設計模式中的簡單工廠模式吧! 在瞭解簡單工廠模式之前,我們要知道C#是一款面向對象的高級程式語言。它有3大特性,封裝、繼承、多態。 ...
  • Nodify學習 一:介紹與使用 - 可樂_加冰 - 博客園 (cnblogs.com) Nodify學習 二:添加節點 - 可樂_加冰 - 博客園 (cnblogs.com) 介紹 Nodify是一個WPF基於節點的編輯器控制項,其中包含一系列節點、連接和連接器組件,旨在簡化構建基於節點的工具的過程 ...
  • 創建一個webapi項目做測試使用。 創建新控制器,搭建一個基礎框架,包括獲取當天日期、wiki的請求地址等 創建一個Http請求幫助類以及方法,用於獲取指定URL的信息 使用http請求訪問指定url,先運行一下,看看返回的內容。內容如圖右邊所示,實際上是一個Json數據。我們主要解析 大事記 部 ...
  • 最近在不少自媒體上看到有關.NET與C#的資訊與評價,感覺大家對.NET與C#還是不太瞭解,尤其是對2016年6月發佈的跨平臺.NET Core 1.0,更是知之甚少。在考慮一番之後,還是決定寫點東西總結一下,也回顧一下.NET的發展歷史。 首先,你沒看錯,.NET是跨平臺的,可以在Windows、 ...
  • Nodify學習 一:介紹與使用 - 可樂_加冰 - 博客園 (cnblogs.com) Nodify學習 二:添加節點 - 可樂_加冰 - 博客園 (cnblogs.com) 添加節點(nodes) 通過上一篇我們已經創建好了編輯器實例現在我們為編輯器添加一個節點 添加model和viewmode ...
  • 前言 資料庫併發,數據審計和軟刪除一直是數據持久化方面的經典問題。早些時候,這些工作需要手寫複雜的SQL或者通過存儲過程和觸發器實現。手寫複雜SQL對軟體可維護性構成了相當大的挑戰,隨著SQL字數的變多,用到的嵌套和複雜語法增加,可讀性和可維護性的難度是幾何級暴漲。因此如何在實現功能的同時控制這些S ...
  • 類型檢查和轉換:當你需要檢查對象是否為特定類型,並且希望在同一時間內將其轉換為那個類型時,模式匹配提供了一種更簡潔的方式來完成這一任務,避免了使用傳統的as和is操作符後還需要進行額外的null檢查。 複雜條件邏輯:在處理複雜的條件邏輯時,特別是涉及到多個條件和類型的情況下,使用模式匹配可以使代碼更 ...
  • 在日常開發中,我們經常需要和文件打交道,特別是桌面開發,有時候就會需要載入大批量的文件,而且可能還會存在部分文件缺失的情況,那麼如何才能快速的判斷文件是否存在呢?如果處理不當的,且文件數量比較多的時候,可能會造成卡頓等情況,進而影響程式的使用體驗。今天就以一個簡單的小例子,簡述兩種不同的判斷文件是否... ...
  • 前言 資料庫併發,數據審計和軟刪除一直是數據持久化方面的經典問題。早些時候,這些工作需要手寫複雜的SQL或者通過存儲過程和觸發器實現。手寫複雜SQL對軟體可維護性構成了相當大的挑戰,隨著SQL字數的變多,用到的嵌套和複雜語法增加,可讀性和可維護性的難度是幾何級暴漲。因此如何在實現功能的同時控制這些S ...