当前位置: 首页 > article >正文

【Spring】定义的Bean缺少隐式依赖

问题描述

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

    @Service
    public class ServiceImpl {
    
        private String serviceName;
    
        public ServiceImpl(String serviceName){
            this.serviceName = serviceName;
        }
    
    }
    
  • ServiceImpl 因为标记为 @Service 而成为一个 Bean。另外我们 ServiceImpl 显式定义了一个构造器。但是,上面的代码不是永远都能正确运行的,有时候会报下面这种错误:

    Parameter 0 of constructor in com.spring.puzzle.class1.example2.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, beanName);
    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, paramNames,
          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 的隐式规则,在修正问题后,我们可能写出更多看似可以运行的程序,代码如下:

    @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 无从选择,只能尝试去调用默认构造器,而这个默认构造器又不存在,所以测试这个程序它会出错。


http://www.kler.cn/a/513894.html

相关文章:

  • 基于Python django的音乐用户偏好分析及可视化系统设计与实现
  • 【游戏设计原理】75 - 最小最大化
  • QT多语言Demo及心得
  • SDL2:Android APP编译使用 -- SDL2多媒体库使用音频实例
  • 游戏AI,让AI 玩游戏有什么作用?
  • 图片生成Prompt编写技巧
  • 解决npm install安装出现packages are looking for funding run `npm fund` for details问题
  • Spring中的事件和事件监听器是如何工作的?
  • GAN 用于图像增强
  • HTML新春烟花
  • 【25考研】考清华的软件工程专业的研究生需要准备什么?
  • 论文速读| A Survey on Data Synthesis and Augmentation for Large Language Models
  • 图片专栏——曝光度调整相关
  • 如何设置HSTS和OCSP Stapling?
  • js高阶-响应式原理
  • 线性规划:机器学习中的优化利器
  • NodeJs如何做API接口单元测试? --【elpis全栈项目】
  • Vue3初学之商品的增,删,改功能
  • Windows下建立Jupyter-lab 编程环境
  • STM32单片机:GPIO模式
  • gitlab使用多数据库
  • 知识图谱中的word2vec 技术是做什么的?
  • 机器学习10-解读CNN代码Pytorch版
  • Flink 使用 Kafka 作为数据源时遇到了偏移量提交失败的问题
  • matlab实现数据极坐标显示
  • 【深度学习】关键技术-模型训练(Model Training)