3.创建带参bean对象

🍈 3.创建带参bean对象

本文主要是在上一章的基础上对上一章中代码存在的问题进行改造,上一章中将bean对象的创建交给了IOC容器,利用反射创建bean对象并保存到IOC容器中,但是忽略了一点,上章中代码只能创建不携带参数的bean对象,所以这一章主要解决的问题就是创建有参数的bean对象,正常的spring创建带参的bean对象时,参数应该注册到了BeanDefinition中,然后先创建然后属性填充,但是本节中我们直接一步到位,简化bean的创建过程。具体的代码在 仓库

原因

​ 为什么上一章中只能创建不带参数的bean对象呢,主要问题出现在createBean函数中,可以查看createBean的代码就能够发现问题:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
@Override
protected Object createBean(String beanName, BeanDefinition beanDefinition) throws BeansException {
    Object bean;
    try {
        //利用反射,根据传递来的类信息创建一个实例化对象
        bean = beanDefinition.getBeanClass().newInstance();
    } catch (InstantiationException | IllegalAccessException e) {
        throw new BeansException("Instantiation of bean failed", e);
    }

    //在这里就是创建成功
    //调用继承自DefaultSingletonBeanRegistry中的addSingleton方法将这个对象保存到容器中
    addSingleton(beanName, bean);
    //将创建的bean返回,返回值用于getBean的返回值
    return bean;
}

​ 在第六行中,直接利用反射,调用newInstance方法创建一个bean对象,此处调用时并没有区分bean对象是否携带参数,导致创建出来的bean对象全都是无参的,所以需要改进的地方就在这里,将这里创建bean对象的代码进行扩充,接收bean对象的参数,就可以在创建bean对象时创建携带参数的bean对象了,其实原始spring框架中的bean对象参数是通过后期属性注入填充的,而不是创建开始就携带参数。在这里为了创建带参bean对象,对于构造函数的选取就需要进行匹配,根据传递的参数列表从而找到匹配的构造函数,创建bean对象时将参数传入匹配的构造函数即可

思路

为了实现上面描述的:可以创建携带参数的bean对象,需要解决两个问题:

  1. 如何接受或者如何传递bean对象的参数
  2. 如何对现有项目合理的扩展,实现可以创建携带参数的bean对象

​ 为了解决第一个问题,我们可以在获取bean对象的时候就传递一些参数,这样既可以用参数区分获取的对象是谁,又可以在没有目标bean对象时,根据传递的参数创建一个目标bean对象。而对外暴露的获取bean对象的接口为getBean,所以第一个问题的突破口就在getBean函数上

​ 为了解决第二个问题,可以在createBean的函数中进行改造,引入一个扩展类,用来实现根据不同的参数匹配不同的构造函数,创建不同的bean对象,对于创建bean对象来说,有两种方式:

  1. 利用JDK反射机制
  2. 利用CGlib字节码机制

所以我们可以单独建立一个实例化模块,专门用来创建bean对象,根据接受的参数不同创建不同的bean对象,对外暴露一个创建bean对象的接口,然后createBean方法中不是简单的调用newInstance方法,而是调用这个封装好的接口来创建目标对象,形成的结构图为:

图 4-1

形成的新的类图如下,可以发现只是多了左边的一个实例化模块,然后在原有的类结构的基础上增加了一些方法,主要是对外提供的接口可以接受bean对象的参数了,并且获取bean对象可以传递参数

image-20231031124847218

在这个类结构图的基础上,对变化的类进行说明,没有描述的部分请参考第二章的 文档

类的说明

  1. InstantiationStrategy:是一个接口,主要提供了创建带参的bean对象的接口instantiate(BeanDefinition beanDefinition, String beanName, Constructor ctor, Object[] args),四个参数的含义分别代表bean对象的类信息,bean对象的名称,创建bean对象用到的构造函数,构造函数中的参数。在createBean方法中调用这个方法之后,就可以得到携带参数的bean对象,接口的结构为:

    image-20231031125857597

    这是本章节的核心接口,有两个实现类,从而有两种创建带参bean对象的方式

  2. SimpleInstantiationStrategy:是InstantiationStrategy的其中一个实现类,也是项目中第一个实例化bean对象的策略类。内部使用JDK的反射机制来创建携带参数的bean对象,类的结构为:

    image-20231031125924252

  3. CglibSubclassingInstantiationStrategy:是InstantiationStrategy的另外一个实现类,也是项目中第二个实例化bean对象的策略类。内部使用了ASM字节码框架来创建携带参数的bean对象,类的结构为:

    image-20231031125940874

    注意:要使用CGlib创建bean对象,需要引入cglib的依赖

    1
    2
    3
    4
    5
    
    <dependency>
        <groupId>cglib</groupId>
        <artifactId>cglib</artifactId>
        <version>3.3.0</version>
    </dependency>
    
  4. AbstractAutowireCapableBeanFactory:创建bean对象的类,在原有的基础上保存了一个成员变量,来代表当前使用哪一种实例化策略,默认使用的是CGlib的策略,并且createBean方法中不再是直接创建bean对象,而是调用了一个createBeanInstance方法,原先的类的结构为:

    image-20231030102532007

    新的类的结构为:

    image-20231031130058902

    红色圈起来的部分是新增的重点,实现了按照指定的策略创建带参的bean对象的功能

  5. AbstractBeanFactory:原来的类中,getBean方法中先尝试直接获取bean对象,否则直接调用createBean方法创建bean对象,现在将这些工作交给了doGetBean方法,又封装了一层,新的结构为:

    image-20231031130335141

    将获取Bean的操作抽取出来变成doGetBean方法的目的是为了减小代码量,因为两个重载版本的getBean方法都需要获取bean对象,只是传递的参数不一样,所以将这部分代码抽取出来,后期调用时传递的参数不一样即可:

    image-20231031133443399

以上就是本节中的代码比与上一节多出来或者修改的部分,目的是为了实现创建带参的bean对象

bean的创建和获取

为了获得带参数的bean,最外层获取bean的时候就需要传递参数,根据参数的不同来匹配不同的bean对象,初始状态下,会利用传递来的参数创建一个新的带参的bean对象保存到IOC容器中并返回,整体的流程为:

未命名文件

getBean方法中可以携带bean的参数,并且在创建bean实例的时候,需要找到匹配的构造函数,具体的函数逻辑在createBeanInstance中

上一章中获取bean对象的流程为:

image-20231030114408629

可以看出,相比于上一章,在createBean的时候增加了两步,本章中按照实例化策略,根据接收的参数来创建带参的bean对象,其整体步骤还是与上一章保持一致

​ 核心就是如何利用传递来的参数来创建带参bean对象,主要是在增加的两个方法createBeanInstanceinstantiate中,前一个方法通过参数列表的类型匹配到目标构造函数,然后将目标构造函数传递给instantiate方法,在instantiate方法中完成创建,createBeanInstance方法的代码如下:

 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
private Object createBeanInstance(BeanDefinition beanDefinition, String beanName, Object[] args) {
    //如果参数列表为空,直接调用实例化策略创建无参对象并返回
    if (args == null)
        return getInstantiationStrategy().instantiate(beanDefinition, beanName, null, null);

    //在这里说明需要创建带参bean对象
    //1.尝试找到目标构造函数
    Constructor constructorToUse = null;

    //获取bean对象的类信息
    Class beanClass = beanDefinition.getBeanClass();
    //获取到这个bean对象的所有构造函数
    Constructor[] declaredConstructors = beanClass.getDeclaredConstructors();
    //遍历所有的构造函数,用参数列表去匹配,从而找到目标构造函数
    for (Constructor declaredConstructor : declaredConstructors) {
        //获取到当前构造函数的参数列表
        Class[] parameterTypes = declaredConstructor.getParameterTypes();
        //参数个数都不一样,肯定不匹配,继续查找下一个
        if (parameterTypes.length != args.length)
            continue;
        //参数个数一样,看类型是否一样
        int i = 0;
        for (; i < parameterTypes.length; i++) {
            //参数列表的类型不匹配,继续搜索合适的构造函数
            if (parameterTypes[i] != args[i].getClass())
                break;
        }
        //参数列表匹配到了末尾说明找到目标构造函数
        if (i == parameterTypes.length) {
            constructorToUse = declaredConstructor;
            break;
        }
    }
    //在这里创建带参的bean对象
    return getInstantiationStrategy().instantiate(beanDefinition, beanName, constructorToUse, args);
}

可以看出,核心就是利用反射得到当前bean对象的所有构造方法,然后再得到每一个构造方法的参数列表,对参数列表进行匹配,需要注意的是,参数匹配时,由于getBean接受参数时使用的是Object数组,所以传递int参数时会将其转换成Integer,故而匹配参数时会出现问题,所以bean对象的构造函数中,不使用基本类型存储参数,在真正的spring中,并不使用这种方式创建带参的bean对象,而是先创建再属性填充的方法,属性填充

总结

为了创建带参数的bean,主要改进了getBean方法和createBean方法,前者使得程序可以接受bean对象的参数,后者可以利用这个参数创建带参的bean对象,内部调用了新增的实例化模块,默认使用CGlib的方式创建bean对象,参考的类图如下:

图 4-2

创建bean对象的过程中存在一些问题,但是整体上还是可以创建带参的bean对象了,主要原理就是按照传递来的参数来找到目标有参构造函数,从而创建带参bean对象

总结来说就是做了两点:

  1. 获取bean对象时允许传递参数
  2. 创建bean对象时允许根据传递来的参数列表找到匹配的构造函数从而直接创建带参bean对象