Spring编程常见错误50例-Spring Bean依赖注入常见错误(上)

使用@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;

参考

极客时间-Spring 编程常见错误 50 例

github.com/jiafu1115/s...

相关推荐
上等猿6 分钟前
集合stream
java
java1234_小锋9 分钟前
MyBatis如何处理延迟加载?
java·开发语言
菠萝咕噜肉i11 分钟前
MyBatis是什么?为什么有全自动ORM框架还是MyBatis比较受欢迎?
java·mybatis·框架·半自动
海绵波波10719 分钟前
flask后端开发(1):第一个Flask项目
后端·python·flask
林的快手25 分钟前
209.长度最小的子数组
java·数据结构·数据库·python·算法·leetcode
weisian1511 小时前
Redis篇--常见问题篇8--缓存一致性3(注解式缓存Spring Cache)
redis·spring·缓存
向阳12181 小时前
mybatis 缓存
java·缓存·mybatis
上等猿1 小时前
函数式编程&Lambda表达式
java
蓝染-惣右介1 小时前
【23种设计模式·全精解析 | 行为型模式篇】11种行为型模式的结构概述、案例实现、优缺点、扩展对比、使用场景、源码解析
java·设计模式