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...

相关推荐
二闹9 分钟前
三个注解,到底该用哪一个?别再傻傻分不清了!
后端
用户490558160812520 分钟前
当控制面更新一条 ACL 规则时,如何更新给数据面
后端
林太白22 分钟前
Nuxt.js搭建一个官网如何简单
前端·javascript·后端
码事漫谈24 分钟前
VS Code 终端完全指南
后端
该用户已不存在1 小时前
OpenJDK、Temurin、GraalVM...到底该装哪个?
java·后端
怀刃1 小时前
内存监控对应解决方案
后端
TT哇1 小时前
@[TOC](计算机是如何⼯作的) JavaEE==网站开发
java·redis·java-ee
码事漫谈1 小时前
VS Code Copilot 内联聊天与提示词技巧指南
后端
Tina学编程1 小时前
48Days-Day19 | ISBN号,kotori和迷宫,矩阵最长递增路径
java·算法
Moonbit2 小时前
MoonBit Perals Vol.06: MoonBit 与 LLVM 共舞 (上):编译前端实现
后端·算法·编程语言