關(guān)于Spring的@Autowired依賴(lài)注入常見(jiàn)錯(cuò)誤的總結(jié)
做不到雨露均沾
經(jīng)常會(huì)遇到,required a single bean, but 2 were found。
根據(jù)ID移除學(xué)生
DataService是個(gè)接口,其實(shí)現(xiàn)依賴(lài)Oracle:
現(xiàn)在期望把部分非核心業(yè)務(wù)從Oracle遷移到Cassandra,自然會(huì)先添加上一個(gè)新的DataService實(shí)現(xiàn):
@Repository
@Slf4j
public class CassandraDataService implements DataService{
@Override
public void deleteStudent(int id) {
log.info("delete student info maintained by cassandra");
}
}
當(dāng)完成支持多個(gè)數(shù)據(jù)庫(kù)的準(zhǔn)備工作時(shí),程序就已經(jīng)無(wú)法啟動(dòng)了,報(bào)錯(cuò)如下:

解析
當(dāng)一個(gè)Bean被構(gòu)建時(shí)的核心步驟:
- 執(zhí)行AbstractAutowireCapableBeanFactory#createBeanInstance:通過(guò)構(gòu)造器反射出該Bean,如構(gòu)建StudentController實(shí)例
- 執(zhí)行AbstractAutowireCapableBeanFactory#populate:填充設(shè)置該Bean,如設(shè)置StudentController實(shí)例中被 @Autowired 標(biāo)記的dataService屬性成員。
“填充”過(guò)程的關(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)鍵代碼
}
}
}
}
因?yàn)镾tudentController含標(biāo)記為Autowired的成員屬性dataService,所以會(huì)使用到AutowiredAnnotationBeanPostProcessor完成“裝配”:找出合適的DataService bean,設(shè)置給StudentController#dataService。
裝配過(guò)程:
1.尋找所有需依賴(lài)注入的字段和方法:AutowiredAnnotationBeanPostProcessor#postProcessProperties

2.根據(jù)依賴(lài)信息尋找依賴(lài)并完成注入。比如字段注入,參考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);
// 尋找“依賴(lài)”,desc為"dataService"的DependencyDescriptor
value = beanFactory.resolveDependency(desc, beanName, autowiredBeanNames, typeConverter);
}
}
// ...
if (value != null) {
ReflectionUtils.makeAccessible(field);
// 裝配“依賴(lài)”
field.set(bean, value);
}
}
案例中的錯(cuò)誤就發(fā)生在上述“尋找依賴(lài)”的過(guò)程中,DefaultListableBeanFactory#doResolveDependency

當(dāng)根據(jù)DataService類(lèi)型找依賴(lài)時(shí),會(huì)找出2個(gè)依賴(lài):
- CassandraDataService
- OracleDataService
在這樣的情況下,如果同時(shí)滿(mǎn)足以下兩個(gè)條件則會(huì)拋出本案例的錯(cuò)誤:
- 調(diào)用determineAutowireCandidate方法來(lái)選出優(yōu)先級(jí)最高的依賴(lài),但是發(fā)現(xiàn)并沒(méi)有優(yōu)先級(jí)可依據(jù)。具體選擇過(guò)程可參考
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í)的決策是先根據(jù)@Primary,其次是@Priority,最后根據(jù)Bean名嚴(yán)格匹配。
如果這些幫助決策優(yōu)先級(jí)的注解都沒(méi)有被使用,名字也不精確匹配,則返回null,告知無(wú)法決策出哪種最合適。
@Autowired要求是必須注入的(required默認(rèn)值true),或注解的屬性類(lèi)型并不是可以接受多個(gè)Bean的類(lèi)型,例如數(shù)組、Map、集合。
這點(diǎn)可以參考DefaultListableBeanFactory#indicatesMultipleBeans:
private boolean indicatesMultipleBeans(Class<?> type) {
return (type.isArray() || (type.isInterface() &&
(Collection.class.isAssignableFrom(type) || Map.class.isAssignableFrom(type))));
}
案例程序能滿(mǎn)足這些條件,所以報(bào)錯(cuò)并不奇怪。而如果我們把這些條件想得簡(jiǎn)單點(diǎn),或許更容易幫助我們?nèi)ダ斫膺@個(gè)設(shè)計(jì)。就像我們?cè)庥龆鄠€(gè)無(wú)法比較優(yōu)劣的選擇,卻必須選擇其一時(shí),與其偷偷地隨便選擇一種,還不如直接報(bào)錯(cuò),起碼可以避免更嚴(yán)重的問(wèn)題發(fā)生。
修正
打破上述兩個(gè)條件中的任何一個(gè)即可,即讓候選項(xiàng)具有優(yōu)先級(jí)或根本不選擇。
但并非每種條件的打破都滿(mǎn)足實(shí)際需求:
如可以通過(guò)使用**@Primary**讓被標(biāo)記的候選者有更高優(yōu)先級(jí),但并不一定符合業(yè)務(wù)需求,好比我們本身需要兩種DB都能使用,而非不可兼得。
@Repository
@Primary
@Slf4j
public class OracleDataService implements DataService{
//省略非關(guān)鍵代碼
}
要同時(shí)支持多種DataService,不同情景精確匹配不同的DataService,可這樣修改:
@Autowired DataService oracleDataService;
將屬性名和Bean名精確匹配,就能實(shí)現(xiàn)完美的注入選擇:
- 需要Oracle時(shí)指定屬性名為oracleDataService
- 需要Cassandra時(shí)則指定屬性名為cassandraDataService
顯式引用Bean時(shí)首字母忽略大小寫(xiě)
還有另外一種解決辦法,即采用@Qualifier顯式指定引用服務(wù),例如采用下面的方式:
@Autowired()
@Qualifier("cassandraDataService")
DataService dataService;
這樣能讓尋找出的Bean只有一個(gè)(即精確匹配),無(wú)需后續(xù)的決策過(guò)程:
DefaultListableBeanFactory#doResolveDependency
@Nullable
public Object doResolveDependency(DependencyDescriptor descriptor, @Nullable String beanName,
@Nullable Set<String> autowiredBeanNames, @Nullable TypeConverter typeConverter) throws BeansException {
//省略其他非關(guān)鍵代碼
//尋找bean過(guò)程
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) {
//省略多個(gè)bean的決策過(guò)程,即案例1重點(diǎn)介紹內(nèi)容
}
//省略其他非關(guān)鍵代碼
}
使用 @Qualifier 指定名稱(chēng)匹配,最終只找到唯一一個(gè)。但使用時(shí),可能會(huì)忽略Bean名稱(chēng)首字母大小寫(xiě)。
如:
@Autowired
@Qualifier("CassandraDataService")
DataService dataService;
運(yùn)行報(bào)錯(cuò):
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 名稱(chēng),默認(rèn)就是類(lèi)名,不過(guò)首字母小寫(xiě)!
假設(shè)要支持SQLServer,定義了一個(gè)名為SQLServerDataService的實(shí)現(xiàn):
@Autowired
@Qualifier("sQLServerDataService")
DataService dataService;
依然出現(xiàn)之前錯(cuò)誤,而若改成SQLServerDataService,則運(yùn)行通過(guò)。
這真是瘋了呀!
顯式引用Bean時(shí),首字母到底是大寫(xiě)還是小寫(xiě)?
答疑
raiseNoMatchingBeanFound(type, descriptor.getResolvableType(), descriptor);
當(dāng)因名稱(chēng)問(wèn)題(例如引用Bean首字母搞錯(cuò)了)找不到Bean,會(huì)拋NoSuchBeanDefinitionException。
不顯式設(shè)置名字的Bean,其默認(rèn)名稱(chēng)首字母到底是大寫(xiě)還是小寫(xiě)呢?
Spring Boot應(yīng)用會(huì)自動(dòng)掃包,找出直接或間接標(biāo)記了 @Component 的BeanDefinition。例如CassandraDataService、SQLServerDataService都被標(biāo)記了@Repository,而Repository本身被@Component標(biāo)記,所以都間接標(biāo)記了@Component。
一旦找出這些Bean信息,就可生成Bean名,然后組合成一個(gè)個(gè)BeanDefinitionHolder返回給上層:
ClassPathBeanDefinitionScanner#doScan

BeanNameGenerator#generateBeanName產(chǎn)生Bean名,有兩種實(shí)現(xiàn)方式:

因?yàn)镈ataService實(shí)現(xiàn)都是使用注解,所以Bean名稱(chēng)的生成邏輯最終調(diào)用的其實(shí)是
AnnotationBeanNameGenerator#generateBeanName

看Bean有無(wú)顯式指明名稱(chēng),若:
- 有
用顯式名稱(chēng)
- 沒(méi)有
生成默認(rèn)名稱(chēng)
案例沒(méi)有給Bean指名,所以生成默認(rèn)名稱(chēng),通過(guò)方法:
buildDefaultBeanName

首先,獲取一個(gè)簡(jiǎn)短的ClassName,然后調(diào)用Introspector#decapitalize方法,設(shè)置首字母大寫(xiě)或小寫(xiě),具體參考下面的代碼實(shí)現(xiàn):

- 一個(gè)類(lèi)名是以?xún)蓚€(gè)大寫(xiě)字母開(kāi)頭,則首字母不變
- 其它情況下默認(rèn)首字母變成小寫(xiě)
SQLServerDataService的Bean,其名稱(chēng)應(yīng)該就是類(lèi)名本身,而CassandraDataService的Bean名稱(chēng)則變成了首字母小寫(xiě)(cassandraDataService)。
修正
引用處修正
@Autowired
@Qualifier("cassandraDataService")
DataService dataService;
定義處顯式指定Bean名字,我們可以保持引用代碼不變,而通過(guò)顯式指明CassandraDataService 的Bean名稱(chēng)為CassandraDataService來(lái)糾正這個(gè)問(wèn)題。
@Repository("CassandraDataService")
@Slf4j
public class CassandraDataService implements DataService {
//省略實(shí)現(xiàn)
}
如果你不太了解源碼,不想糾結(jié)于首字母到底是大寫(xiě)還是小寫(xiě),建議第二種方法
引用內(nèi)部類(lèi)的Bean遺忘類(lèi)名
這就能搞定所有Bean顯式引用不出 bug 嗎?
沿用上面案例,稍微再添加點(diǎn)別的需求,例如我們需要定義一個(gè)內(nèi)部類(lèi)來(lái)實(shí)現(xiàn)一種新的DataService,代碼如下:
public class StudentController {
@Repository
public static class InnerClassDataService implements DataService{
@Override
public void deleteStudent(int id) {
//空實(shí)現(xiàn)
}
}
// ...
}
這時(shí)一般都用下面的方式直接去顯式引用這個(gè)Bean:
@Autowired
@Qualifier("innerClassDataService")
DataService innerClassDataService;
那直接采用首字母小寫(xiě),這樣就萬(wàn)無(wú)一失了嗎?
仍報(bào)錯(cuò)“找不到Bean”,why?
答疑
現(xiàn)在問(wèn)題是“如何引用內(nèi)部類(lèi)的Bean”。
在AnnotationBeanNameGenerator#buildDefaultBeanName,只關(guān)注了首字母是否小寫(xiě),而在最后變換首字母前,有這么一行處理 class 名稱(chēng)的:

我們可以看下它的實(shí)現(xiàn):
ClassUtils#getShortName

假設(shè)是個(gè)內(nèi)部類(lèi),例如下面的類(lèi)名:
com.javaedge.StudentController.InnerClassDataService
經(jīng)過(guò)該方法處理后,得到名稱(chēng):
StudentController.InnerClassDataService
最后經(jīng)Introspector.decapitalize首字母變換,得到Bean名稱(chēng):
studentController.InnerClassDataService
所以直接使用 innerClassDataService 找不到想要的Bean。
修正
@Autowired
@Qualifier("studentController.InnerClassDataService")
DataService innerClassDataService;
總結(jié)
像第一個(gè)案例,同種類(lèi)型的實(shí)現(xiàn),可能不是同時(shí)出現(xiàn)在自己的項(xiàng)目代碼中,而是有部分實(shí)現(xiàn)出現(xiàn)在依賴(lài)的類(lèi)庫(kù)??磥?lái)研究源碼的確能讓我們少寫(xiě)幾個(gè) bug!
到此這篇關(guān)于關(guān)于Spring的@Autowired依賴(lài)注入常見(jiàn)錯(cuò)誤的總結(jié)的文章就介紹到這了,更多相關(guān)Spring @Autowired 依賴(lài)注入內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
MyBatis批量更新(update foreach)報(bào)錯(cuò)問(wèn)題
這篇文章主要介紹了MyBatis批量更新(update foreach)報(bào)錯(cuò)問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-08-08
SpringBoot讀取yml文件中配置數(shù)組的2種方法
這篇文章主要介紹了SpringBoot讀取yml文件中配置數(shù)組的2種方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-12-12
用Java將字符串的首字母轉(zhuǎn)換大小寫(xiě)
在項(xiàng)目開(kāi)發(fā)的時(shí)候會(huì)需要統(tǒng)一字符串的格式,比如首字母要求統(tǒng)一大寫(xiě)或小寫(xiě),那用Java如何實(shí)現(xiàn)這一功能?下面一起來(lái)學(xué)習(xí)學(xué)習(xí)。2016-08-08
Spring Security 密碼驗(yàn)證動(dòng)態(tài)加鹽的驗(yàn)證處理方法
小編最近在改造項(xiàng)目,需要將gateway整合security在一起進(jìn)行認(rèn)證和鑒權(quán),今天小編給大家分享Spring Security 密碼驗(yàn)證動(dòng)態(tài)加鹽的驗(yàn)證處理方法,感興趣的朋友一起看看吧2021-06-06
springcloud?gateway無(wú)法路由問(wèn)題的解決
gateway網(wǎng)關(guān)的重要作用之一便是進(jìn)行路由轉(zhuǎn)發(fā)工作,下面這篇文章主要給大家介紹了關(guān)于springcloud?gateway無(wú)法路由問(wèn)題的解決方法,文中通過(guò)實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2023-05-05
SpringData @Query和@Modifying注解原理解析
這篇文章主要介紹了SpringData @Query和@Modifying注解原理解析,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-08-08

