纯净、安全、绿色的下载网站

首页|软件分类|下载排行|最新软件|IT学院

当前位置:首页IT学院IT技术

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

JavaEdge.   2021-09-15 我要评论
想了解关于Spring的@Autowired依赖注入常见错误的总结的相关内容吗JavaEdge.在本文为您仔细讲解Spring @Autowired 依赖注入的相关知识和一些Code实例欢迎阅读和指正我们先划重点:Spring,@Autowired,依赖注入,Java,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 : 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<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;
}

优先级的决策是先根据@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不同情景精确匹配不同的DataService可这样修改:

@Autowired
DataService oracleDataService;

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

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

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

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

@Autowired()
@Qualifier("cassandraDataService")
DataService dataService;

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

DefaultListableBeanFactory#doResolveDependency

@Nullable
public Object doResolveDependency(DependencyDescriptor descriptor, @Nullable String beanName,
      @Nullable Set<String> autowiredBeanNames, @Nullable TypeConverter typeConverter) throws BeansException {
      //省略其他非关键代码
      //寻找bean过程
      Map<String, Object> 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 annotations:
       {@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!


相关文章

猜您喜欢

  • Python zipfile压缩包 Python 标准库zipfile将文件夹加入压缩包的操作方法

    想了解Python 标准库zipfile将文件夹加入压缩包的操作方法的相关内容吗Likianta Me在本文为您仔细讲解Python zipfile压缩包的相关知识和一些Code实例欢迎阅读和指正我们先划重点:Python,zipfile压缩包,Python,zipfile文件压缩包下面大家一起来学习吧..
  • Spring Boot @Async异步线程池 Spring Boot之@Async异步线程池示例详解

    想了解Spring Boot之@Async异步线程池示例详解的相关内容吗hguisu在本文为您仔细讲解Spring Boot @Async异步线程池的相关知识和一些Code实例欢迎阅读和指正我们先划重点:springboot异步线程池,springboot线程池使用实例,springboot,@async的使用下面大家一起来学习吧..

网友评论

Copyright 2020 www.sopisoft.net 【绿软下载站】 版权所有 软件发布

声明:所有软件和文章来自软件开发商或者作者 如有异议 请与本站联系 点此查看联系方式