關(guān)于Spring的@Autowired依賴注入常見錯誤的總結(jié)
做不到雨露均沾
經(jīng)常會遇到,required a single bean, but 2 were found。
根據(jù)ID移除學生
DataService是個接口,其實現(xiàn)依賴Oracle:
現(xiàn)在期望把部分非核心業(yè)務(wù)從Oracle遷移到Cassandra,自然會先添加上一個新的DataService實現(xiàn):
@Repository @Slf4j public class CassandraDataService implements DataService{ @Override public void deleteStudent(int id) { log.info("delete student info maintained by cassandra"); } }
當完成支持多個數(shù)據(jù)庫的準備工作時,程序就已經(jīng)無法啟動了,報錯如下:
解析
當一個Bean被構(gòu)建時的核心步驟:
- 執(zhí)行AbstractAutowireCapableBeanFactory#createBeanInstance:通過構(gòu)造器反射出該Bean,如構(gòu)建StudentController實例
- 執(zhí)行AbstractAutowireCapableBeanFactory#populate:填充設(shè)置該Bean,如設(shè)置StudentController實例中被 @Autowired 標記的dataService屬性成員。
“填充”過程的關(guān)鍵就是執(zhí)行各種BeanPostProcessor處理器,關(guān)鍵代碼如下:
protected void populateBean(String beanName, RootBeanDefinition mbd, @Nullable BeanWrapper bw) { //省略非關(guān)鍵代碼 for (BeanPostProcessor bp : getBeanPostProcessors()) { if (bp instanceof InstantiationAwareBeanPostProcessor) { InstantiationAwareBeanPostProcessor ibp = (InstantiationAwareBeanPostProcessor) bp; PropertyValues pvsToUse = ibp.postProcessProperties(pvs, bw.getWrappedInstance(), beanName); //省略非關(guān)鍵代碼 } } } }
因為StudentController含標記為Autowired的成員屬性dataService,所以會使用到AutowiredAnnotationBeanPostProcessor完成“裝配”:找出合適的DataService bean,設(shè)置給StudentController#dataService。
裝配過程:
1.尋找所有需依賴注入的字段和方法:AutowiredAnnotationBeanPostProcessor#postProcessProperties
2.根據(jù)依賴信息尋找依賴并完成注入。比如字段注入,參考AutowiredFieldElement#inject方法:
@Override protected void inject(Object bean, @Nullable String beanName, @Nullable PropertyValues pvs) throws Throwable { Field field = (Field) this.member; Object value; // ... try { DependencyDescriptor desc = new DependencyDescriptor(field, this.required); // 尋找“依賴”,desc為"dataService"的DependencyDescriptor value = beanFactory.resolveDependency(desc, beanName, autowiredBeanNames, typeConverter); } } // ... if (value != null) { ReflectionUtils.makeAccessible(field); // 裝配“依賴” field.set(bean, value); } }
案例中的錯誤就發(fā)生在上述“尋找依賴”的過程中,DefaultListableBeanFactory#doResolveDependency
當根據(jù)DataService類型找依賴時,會找出2個依賴:
- CassandraDataService
- OracleDataService
在這樣的情況下,如果同時滿足以下兩個條件則會拋出本案例的錯誤:
- 調(diào)用determineAutowireCandidate方法來選出優(yōu)先級最高的依賴,但是發(fā)現(xiàn)并沒有優(yōu)先級可依據(jù)。具體選擇過程可參考
DefaultListableBeanFactory#determineAutowireCandidate: protected String determineAutowireCandidate(Map<String, Object> candidates, DependencyDescriptor descriptor) { Class<?> requiredType = descriptor.getDependencyType(); String primaryCandidate = determinePrimaryCandidate(candidates, requiredType); if (primaryCandidate != null) { return primaryCandidate; } String priorityCandidate = determineHighestPriorityCandidate(candidates, requiredType); if (priorityCandidate != null) { return priorityCandidate; } // Fallback for (Map.Entry<String, Object> entry : candidates.entrySet()) { String candidateName = entry.getKey(); Object beanInstance = entry.getValue(); if ((beanInstance != null && this.resolvableDependencies.containsValue(beanInstance)) || matchesBeanName(candidateName, descriptor.getDependencyName())) { return candidateName; } } return null; }
優(yōu)先級的決策是先根據(jù)@Primary,其次是@Priority,最后根據(jù)Bean名嚴格匹配。
如果這些幫助決策優(yōu)先級的注解都沒有被使用,名字也不精確匹配,則返回null,告知無法決策出哪種最合適。
@Autowired要求是必須注入的(required默認值true),或注解的屬性類型并不是可以接受多個Bean的類型,例如數(shù)組、Map、集合。
這點可以參考DefaultListableBeanFactory#indicatesMultipleBeans:
private boolean indicatesMultipleBeans(Class<?> type) { return (type.isArray() || (type.isInterface() && (Collection.class.isAssignableFrom(type) || Map.class.isAssignableFrom(type)))); }
案例程序能滿足這些條件,所以報錯并不奇怪。而如果我們把這些條件想得簡單點,或許更容易幫助我們?nèi)ダ斫膺@個設(shè)計。就像我們遭遇多個無法比較優(yōu)劣的選擇,卻必須選擇其一時,與其偷偷地隨便選擇一種,還不如直接報錯,起碼可以避免更嚴重的問題發(fā)生。
修正
打破上述兩個條件中的任何一個即可,即讓候選項具有優(yōu)先級或根本不選擇。
但并非每種條件的打破都滿足實際需求:
如可以通過使用**@Primary**讓被標記的候選者有更高優(yōu)先級,但并不一定符合業(yè)務(wù)需求,好比我們本身需要兩種DB都能使用,而非不可兼得。
@Repository @Primary @Slf4j public class OracleDataService implements DataService{ //省略非關(guān)鍵代碼 }
要同時支持多種DataService,不同情景精確匹配不同的DataService,可這樣修改:
@Autowired DataService oracleDataService;
將屬性名和Bean名精確匹配,就能實現(xiàn)完美的注入選擇:
- 需要Oracle時指定屬性名為oracleDataService
- 需要Cassandra時則指定屬性名為cassandraDataService
顯式引用Bean時首字母忽略大小寫
還有另外一種解決辦法,即采用@Qualifier顯式指定引用服務(wù),例如采用下面的方式:
@Autowired() @Qualifier("cassandraDataService") DataService dataService;
這樣能讓尋找出的Bean只有一個(即精確匹配),無需后續(xù)的決策過程:
DefaultListableBeanFactory#doResolveDependency
@Nullable public Object doResolveDependency(DependencyDescriptor descriptor, @Nullable String beanName, @Nullable Set<String> autowiredBeanNames, @Nullable TypeConverter typeConverter) throws BeansException { //省略其他非關(guān)鍵代碼 //尋找bean過程 Map<String, Object> matchingBeans = findAutowireCandidates(beanName, type, descriptor); if (matchingBeans.isEmpty()) { if (isRequired(descriptor)) { raiseNoMatchingBeanFound(type, descriptor.getResolvableType(), descriptor); } return null; } //省略其他非關(guān)鍵代碼 if (matchingBeans.size() > 1) { //省略多個bean的決策過程,即案例1重點介紹內(nèi)容 } //省略其他非關(guān)鍵代碼 }
使用 @Qualifier 指定名稱匹配,最終只找到唯一一個。但使用時,可能會忽略Bean名稱首字母大小寫。
如:
@Autowired @Qualifier("CassandraDataService") DataService dataService;
運行報錯:
Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'studentController': Unsatisfied dependency expressed through field 'dataService'; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'com.spring.puzzle.class2.example2.DataService' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true), @org.springframework.beans.factory.annotation.Qualifier(value=CassandraDataService)}
若未顯式指定 bean 名稱,默認就是類名,不過首字母小寫!
假設(shè)要支持SQLServer,定義了一個名為SQLServerDataService的實現(xiàn):
@Autowired @Qualifier("sQLServerDataService") DataService dataService;
依然出現(xiàn)之前錯誤,而若改成SQLServerDataService,則運行通過。
這真是瘋了呀!
顯式引用Bean時,首字母到底是大寫還是小寫?
答疑
raiseNoMatchingBeanFound(type, descriptor.getResolvableType(), descriptor);
當因名稱問題(例如引用Bean首字母搞錯了)找不到Bean,會拋NoSuchBeanDefinitionException。
不顯式設(shè)置名字的Bean,其默認名稱首字母到底是大寫還是小寫呢?
Spring Boot應(yīng)用會自動掃包,找出直接或間接標記了 @Component 的BeanDefinition。例如CassandraDataService、SQLServerDataService都被標記了@Repository,而Repository本身被@Component標記,所以都間接標記了@Component。
一旦找出這些Bean信息,就可生成Bean名,然后組合成一個個BeanDefinitionHolder返回給上層:
ClassPathBeanDefinitionScanner#doScan
BeanNameGenerator#generateBeanName產(chǎn)生Bean名,有兩種實現(xiàn)方式:
因為DataService實現(xiàn)都是使用注解,所以Bean名稱的生成邏輯最終調(diào)用的其實是
AnnotationBeanNameGenerator#generateBeanName
看Bean有無顯式指明名稱,若:
- 有
用顯式名稱
- 沒有
生成默認名稱
案例沒有給Bean指名,所以生成默認名稱,通過方法:
buildDefaultBeanName
首先,獲取一個簡短的ClassName,然后調(diào)用Introspector#decapitalize方法,設(shè)置首字母大寫或小寫,具體參考下面的代碼實現(xiàn):
- 一個類名是以兩個大寫字母開頭,則首字母不變
- 其它情況下默認首字母變成小寫
SQLServerDataService的Bean,其名稱應(yīng)該就是類名本身,而CassandraDataService的Bean名稱則變成了首字母小寫(cassandraDataService)。
修正
引用處修正
@Autowired @Qualifier("cassandraDataService") DataService dataService;
定義處顯式指定Bean名字,我們可以保持引用代碼不變,而通過顯式指明CassandraDataService 的Bean名稱為CassandraDataService來糾正這個問題。
@Repository("CassandraDataService") @Slf4j public class CassandraDataService implements DataService { //省略實現(xiàn) }
如果你不太了解源碼,不想糾結(jié)于首字母到底是大寫還是小寫,建議第二種方法
引用內(nèi)部類的Bean遺忘類名
這就能搞定所有Bean顯式引用不出 bug 嗎?
沿用上面案例,稍微再添加點別的需求,例如我們需要定義一個內(nèi)部類來實現(xiàn)一種新的DataService,代碼如下:
public class StudentController { @Repository public static class InnerClassDataService implements DataService{ @Override public void deleteStudent(int id) { //空實現(xiàn) } } // ... }
這時一般都用下面的方式直接去顯式引用這個Bean:
@Autowired @Qualifier("innerClassDataService") DataService innerClassDataService;
那直接采用首字母小寫,這樣就萬無一失了嗎?
仍報錯“找不到Bean”,why?
答疑
現(xiàn)在問題是“如何引用內(nèi)部類的Bean”。
在AnnotationBeanNameGenerator#buildDefaultBeanName,只關(guān)注了首字母是否小寫,而在最后變換首字母前,有這么一行處理 class 名稱的:
我們可以看下它的實現(xiàn):
ClassUtils#getShortName
假設(shè)是個內(nèi)部類,例如下面的類名:
com.javaedge.StudentController.InnerClassDataService
經(jīng)過該方法處理后,得到名稱:
StudentController.InnerClassDataService
最后經(jīng)Introspector.decapitalize
首字母變換,得到Bean名稱:
studentController.InnerClassDataService
所以直接使用 innerClassDataService 找不到想要的Bean。
修正
@Autowired @Qualifier("studentController.InnerClassDataService") DataService innerClassDataService;
總結(jié)
像第一個案例,同種類型的實現(xiàn),可能不是同時出現(xiàn)在自己的項目代碼中,而是有部分實現(xiàn)出現(xiàn)在依賴的類庫??磥硌芯吭创a的確能讓我們少寫幾個 bug!
到此這篇關(guān)于關(guān)于Spring的@Autowired依賴注入常見錯誤的總結(jié)的文章就介紹到這了,更多相關(guān)Spring @Autowired 依賴注入內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
MyBatis批量更新(update foreach)報錯問題
這篇文章主要介紹了MyBatis批量更新(update foreach)報錯問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-08-08SpringBoot讀取yml文件中配置數(shù)組的2種方法
這篇文章主要介紹了SpringBoot讀取yml文件中配置數(shù)組的2種方法,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-12-12Spring Security 密碼驗證動態(tài)加鹽的驗證處理方法
小編最近在改造項目,需要將gateway整合security在一起進行認證和鑒權(quán),今天小編給大家分享Spring Security 密碼驗證動態(tài)加鹽的驗證處理方法,感興趣的朋友一起看看吧2021-06-06SpringData @Query和@Modifying注解原理解析
這篇文章主要介紹了SpringData @Query和@Modifying注解原理解析,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下2020-08-08