关于Spring的@Autowired依赖注入常见错误的总结

网友投稿 969 2022-12-08

关于Spring的@Autowired依赖注入常见错误的总结

关于Spring的@Autowired依赖注入常见错误的总结

做不到雨露均沾

经常会遇到,required a single bean, but 2 were found。

根据ID移除学生

DataService是个接口,其实现依赖Oracle:

现在期望把部分非核心业务从Oracle迁移到Cassandra,自然会先添加上一个新的DataService实现:

@Repository

@Slf4j

public class CassandraDataService implements DataService{

@Override

public void deleteStudent(int id) {

log.info("delete student info maintained by cassandra");

}

}

当完成支持多个数据库的准备工作时,程序就已经无法启动了,报错如下:

解析

当一个Bean被构建时的核心步骤:

执行AbstractAutowireCapableBeanFactory#createBeanInstance:通过构造器反射出该Bean,如构建StudentController实例

执行AbstractAutowireCapableBeanFactory#populate:填充设置该Bean,如设置StudentController实例中被 @Autowired 标记的dataService属性成员。

“填充”过程的关键就是执行各种BeanPostProcessor处理器,关键代码如下:

protected void populateBean(String beanName, RootBeanDefinition mbd, @Nullable BeanWrapper bw) {

//省略非关键代码

for (BeanPostProcessor bp http://: getBeanPostProcessors()) {

if (bp instanceof InstantiationAwareBeanPostProcessor) {

InstantiationAwareBeanPostProcessor ibp = (InstantiationAwareBeanPostProcessor) bp;

PropertyValues pvsToUse = ibp.postProcessProperties(pvs, bw.getWrappedInstance(), beanName);

//省略非关键代码

}

}

}

}

因为StudentController含标记为Autowired的成员属性dataService,所以会使用到AutowiredAnnotationBeanPostProcessor完成“装配”:找出合适的DataService bean,设置给StudentController#dataService。

装配过程:

1.寻找所有需依赖注入的字段和方法:AutowiredAnnotationBeanPostProcessor#postProcessProperties

2.根据依赖信息寻找依赖并完成注入。比如字段注入,参考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);

}

}

案例中的错误就发生在上述“寻找依赖”的过程中,DefaultListableBeanFactory#doResolveDependency

当根据DataService类型找依赖时,会找出2个依赖:

CassandraDataService

OracleDataService

在这样的情况下,如果同时满足以下两个条件则会抛出本案例的错误:

调用determineAutowireCandidate方法来选出优先级最高的依赖,但是发现并没有优先级可依据。具体选择过程可参考

DefaultListableBeanFactory#determineAutowireCandidate:

protected String determineAutowireCandidate(Map 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 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;

}

优先级的决策是先根据@Primary,其次是@Priority,最后根据Bean名严格匹配。

如果这些帮助决策优先级的注解都没有被使用,名字也不精确匹配,则返回null,告知无法决策出哪种最合适。

@Autowired要求是必须注入的(required默认值true),或注解的属性类型并不是可以接受多个Bean的类型,例如数组、Map、集合。

这点可以参考DefaultListableBeanFactory#indicatesMultipleBeans:

private boolean indicatesMultipleBeans(Class> type) {

return (type.isArray() || (type.isInterface() &&

(Collection.class.isAssignableFrom(type) || Map.class.isAssignableFrom(type))));

}

案例程序能满足这些条件,所以报错并不奇怪。而如果我们把这些条件想得简单点,或许更容易帮助我们去理解这个设计。就像我们遭遇多个无法比较优劣的选择,却必须选择其一时,与其偷偷地随便选择一种,还不如直接报错,起码可以避免更严重的问题发生。

修正

打破上述两个条件中的任何一个即可,即让候选项具有优先级或根本不选择。

但并非每种条件的打破都满足实际需求:

如可以通过使用**@Primary**让被标记的候选者有更高优先级,但并不一定符合业务需求,好比我们本身需要两种DB都能使用,而非不可兼得。

@Repository

@Primary

@Slf4j

public class OracleDataService implements DataService{

//省略非关键代码

}

要同时支持多种DataService,不同情景精确匹配http://不同的DataService,可这样修改:

@Autowired

DataService oracleDataService;

将属性名和Bean名精确匹配,就能实现完美的注入选择:

需要Oracle时指定属性名为oracleDataService

需要Cassandra时则指定属性名为cassandraDataService

显式引用Bean时首字母忽略大小写

还有另外一种解决办法,即采用@QualifierLyIsLYNFtB显式指定引用服务,例如采用下面的方式:

@Autowired()

@Qualifier("cassandraDataService")

DataService dataService;

这样能让寻找出的Bean只有一个(即精确匹配),无需后续的决策过程:

DefaultListableBeanFactory#doResolveDependency

@Nullable

public Object doResolveDependency(DependencyDescriptor descriptor, @Nullable String beanName,

@Nullable Set autowiredBeanNames, @Nullable TypeConverter typeConverter) throws BeansException {

//省略其他非关键代码

//寻找bean过程

Map matchingBeans = findAutowireCandidates(beanName, type, descriptor);

if (matchingBeans.isEmpty()) {

if (isRequired(descriptor)) {

raiseNoMatchingBeanFound(type, descriptor.getResolvableType(), descriptor);

}

return null;

}

//省略其他非关键代码

if (matchingBeans.size() > 1) {

//省略多个bean的决策过程,即案例1重点介绍内容

}

//省略其他非关键代码

}

使用 @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 annotaLyIsLYNFtBtions:

{@org.springframework.beans.factory.annotation.Autowired(required=true),

@org.springframework.beans.factory.annotation.Qualifier(value=CassandraDataService)}

若未显式指定 bean 名称,默认就是类名,不过首字母小写!

假设要支持SQLServer,定义了一个名为SQLServerDataService的实现:

@Autowired

@Qualifier("sQLServerDataService")

DataService dataService;

依然出现之前错误,而若改成SQLServerDataService,则运行通过。

这真是疯了呀!

显式引用Bean时,首字母到底是大写还是小写?

答疑

raiseNoMatchingBeanFound(type, descriptor.getResolvableType(),

descriptor);

当因名称问题(例如引用Bean首字母搞错了)找不到Bean,会抛NoSuchBeanDefinitionException。

不显式设置名字的Bean,其默认名称首字母到底是大写还是小写呢?

Spring Boot应用会自动扫包,找出直接或间接标记了 @Component 的BeanDefinition。例如CassandraDataService、SQLServerDataService都被标记了@Repository,而Repository本身被@Component标记,所以都间接标记了@Component。

一旦找出这些Bean信息,就可生成Bean名,然后组合成一个个BeanDefinitionHolder返回给上层:

ClassPathBeanDefinitionScanner#doScan

BeanNameGenerator#generateBeanName产生Bean名,有两种实现方式:

因为DataService实现都是使用注解,所以Bean名称的生成逻辑最终调用的其实是

AnnotationBeanNameGenerator#generateBeanName

看Bean有无显式指明名称,若:

用显式名称

没有

生成默认名称

案例没有给Bean指名,所以生成默认名称,通过方法:

buildDefaultBeanName

首先,获取一个简短的ClassName,然后调用Introspector#decapitalize方法,设置首字母大写或小写,具体参考下面的代码实现:

一个类名是以两个大写字母开头,则首字母不变

其它情况下默认首字母变成小写

SQLServerDataService的Bean,其名称应该就是类名本身,而CassandraDataService的Bean名称则变成了首字母小写(cassandraDataService)。

修正

引用处修正

@Autowired

@Qualifier("cassandraDataService")

DataService dataService;

定义处显式指定Bean名字,我们可以保持引用代码不变,而通过显式指明CassandraDataService 的Bean名称为CassandraDataService来纠正这个问题。

@Repository("CassandraDataService")

@Slf4j

public class CassandraDataService implements DataService {

//省略实现

}

如果你不太了解源码,不想纠结于首字母到底是大写还是小写,建议第二种方法

引用内部类的Bean遗忘类名

这就能搞定所有Bean显式引用不出 bug 吗?

沿用上面案例,稍微再添加点别的需求,例如我们需要定义一个内部类来实现一种新的DataService,代码如下:

public class StudentController {

@Repository

public static class InnerClassDataService implements DataService{

@Override

public void deleteStudent(int id) {

//空实现

}

}

// ...

}

这时一般都用下面的方式直接去显式引用这个Bean:

@Autowired

@Qualifier("innerClassDataService")

DataService innerClassDataService;

那直接采用首字母小写,这样就万无一失了吗?

仍报错“找不到Bean”,why?

答疑

现在问题是“如何引用内部类的Bean”。

在AnnotationBeanNameGenerator#buildDefaultBeanName,只关注了首字母是否小写,而在最后变换首字母前,有这么一行处理 class 名称的:

我们可以看下它的实现:

ClassUtils#getShortName

假设是个内部类,例如下面的类名:

com.javaedge.StudentController.InnerClassDataService

经过该方法处理后,得到名称:

StudentController.InnerClassDataService

最后经Introspector.decapitalize首字母变换,得到Bean名称:

studentController.InnerClassDataService

所以直接使用 innerClassDataService 找不到想要的Bean。

修正

@Autowired

@Qualifier("studentController.InnerClassDataService")

DataService innerClassDataService;

总结

像第一个案例,同种类型的实现,可能不是同时出现在自己的项目代码中,而是有部分实现出现在依赖的类库。看来研究源码的确能让我们少写几个 bug!

版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:基于SpringBoot开机启动与@Order注解
下一篇:详解MyBatis resultType与resultMap中的几种返回类型
相关文章

 发表评论

暂时没有评论,来抢沙发吧~