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


    使用@Autowired时扫描到多个Bean

    问题

    对于同一个接口存在多个实现类,此时使用@Autowired注解会出现required a single bean, but 2 were found

    // 控制器类
    @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");
     }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    原因

    当一个Bean被构建时,核心包括两个基本步骤:

    • 执行AbstractAutowireCapableBeanFactory#createBeanInstance方法:通过构造器反射构造出该Bean(即构建出StudentController的实例)

    • 执行AbstractAutowireCapableBeanFactory#populate方法:填充(即设置)该Bean(即设置StudentController实例中被@Autowired标记的

      dataService属性成员)

      • 寻找出所有需要依赖注入的字段和方法:
      // AutowiredAnnotationBeanPostProcessor#postProcessProperties
      InjectionMetadata metadata = findAutowiringMetadata(beanName, bean.getClass(), pvs);
      
      • 1
      • 2
      • 根据依赖信息寻找出依赖并完成注入,以字段注入为例:
      // 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);
          }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 在注入时发现存在两个匹配的Bean,此时需要进行判断:

        // 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();
                }   
            }
        }
        
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
        • 9
        • 10
        • 11
        • 12
        • 13
        • 14
        • 15
        • 16
        • 17
        • 18
        • 19
        • 20
        • 21
        • 22
        • 23
        • 24
        • 25
        • 26
        • 27
        • 28
        • 29
        • 30
        • 31
        • 32
        • 依次按照@Primary@Priority和Bean名字来匹配准确的Bean:
        // 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;
        }
        
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
        • 9
        • 10
        • 11
        • 12
        • 13
        • 14
        • 15
        • 16
        • 17
        • 18
        • 19
        • 20
        • 21
        • 22
        • 23
        • 24
        • 判断@Autowired要求是必须注入的和解的属性类型不是可接受多个Bean的类型:
        // DefaultListableBeanFactory#indicatesMultipleBeans
        private boolean indicatesMultipleBeans(Class<?> type) {
            return (type.isArray() || (type.isInterface() &&
                                       (Collection.class.isAssignableFrom(type) || Map.class.isAssignableFrom(type))));
        }
        
        • 1
        • 2
        • 3
        • 4
        • 5

    解决方式

    使上述分析的两个情况其中一个不成立即可,但是需要根据业务需求进行选择,比如使用标记@Primary标记实现类,虽然可避免报错但是不能使用多个@Primary()注解标记同一个类型的bean,所以可进行BeanName的精确匹配:

    // 此时需要使用Oracle
    @Autowired
    DataService oracleDataService;
    
    • 1
    • 2
    • 3

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

    问题

    在第一个案例中还可以使用@Qualifier显式指定引用的是哪种服务,但是在使用的时候可能会忽略Bean的名称首字母大小写

    • 对于CassandraDataService类,如果选择指定为@Qualifier("CassandraDataService")会报错
    @Autowired()
    @Qualifier("CassandraDataService")  // 报错,这里需要写为cassandraDataService
    DataService dataService;
    
    • 1
    • 2
    • 3
    • 对于SQLiteDataService类,如果选择指定为@Qualifier("sQLiteDataService")会报错

    为什么有些类首字母需要大写,而有些则需要小写?


    原因

    在SpringBoot启动时会自动扫描当前的Package,进而找到直接或间接标记了@Component的Bean的定义(即BeanDefinition),找出这些Bean的信息就可生成这些Bean的名字,然后组合成一个个BeanDefinitionHolder返回给上层

    // 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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    在生成Bean的名字时有两种生成方式:

    • 如果Bean有显示指定名称(即在CassandraDataService@Repository注解中写名称)则用显式名称
    • 如果没显示指定名称则产生一个默认名称
    // 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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

    分析后原因在于decapitalize方法中的设置Bean名称首字母的规则:如果一个类名是以两个大写字母开头的则首字母不变,其它情况下默认首字母变成小写


    解决方式

    两种解决方式:

    • 纠正首字母大小写问题:
    @Autowired
    @Qualifier("cassandraDataService")  // 第二个字母为小写就只需将首字母小写
    DataService dataService;
    
    @Autowired
    @Qualifier("SQLiteDataService")  // 第二个字母为大写首字母依旧保持大写
    DataService dataService;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 显式指定Bean名字:
    @Repository("CassandraDataService")  // 显示定义
    @Slf4j
    public class CassandraDataService implements DataService {
        @Override
        public void deleteStudent(int id) {
            log.info("delete student info maintained by cassandra");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    引用内部类的Bean遗忘类名

    问题

    假设直接使用内部类来定义一个DataService接口的实现类,并且根据上面案例的经验对Bean名称按规定书写,还是出现了报错:

    @Repository
    public static class InnerClassDataService implements DataService{
        @Override
        public void deleteStudent(int id) {
            ...
        }
    }
    @Autowired
    @Qualifier("innerClassDataService")
    DataService innerClassDataService;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    原因

    在进行首字母变换前还进行了对class名称的处理:

    // 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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    // 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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    对于内部类的className=com.spring.puzzle.class2.example3.StudentController$InnerClassDataService,经过上述逻辑后为得到的类名为StudentController.InnerClassDataService,对它进行首字母转换后得到studentController.InnerClassDataService,与预期结果不符


    解决方式

    按照其对内部类转换后的类名写即可:

    @Autowired
    @Qualifier("studentController.InnerClassDataService")
    DataService innerClassDataService;
    
    • 1
    • 2
    • 3

    参考

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

    https://github.com/jiafu1115/springissue/tree/master/src/main/java/com/spring/puzzle/class2

  • 相关阅读:
    利用Socks5代理IP加强跨界电商爬虫的网络安全
    逢人必推的4款实用软件,国产良心,相遇不易
    《七月集训》第二十二日——有序集合
    Ubuntu如何创建一个子用户并赋与管理员权限
    Promise详解
    通过内网穿透,在Windows 10系统下搭建个人《我的世界》服务器公网联机
    【Java开发岗:Spring篇】
    抖音预约服务小程序开发:前端与后端技术的完美融合
    超标量处理器设计 姚永斌 第6章 指令解码 摘录
    掌握C语言指针,轻松解锁代码高效性与灵活性
  • 原文地址:https://blog.csdn.net/qq_41398418/article/details/132779061