定义的 Bean 缺少隐式依赖

  Java   9分钟   309浏览   0评论

定义的 Bean 缺少隐式依赖

初学 Spring 时,我们往往不能快速转化思维。例如,在程序开发过程中,有时候,一方面我们把一个类定义成 Bean,同时又觉得这个 Bean 的定义除了加了一些 Spring 注解外,并没有什么不同。所以在后续使用时,有时候我们会不假思索地去随意定义它,例如我们会写出下面这样的代码:

package com.zou.service.impl;

import org.springframework.stereotype.Service;

/**
 * @author: 邹祥发
 * @date: 2022/6/21 21:14
 */
@Service
public class ServiceImpl {
    private String serviceName;

    public ServiceImpl(String serviceName) {
        this.serviceName = serviceName;
    }
}

ServiceImpl 因为标记为 @Service 而成为一个 Bean。另外我们 ServiceImpl 显式定义了一个构造器。但是,上面的代码不是永远都能正确运行的,有时候会报下面这种错误:

Parameter 0 of constructor in com.zou.service.impl.ServiceImpl required a bean of type 'java.lang.String' that could not be found.

那这种错误是怎么发生的呢?下面我们来分析一下。

解析

当创建一个 Bean 时,调用的方法是AbstractAutowireCapableBeanFactory#createBeanInstance。它主要包含两大基本步骤:寻找构造器和通过反射调用构造器创建实例。对于这个案例,最核心的代码执行,你可以参考下面的代码片段:

// Candidate constructors for autowiring?
Constructor<?>[] ctors = determineConstructorsFromBeanPostProcessors(beanClass);
if(ctors != null || mbd.getResolvedAutowireMode()==AUTOWIRE_CONSTRUCTOR || mbd.hasConstructorArgumentValues()||!ObjectUtils.isEmpty(args)){
    return autowireConstructor(beanName, mbd, ctors, args);
}

Spring 会先执行 determineConstructorsFromBeanPostProcessors 方法来获取构造器,然后通过 autowireConstructor 方法带着构造器去创建实例。很明显,在本案例中只有一个构造器,所以非常容易跟踪这个问题。autowireConstructor 方法要创建实例,不仅需要知道是哪个构造器,还需要知道构造器对应的参数,这点从最后创建实例的方法名也可以看出,参考如下(即
ConstructorResolver#instantiate):

private Object instantiate(
String beanName, RootBeanDefinition mbd,Constructor<?> constructorToUse, Object[] argsToUse)

那么上述方法中存储构造参数的 argsToUse 如何获取呢?换言之,当我们已经知道构造器ServiceImpl(String serviceName),要创建出 ServiceImpl 实例,如何确定serviceName 的值是多少?
很明显,这里是在使用 Spring,我们不能直接显式使用 new 关键字来创建实例。Spring只能是去寻找依赖来作为构造器调用参数。那么这个参数如何获取呢?可以参考下面的代码片段(即ConstructorResolver#autowireConstructor):

argsHolder = createArgumentArray(beanName, mbd, resolvedValues, bw, paramTypes
getUserDeclaredConstructor(candidate), autowiring, candidates.length == 1);

我们可以调用 createArgumentArray 方法来构建调用构造器的参数数组,而这个方法的最终实现是从 BeanFactory 中获取 Bean,可以参考下述调用:

return this.beanFactory.resolveDependency(
new DependencyDescriptor(param, true), beanName, autowiredBeanNames, typeConverter);

如图所示,上述的调用即是根据参数来寻找对应的 Bean,在本案例中,如果找不到对应的Bean 就会抛出异常,提示装配失败。

解决方案

从源码级别了解了错误的原因后,现在反思为什么会出现这个错误。追根溯源,正如开头所述,因为不了解很多隐式的规则:我们定义一个类为 Bean,如果再显式定义了构造器,那么这个 Bean 在构建时,会自动根据构造器参数定义寻找对应的 Bean,然后反射创建出
这个 Bean。了解了这个隐式规则后,解决这个问题就简单多了。我们可以直接定义一个能让 Spring 装配给 ServiceImpl 构造器参数的 Bean,例如定义如下:

//这个bean装配给ServiceImpl的构造器参数“serviceName”
@Bean
public String serviceName() {
   return "MyServiceName";
}

再次运行程序,发现一切正常了。

所以,我们在使用 Spring 时,不要总想着定义的 Bean 也可以在非 Spring 场合直接用new 关键字显式使用,这种思路是不可取的。
另外,类似的,假设我们不了解 Spring 的隐式规则,在修正问题后,我们可能写出更多看似可以运行的程序,代码如下:

package com.zou.service.impl;

import org.springframework.stereotype.Service;

/**
 * @author: 邹祥发
 * @date: 2022/6/21 19:14
 */
@Service
public class ServiceImpl {
    private String serviceName;
    public ServiceImpl(String serviceName) {
        this.serviceName = serviceName;
    }
    public ServiceImpl(String serviceName, String otherStringParameter) {
        this.serviceName = serviceName;
    }
}

如果我们仍用非 Spring 的思维去审阅这段代码,可能不会觉得有什么问题,毕竟 String类型可以自动装配了,无非就是增加了一个 String 类型的参数而已。但是如果你了解 Spring 内部是用反射来构建 Bean 的话,就不难发现问题所在:存在两个构造器,都可以调用时,到底应该调用哪个呢?最终 Spring 无从选择,只能尝试去调用默认构造器,而这个默认构造器又不存在,所以测试这个程序它会出错。

本项目代码链接:https://github.com/Zou2021/springboot

如果你觉得文章对你有帮助,那就请作者喝杯咖啡吧☕
微信
支付宝
  0 条评论