springboot多數據源使用@Qualifier自動註入無效的解決

@Qualifier自動註入無效的解決

問題

使用springboot進行多數據源時,發生瞭單例DataSource對應多個DataSourceBean的問題。

具體錯誤如下:XXXXX required a single bean, but 3 were found。通過@Qualifier來區分,或是在@Bean中添加name屬性來區分,都沒有作用。

問題的根本原因

主要在於SpringBoot的DataSourceInitializer,該類在autoConfigure包中,用來自動初始化一個內置的DataSource實例,在創建該實例的時候發生瞭註入的問題。

創建實例的流程(記錄一下方便以後再次調試):

1. 調用DataSourceInitializer的構造方法

2. 調用AbstractAutowireCapableBeanFactory的applyMergedBeanDefinitionPostProcessors方法

3. 調用AbstractAutowireCapableBeanFactory的initializeBean方法

4. AbstractAutowireCapableBeanFactory的applyBeanPostProcessorsBeforeInitialization方法,其中有一個循環是用多種Bean處理器來處理DataSourceInitializer對象

5. 之後使用反射的方式跳轉到DataSourceInitializer的init方法

6.裡面通過this.applicationContext.getBean(DataSource.class)來獲取所有DataSource的實現類對象實例。

7. DefaultListableBeanFactory的resolveNamedBean方法中來選取實例對象,通過裡面的getBeanNamesForType方法獲取到所有的符合requireType(也就是DataSource.class)的對象。

8. 如果對象實例的實例數量大於1,則會進入以下的兩個判斷:

判斷是否有Primary的實例,或者是優先級高的實例對象,如果有,則將候選對象名賦值給candidateName。沒有則置為空,最後拋出多個實例的異常。

其中問題出在

因為項目中隻使用瞭@Qualifier,而且springboot的DataSourceInitializer沒有對@Qualifier的處理,所以沒有對實例進行匹配。

造成多個數據源實例。對於存在多數據源的情況,他們做的補救措施是在代碼中添加瞭是否是Primary和是否是HighestPriority的判斷,

來處理采用哪個數據源。所以解決的方式也因此出來瞭。就是將某個實例標記為Primary或者HighestPriority。

解決問題的方法

在某個@Bean上添加@Primary註解,就可以做到唯一區分。

這裡其實應該還可以通過優先級來區分,但使用@Order發現並不是這個優先級,也沒找到相關的資料,所以之後再研究一下。

@Qualifier的作用和應用

@Qualifier的作用

這是官方的介紹

This annotation may be used on a field or parameter as a qualifier for candidate beans when autowiring. It may also be used to annotate other custom annotations that can then in turn be used as qualifiers.

簡單的理解就是:

  • 在使用@Autowire自動註入的時候,加上@Qualifier(“test”)可以指定註入哪個對象;
  • 可以作為篩選的限定符,我們在做自定義註解時可以在其定義上增加@Qualifier,用來篩選需要的對象。這個理解看下面的代碼吧,不好解釋。

功能介紹

首先是對(1)的理解。

//我們定義瞭兩個TestClass對象,分別是testClass1和testClass2
//我們如果在另外一個對象中直接使用@Autowire去註入的話,spring肯定不知道使用哪個對象
//會排除異常 required a single bean, but 2 were found
@Configuration
public class TestConfiguration {
   @Bean("testClass1")
   TestClass testClass1(){
       return new TestClass("TestClass1");
   }
   @Bean("testClass2")
   TestClass testClass2(){
       return new TestClass("TestClass2");
   }
}

下面是正常的引用

@RestController
public class TestController {
 
    //此時這兩個註解的連用就類似 @Resource(name="testClass1")
    @Autowired
    @Qualifier("testClass1")
    private TestClass testClass;
 
    @GetMapping("/test")
    public Object test(){
        return testClassList;
    }

}

@Autowired和@Qualifier這兩個註解的連用在這個位置就類似 @Resource(name=“testClass1”)

對(2)的理解

@Configuration
public class TestConfiguration {
    //我們調整下在testClass1上增加@Qualifier註解
    @Qualifier
    @Bean("testClass1")
    TestClass testClass1(){
        return new TestClass("TestClass1");
    }
 
    @Bean("testClass2")
    TestClass testClass2(){
        return new TestClass("TestClass2");
    }
}
@RestController
public class TestController {
    //我們這裡使用一個list去接收testClass的對象
    @Autowired
    List<TestClass> testClassList= Collections.emptyList();
    
    @GetMapping("/test")
    public Object test(){
        return testClassList;
    }
}

我們調用得到的結果是

[
{
“name”: “TestClass1”
},
{
“name”: “TestClass2”
}
]

我們可以看到所有的testclass都獲取到瞭。接下來我們修改下代碼

@RestController
public class TestController {
 
    @Qualifier //我們在這增加註解
    @Autowired
    List<TestClass> testClassList= Collections.emptyList();
 
    @GetMapping("/test")
    public Object test(){
        return testClassList;
    }
}

和上面代碼對比就是在接收參數上增加瞭@Qualifier註解,這樣看是有什麼區別,我們調用下,結果如下:

[
{
“name”: “TestClass1”
}
]

返回結果隻剩下增加瞭@Qualifier註解的TestClass對象,這樣我們就可以理解官方說的標記篩選是什麼意思瞭。

另外,@Qualifier註解是可以指定value的,這樣我們可以通過values來分類篩選想要的對象瞭,這裡不列舉代碼瞭,感興趣的同學自己試試。

以上為個人經驗,希望能給大傢一個參考,也希望大傢多多支持WalkonNet。

推薦閱讀: