使用@Autowired时扫描到多个Bean
问题
对于同一个接口存在多个实现类,此时使用@Autowired
注解会出现required a single bean, but 2 were found
java
// 控制器类
@RestController
@Slf4j
@Validated
public class StudentController {
@Autowired
DataService dataService;
@Autowired
DataService oracleDataService;
@RequestMapping(path = "students/{id}", method = RequestMethod.DELETE)
public void deleteStudent(@PathVariable("id") @Range(min = 1,max = 100) int id){
dataService.deleteStudent(id);
};
}
// DataService接口
public interface DataService {
void deleteStudent(int id);
}
// 接口的具体实现
@Repository
@Slf4j
public class OracleDataService implements DataService {
@Override
public void deleteStudent(int id) {
log.info("delete student info maintained by oracle");
}
}
@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
属性成员)- 寻找出所有需要依赖注入的字段和方法:
java// AutowiredAnnotationBeanPostProcessor#postProcessProperties InjectionMetadata metadata = findAutowiringMetadata(beanName, bean.getClass(), pvs);
- 根据依赖信息寻找出依赖并完成注入,以字段注入为例:
java// AutowiredFieldElement#inject protected void inject(Object bean, @Nullable String beanName, @Nullable PropertyValues pvs) throws Throwable { Field field = (Field) this.member; Object value; ... try { // *寻找依赖,desc为dataService的DependencyDescriptor value = beanFactory.resolveDependency(desc, beanName, autowiredBeanNames, typeConverter); } ... if (value != null) { ReflectionUtils.makeAccessible(field); // 装配依赖 field.set(bean, value); } }
-
在注入时发现存在两个匹配的Bean,此时需要进行判断:
java// DefaultListableBeanFactory#doResolveDependency(即beanFactory.resolveDependency的底层逻辑) @Nullable public Object doResolveDependency(DependencyDescriptor descriptor, @Nullable String beanName, @Nullable Set<String> autowiredBeanNames, @Nullable TypeConverter typeConverter) throws BeansException { try { ... if (matchingBeans.size() > 1) { // *Ⅰ、首先依次按照@Primary、@Priority和Bean名字来匹配准确的Bean(该步也是后续提供解决方式的关键) autowiredBeanName = determineAutowireCandidate(matchingBeans, descriptor); if (autowiredBeanName == null) { // *Ⅱ、如果选择不出来则判断是否满足以下两个条件: // ①@Autowired要求是必须注入的(即required保持默认值为true) // ②注解的属性类型不是可接受多个Bean的类型,例如数组、Map、集合 if (isRequired(descriptor) || !indicatesMultipleBeans(type)) { // 执行到该步骤的话就是抛出异常了 return descriptor.resolveNotUnique(descriptor.getResolvableType(), matchingBeans); } else { return null; } } instanceCandidate = matchingBeans.get(autowiredBeanName); } else { // 只匹配到一个(即只有一个实现类) Map.Entry<String, Object> entry = matchingBeans.entrySet().iterator().next(); autowiredBeanName = entry.getKey(); instanceCandidate = entry.getValue(); } } }
- 依次按照
@Primary
、@Priority
和Bean名字来匹配准确的Bean:
java// DefaultListableBeanFactory#determineAutowireCandidate protected String determineAutowireCandidate(Map<String, Object> candidates, DependencyDescriptor descriptor) { Class<?> requiredType = descriptor.getDependencyType(); // ①先根据@Primary来决策 String primaryCandidate = determinePrimaryCandidate(candidates, requiredType); if (primaryCandidate != null) { return primaryCandidate; } // ②再根据@Priority决策 String priorityCandidate = determineHighestPriorityCandidate(candidates, requiredType); if (priorityCandidate != null) { return priorityCandidate; } // ③最后尝试根据Bean名字的严格匹配来决策 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; }
- 判断
@Autowired
要求是必须注入的和解的属性类型不是可接受多个Bean的类型:
java// DefaultListableBeanFactory#indicatesMultipleBeans private boolean indicatesMultipleBeans(Class<?> type) { return (type.isArray() || (type.isInterface() && (Collection.class.isAssignableFrom(type) || Map.class.isAssignableFrom(type)))); }
- 依次按照
解决方式
使上述分析的两个情况其中一个不成立即可,但是需要根据业务需求进行选择,比如使用标记@Primary标记实现类,虽然可避免报错但是不能使用多个@Primary()
注解标记同一个类型的bean,所以可进行BeanName的精确匹配:
java
// 此时需要使用Oracle
@Autowired
DataService oracleDataService;
显式引用Bean时首字母忽略大小写
问题
在第一个案例中还可以使用@Qualifier
显式指定引用的是哪种服务,但是在使用的时候可能会忽略Bean的名称首字母大小写
- 对于
CassandraDataService
类,如果选择指定为@Qualifier("CassandraDataService")
会报错
java
@Autowired()
@Qualifier("CassandraDataService") // 报错,这里需要写为cassandraDataService
DataService dataService;
- 对于
SQLiteDataService
类,如果选择指定为@Qualifier("sQLiteDataService")
会报错
为什么有些类首字母需要大写,而有些则需要小写?
原因
在SpringBoot启动时会自动扫描当前的Package,进而找到直接或间接标记了@Component
的Bean的定义(即BeanDefinition
),找出这些Bean的信息就可生成这些Bean的名字,然后组合成一个个BeanDefinitionHolder
返回给上层
java
// ClassPathBeanDefinitionScanner#doScan
protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
Assert.notEmpty(basePackages, "At least one base package must be specified");
Set<BeanDefinitionHolder> beanDefinitions = new LinkedHashSet<>();
for (String basePackage : basePackages) {
Set<BeanDefinition> candidates = findCandidateComponents(basePackage);
for (BeanDefinition candidate : candidates) {
ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(candidate);
candidate.setScope(scopeMetadata.getScopeName());
// *生成Bean的名字
String beanName = this.beanNameGenerator.generateBeanName(candidate, this.registry);
if (candidate instanceof AbstractBeanDefinition) {
postProcessBeanDefinition((AbstractBeanDefinition) candidate, beanName);
}
if (candidate instanceof AnnotatedBeanDefinition) {
AnnotationConfigUtils.processCommonDefinitionAnnotations((AnnotatedBeanDefinition) candidate);
}
if (checkCandidate(beanName, candidate)) {
BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName);
definitionHolder =
AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);
beanDefinitions.add(definitionHolder);
registerBeanDefinition(definitionHolder, this.registry);
}
}
}
return beanDefinitions;
}
在生成Bean的名字时有两种生成方式:
- 如果Bean有显示指定名称(即在
CassandraDataService
的@Repository
注解中写名称)则用显式名称 - 如果没显示指定名称则产生一个默认名称
java
// AnnotationBeanNameGenerator#generateBeanName
@Override
public String generateBeanName(BeanDefinition definition, BeanDefinitionRegistry registry) {
if (definition instanceof AnnotatedBeanDefinition) {
String beanName = determineBeanNameFromAnnotation((AnnotatedBeanDefinition) definition);
if (StringUtils.hasText(beanName)) {
return beanName;
}
}
// *生成默认名称
return buildDefaultBeanName(definition, registry);
}
// AnnotationBeanNameGenerator#buildDefaultBeanName
// 默认名称生成方法
protected String buildDefaultBeanName(BeanDefinition definition) {
String beanClassName = definition.getBeanClassName();
Assert.state(beanClassName != null, "No bean class name set");
String shortClassName = ClassUtils.getShortName(beanClassName);
// *设置首字母的规则
return Introspector.decapitalize(shortClassName);
}
// Introspector#decapitalize
// 设置首字母的规则:如果一个类名是以两个大写字母开头的则首字母不变,其它情况下默认首字母变成小写
public static String decapitalize(String name) {
if (name == null || name.length() == 0) {
return name;
}
if (name.length() > 1 && Character.isUpperCase(name.charAt(1)) &&
Character.isUpperCase(name.charAt(0))){
return name;
}
char chars[] = name.toCharArray();
chars[0] = Character.toLowerCase(chars[0]);
return new String(chars);
}
分析后原因在于decapitalize
方法中的设置Bean名称首字母的规则:如果一个类名是以两个大写字母开头的则首字母不变,其它情况下默认首字母变成小写
解决方式
两种解决方式:
- 纠正首字母大小写问题:
java
@Autowired
@Qualifier("cassandraDataService") // 第二个字母为小写就只需将首字母小写
DataService dataService;
@Autowired
@Qualifier("SQLiteDataService") // 第二个字母为大写首字母依旧保持大写
DataService dataService;
- 显式指定Bean名字:
java
@Repository("CassandraDataService") // 显示定义
@Slf4j
public class CassandraDataService implements DataService {
@Override
public void deleteStudent(int id) {
log.info("delete student info maintained by cassandra");
}
}
引用内部类的Bean遗忘类名
问题
假设直接使用内部类来定义一个DataService
接口的实现类,并且根据上面案例的经验对Bean名称按规定书写,还是出现了报错:
java
@Repository
public static class InnerClassDataService implements DataService{
@Override
public void deleteStudent(int id) {
...
}
}
@Autowired
@Qualifier("innerClassDataService")
DataService innerClassDataService;
原因
在进行首字母变换前还进行了对class名称的处理:
java
// AnnotationBeanNameGenerator#buildDefaultBeanName
protected String buildDefaultBeanName(BeanDefinition definition) {
String beanClassName = definition.getBeanClassName();
Assert.state(beanClassName != null, "No bean class name set");
// 对class名称的处理
String shortClassName = ClassUtils.getShortName(beanClassName);
return Introspector.decapitalize(shortClassName);
}
java
// ClassUtils#getShortName
public static String getShortName(String className) {
Assert.hasLength(className, "Class name must not be empty");
int lastDotIndex = className.lastIndexOf(PACKAGE_SEPARATOR);
int nameEndIndex = className.indexOf(CGLIB_CLASS_SEPARATOR);
if (nameEndIndex == -1) {
nameEndIndex = className.length();
}
String shortName = className.substring(lastDotIndex + 1, nameEndIndex);
shortName = shortName.replace(INNER_CLASS_SEPARATOR, PACKAGE_SEPARATOR);
return shortName;
}
对于内部类的className=com.spring.puzzle.class2.example3.StudentController$InnerClassDataService
,经过上述逻辑后为得到的类名为StudentController.InnerClassDataService
,对它进行首字母转换后得到studentController.InnerClassDataService
,与预期结果不符
解决方式
按照其对内部类转换后的类名写即可:
java
@Autowired
@Qualifier("studentController.InnerClassDataService")
DataService innerClassDataService;