Spring编程常见错误(二)

在使用@Autowire注解时,无论是老鸟还是小鸟,一定都会遇到过这个问题:

required a single bean, but 2 were found

意思是需要一个bean,但实际上我们可能提供了多个。

为了重现这个错误,我们可以写个案例来实现一下:

@Controller
public class StudentController {

    @Autowired
    StudentService studentService;

    @GetMapping("/handle")
    @ResponseBody
    public void handle(){
        studentService.handle();
    }

}

其中StudentService是一个接口:

public interface StudentService {
    void handle();
}

下面是接口的实现类:

@Service
@Slf4j
public class StudentServiceImpl implements StudentService{

    @Override
    public void handle() {
        log.info("handle已处理.....");
    }
}

截止目前,运行并测试程序是毫无问题的。但是需求往往是源源不断的,某天我们可能接到任务,需要扩展实现类:

@Slf4j
@Service
public class StudentServiceImpl2 implements StudentService{
    @Override
    public void handle() {
        log.info("handle By StudentServiceImpl2...");
    }
}

当我们完成功能的实现后,程序已经无法正常工作了:

 Spring还是非常人性化的,把符合的bean的绝对路径给我们展示了出来,方便我们更改,那么这个错误是怎么产生的呢,接下来我们来分析一下。

案例解析

要找到这个问题的根源,我们就需要对 @Autowired 实现的依赖注入的原理有一定的了解。首先,我们先来了解下 @Autowired 发生的位置和核心过程。

1,当一个 Bean 被构建时,核心包括两个基本步骤:执行 AbstractAutowireCapableBeanFactory#createBeanInstance 方法:通过构造器反射构造出这个 Bean,在此案例中相当于构建出 StudentController 的实例;

2,执行 AbstractAutowireCapableBeanFactory#populate 方法:填充这个 Bean,在本案例中,相当于设置 StudentController 实例中被 @Autowired 标记的 studentService 属性成员。

在步骤 2 中,“填充”过程的关键就是执行各种 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 的成员属性 studentService,所以会使用到 AutowiredAnnotationBeanPostProcessor(BeanPostProcessor 中的一种)来完成“装配”过程:找出合适的 StudentService 的 bean 并设置给 StudentController.studentService。如果深究这个装配过程,又可以细分为两个步骤:

1,寻找出所有需要依赖注入的字段和方法,参考 AutowiredAnnotationBeanPostProcessor.postProcessProperties 中的代码行:


InjectionMetadata metadata = findAutowiringMetadata(beanName, bean.getClass(), pvs);

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为"studentService"的DependencyDescriptor
         value = beanFactory.resolveDependency(desc, beanName, autowiredBeanNames, typeConverter);
      }
      
   }
   //省略非关键代码
   if (value != null) {
      ReflectionUtils.makeAccessible(field);
      //装配“依赖”
      field.set(bean, value);
   }
}

说到这里,我们基本了解了 @Autowired 过程发生的位置和过程。而且很明显,我们案例中的错误就发生在上述“寻找依赖”的过程中,那么到底是怎么发生的呢?我们可以继续刨根问底。

1,调用 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,告知无法决策出哪种最合适。

2,@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 的方式来让被标记的候选者有更高优先级,从而避免报错,但是它并不一定符合业务需求。

@Service
@Slf4j
@Primary
public class StudentServiceImpl implements StudentService{

    @Override
    public void handle() {
        log.info("handle已处理.....");
    }
}

我们同样可以使用下面的方式去修改:

@Autowired
StudentService studentServiceImpl;

修改方式的精髓在于将属性名和 Bean 名字精确匹配,这样就可以让注入选择不犯难.

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

有个金丝熊叫老许

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值