孙哥Spring源码第8集

第8集 属性填充

【视频来源于:B站up主孙帅suns Spring源码视频】【微信号:suns45】

8.1 上节回顾

学习总结

  • 学一个东西 应该学透彻,纯垒知识是会有问题的,当我们真正进入到开发领域当中,后续我们要注意的越来越专,越来越精,而不是原来一样学一个很大的体现,我们学一个很大的体系的目的是,能够入门,把我们的知识体系化,我们现在的这个阶段,大家很重要的一件事是如何让我们的知识更加有深度?不能为了赶课进行学习。

MargedBeanDefinition合并bean的定义

  • 目的:解决父子bean的问题。
  • 不仅仅考虑父子容器,父子bean在父子容器中的出现这是更为复杂的情况。
  • 如果出现这种的话,按这个流程走下去,还得出现一个完整性的递归。
  • 当MargedBeanDefinition之后就具备创建了对象的能力,实际上我们可以去创建对象了实际上可以去创建一个doCreateBean这个方法去创建对象,那创建对象的过程实际上又是一个比较复杂的体系化的工作,作为这个体系化的工作,包括什么呢?
    • 首先包括before的操作,首先就会create,接着就是after的操作。大概会完成这些内容的处理工作,这是我们所说的在创建对象的时候我们实际上需要考虑的一个问题,我们只针对的是单实例bean,有其他形式的bean呢,也会通过类似的方式进行创建,在创建bean的过程当中,还是需要考虑到几个分支,这几个分支主要就是我们说的scope,这块我们实际上还要考虑对象的一个实际情况,又可能会出现多种创建方式,比如说singleton prototype 其他scope,研究到这里呢,我们就可以去探究createBean是如何去处理的了,它会分为创建对象、填充属性、初始化。
  • 在整个MargedBeanDefinition之前的部分实际上是spring在创建对象过程中的一个查找,那么在这之后才是真正的创建对象 ,所以我们一直强调doGetBean其实是有两部分工作的 这两部分工作分别是什么呢?对象的创建,和对象的查找,实际上他先完成的是对象的查找,进而才会完成对象的创建工作,我们现在关注的是对象的创建过程。
  • 回看上次笔记,在getSingleton当中,我们已经研究完了lamba表达返回createBean->doCreateBean
  • doCreateBean 我们猜想的是 创建对象 属性填充 实例化,进入doCreateBean的方法中,整个的代码还是比较多的,我们看主干。

8.2 createBeanInstance 解析

8.2.1 怎么实例化一个对象

核心是createBeanInstance创建Bean实例,最后给我们返回了一个instanceWrapper,这个Wrapper在上面声明了,那它的类型是什么呢?BeanWrapper,这里其实是有两个问题是需要关注的。第一个问题就是 研究一个新框架的源码时候,研究起来非常乱,抓不住重点,我不能够像老师一样在很多代码中 挑出主干,为什么我不能呢?要想确定核心代码,我要告诉各位的是,确定核心代码是有两个非常重要的技巧的,一个技巧是望文知义 通过方法名字来猜,通过注释来猜,这只是针对Sprng的时候,当确定完是主干的时候可以通过debug的方式去验证这个问题,需要去看Debug窗口
经过这样的分析doCreateBean里面最核心的方法就是createBeanInstance帮我们完成对象的创建

8.2.2 创建的对象为什么返回BeanWrapper

孙哥Spring源码第8集_第1张图片
  • 这也就是说我们通过createBeanInstance完成了对象的创建但是同时对对象进行了一层包装,看BeanWrapper叫做包装,它肯定是包装了User,User的功能它肯定是具备的,同时我们再包一层的目的是什么?显然是进行了功能性的扩展。那这个功能性的扩展是什么呢?

  • 接下来分析,找到BeanWrapper的定义,看看它的实现类BeanWrapperImpl

  • 这个rootInstance和rootClass代表了什么呢?

  • rootInstance是User类,rootClass是User类型,还有一个

  • propertyValues【Map】propertyValues【PropertyValues】

8.2.3 为什么要设计PropertyValues?

考虑一个问题,日后在实战当中假定有个类叫User,User中有两个属性 一个是id 一个是name,如果想使用的话应该怎么办?

先获取User对象,通过user的相关方法获取对应的属性值,比如说通过user.getId(),通过user.getName(),来获取。这样调用起来是比较繁琐的,对于wrapper的封装思想就是把User对象存储到Wrapper中,同时把id和name也存储到Wrapper中,这样为了方便程序直接操作对象的属性。

8.2.4 spring访问属性的原因以及目的

  • spring要使用这些属性的目的是什么呢? 一方面是用这个属性 进行属性填充,另外一方面一定会设计到一个问题就是类型转化器。

  • 作为类型转换器来讲他的核心作用是什么呢?把spring当中配置文件中的字符串信息,转换成我们所说的对应对象属性的实际类型。

  • 我有一个User对应的可能会有一个xml 里面配置了bean标签有id和name值,此时配置文件中id的value值是字符串类型,此时在User当中持有的属性是什么类型呢?User中的id是Integer类型,我们应该在注入的过程当中把 integer的1填入进去而不是字符串的1填入进去,显然这是不行的,在spring中这么写了它也是没有问题的,它仍然能从string类型转换成integer类型,这里设计到了类型转换器,实际上我们说的wrapper进行对象的包装,很重要的一部分原因就是为了把这个属性对应的类型转换器也放置在Wrapper中。

  • 这个Wrapper实际上是为了既存有对象本身,又存有对象的属性所需要的类型转换器。怎么证明这一点?

    • 在 BeanWrapper中有这么一个东西

    • conversionService作用就是做自定义类型转换器,typeConverterDelegate 类型转换的一个委托对象。

    • 整个包装beanWrapper的一个核心就是既把对象存起来又把它的属性获取存起来 同时还要匹配类型转换器。

  • 有些同学会问,你老给我讲配置文件,那我要是基于注解该怎么办呢?基于注解为id赋值,我应该怎么赋值?如果要基于注解的话必须要加一个@Value注解它会读取对应properties相关配置文件的内容。

    app.properties
    user.id=1
    @Value(${user.id})
    public Integer id;
    还需要注意的是加@PropertySource(配置文件名称)这个注解   
    
    

    采用注解跟配置文件是一样的,你配置文件存的是字符串,你也需要转成integer类型。

    封装wrapper的目的就是把类型与之匹配的类型转换器进行配对儿式的封装便于后续的操作。

8.2.5 类型转换器

既然提到类型转换器,必须让大家有一个意识

有两种类型转换器

8.2.5.1、内置的类型转换器

Stirng->Integer

String->List

String->数组类型

这些都是内置类型转换器可以解决的问题,spring内置好了。

8.2.5.2、自定义类型转换器

string->Date 日期类型

8.2.5.3、类型转换器的开发讲解
  • 从类型转换器的开发来讲
    • 早期:PropertyEditorSupport 注册到CustomeEditorConfigure
    • 现在:Converter接口,这个bean注册的名称必须是conversationService
    • 这个beanWarpper里面实际上就是通过conversationService的方式来绑定的类型转换器,这两种方式spring都是支持的。
  • 到现在为止,大家梳理了一个很清晰的脉络,就是为什么通过我们所说的刚才看到的 createBeanInstance创建的实例为什么要对其进行包装,进行包装的原因很简单,不仅获取了实例相关的内容,同时还在这块进行了类型转换器的封装或者说是自定义类型转换器的封装,创建的对象会封装成一个叫做BeanWrapper的对象,目的就是 既要存创建的对象 要想其把对应的类型转换器一起封装起来便于后续的操作。
  • 目的: 把创建的对象和自定义的类型转换器 统一封装,便于后续使用。
  • 这个封装是见仁见智的,是他自己的封装逻辑。

8.2.6 createBeanInstance如何创建对象

孙哥Spring源码第8集_第2张图片

因为有了这个对象之后它才可以进行封装,那么是怎么创建的呢?显然需要对象的描述信息,也就是我们说的BeanDefinition,这个方法一定是需要BeanDefinition,作为创建对象的参数传进去的,只有有了BeanDefinition的描述信息之后它才能按照这个描述信息把对象给我们创建出来,有了BeanDefintion之后,我们大概能推断出来 它肯定是通过反射的方式来创建对象。

8.2.6.1 createBeanInstance方法详解
孙哥Spring源码第8集_第3张图片
  • 经过debug发现 最终的核心逻辑是 instantiateBean(beanName,mbd) 把无参的构造方式作为默认的实现。
  • 创建对象的过程中,核心的代码 一块是 instantiateUsingFactoryMethod(beanName,mbd,args) 通过工厂方法创建对象,
    • image-20230508110908729
  • 另外一块 instantiateBean(beanName,mbd)是通过默认的无参构造来创建对象。
    • image-20230508110928423
8.2.6.2 instantiateBean方法讲解

进入到instantiateBean这个方法中,spring容器当中很少做安全校验,核心是getInstantiationStrategy().instantiate(mbd,beanName,parent)来创建实例然后就进行了 BeanWrapperImpl的封装 最终返回给客户。

  • 孙哥Spring源码第8集_第4张图片
8.2.6.3 getInstantiationStrategy().instantiate()讲解
孙哥Spring源码第8集_第5张图片 孙哥Spring源码第8集_第6张图片

进入getInstantiationStrategy().instantiate()这个方法中找到instantiateClass再进去找到ctor.newInstance(args)把你的构造参数给我,我调用构造方法来创建对象,这是核心。找到层层包装,底层还是调用了反射,这个代码我们自己写行不行,我们自己写肯定是可以的,测试获取反射对象。

//无参构造
Clazz clazz = Class.forName("")
Constcutor ctor = clazz.getDeclaredConstructor();    
User user = (User)ctor.newInstance()
8.2.6.4 总结
孙哥Spring源码第8集_第7张图片

查看被包装的BeanWrapper,查看rootObject就是封装的User对象属性都是null目前只是完成了对象的创建,还没有进行填充。

查看typeConverterDelegate、conversionService是一种转换器,

customEditors、customEditorsForPath、customEditorCache是旧的类型转换器。

还有默认的类型转换器 defaultEditors

整个这么多属性除了这个与被包装的wrappedObject对象相关的一些属性之外,剩下的这些属性就都是以类型转换器相关的。

当讨论完创建对象这块的话

8.3 属性的填充

属性填充的核心是 populate() 关注点应该放在哪呢?
属性填充说的通俗一点就是set注入。

8.3.1 三种注入

我们整个的注入其实上是分为 三种的,
一种 是set注入,一种是自动注入,一种是构造注入。
而构造注入显示是调用构造方法 也就是createBeanInstance在创建对象的同事完成构造注入。

填充属性这块不需要讨论构造注入,自动注入是在populateBean中是有所体现的,但是这部分源码我们要选择性的忽略
为什么要忽略,是因为我们现在已经没有人在使autowired/default-autowired,这部分我们永远都不可能用,可以选择性忽略不看。
最后筛来筛去,无外乎整个的注入实际上主要是set注入。

8.3.2 set注入从形式角度分析-这是第一个考虑的问题

  • 那这个时候在看一下set注入这块,它应该是怎么来做的?

  • 首先从形式角度它分为两种

    • 1、 xml 通过
    • 2、注解形式 @Autowired @Value @Inject @Resources 这都是一套体系。
  • 从形式上来讲分为xml类型和注解类型

  • 从属性的类型上来讲又会分为 JDK所提供的,还有一种是用户自定义的类型/自建的类型

    • @Value等同于JDK类型

    • @Autowired 等同于自建类型的注入或者就是property ref的那种方式

  • 既要包括 xml的解析,xml的解析包括JDK的也要包括自建的。同时也要兼顾注解 也要考虑JDK和用户自定义

8.3.3 set注入从类型转化角度分析-这是第二个考虑的问题

第二个在属性填充过程中要注意的问题是什么呢?类型转换,刚才在BeanWrapper里面封装好了这个bean,实际上这个数据和对象的数据是分开的,本身的数据和对象的数据是分开的,它没有填充,一旦发生了填充,一定会涉及到类型转化,再回顾一下刚才的BeanWrapper,重新dubug再过一遍 查看instanceBeanWrapper查看rootObject,此时发现对象是空的说明还没有赋值,那么属性数据都存在哪了呢?找到它的描述符,就是BeanDefinition mdb,打开这个mbd,是没有一个成型的User对象,但是它里面会有User对象的描述信息,包括对象有什么样的属性,有什么样的数据,查看它的propertyValues里面有propertyValueList 里面有 name 和password这是它的属性,还有它的值在哪呢?name值在propertyValue里面在value里面,value是一个TypedStringValue类型的,password的value也是TypedStringValue类型。添加一个 Interger类型的id 值为1,发现这个id的value类型也是TypedStringValue,值是字符串1。
而最终这个值,我们肯定要赋给Integer id,显然会涉及到类型转化,在这个环节里面没有类型转化,mbd是在这拿的还没有到类型转化的工作,只是把对象给我们了,属性的数据还在mbd里面,那么在哪发生了转化?是在populateBean()发生了转化,所以此时发现在填充属性的过程当中,为什么需要这个instanceWrapper对象,我想为user填充数据,当然会需要user对象,有这个instanceWrapper就够了,但是user成员变量的数据是在mbd MergeBeanDefine中存着呢,所以它也必须给populateBean当做参数,刚才查看的id是字符串1,也就是说populateBean里面一定会涉及到字符串1变成Integer1 给user赋值,在填充属性的过程当中,永远不可能规避的一个核心是什么?涉及到类型转换,这个类型转换一定会涉及到 内置类型转换器,如果 配置了自定义类型转换器的时候 也会在这个populateBean中进行调用。

以上就是我们所说的在属性填充中要整体考虑的内容。

8.4 populateBean()解析

了解到populateBean的整个作用以及核心细节之后,然后进入方法中查看细节

1、先获取到mbd中的propertyValues

2、自动注入的处理,现在很少处理自动注入相关的,基本上没人使用 autowire=“byType/byName”,即使退1w步,注解没有成为主流,也没有人会使用。

3、判断注入形式是注解的还是property标签

4、基于注解的形式为你的成员变量完成注入一大块逻辑,通过beanPostProcessor的方式完成了基于注解的注入@Value @Autowired,我们当时在讲spring的BeanPostProcessor的时候不管是外部扩展还是内部扩展都是非常重要的。

用的是beanPostProcessor的形式,但是需要关注的是beanPostProcessor在spring的内部有很多,通过AutowiredAnnotationBeanPostProcessor完成了@Value @Autowired的赋值操作,你会发现它整个代码是需要循环的,但是只有在AutowiredAnnotationBeanPostProcessor处理的时候才会进行赋值,而赋值的核心是什么?ibp.postProcessProperties()

5、校验

6、pvs就是我们的属性值信息 applyPropertyValues(beanName,mbd,bw,pvs)非注解的属性填充

先关注非注解的属性填充

1、安全校验

2、获取PropertyValues赋值给original

3、有没有自定义类型转换器,没有就是用BeanWrapper

4、创建BeanDefinitionValueResolver valueResolver负责类型转换,自定义转换器是null,那它解决的一定是内置类型转换。

5、遍历PropertyValue 发生类型转换,拿属性name【id】,拿类型转换器value【TypeStringValue】,从类型转换器value中把value提出来【“1”】,拿转化好的value【把字符串1转换成integer 1】,判断是不是final 是否可写, convertForProperty()负责转换工作【把字符串 1 变成Integer 1】

6、把处理好的值 赋值给PropertyValue,循环结束标记转换完成

7、把转换好的数据交给bw,此时再看 发现User的属性 就有值了。内部肯定是通过反射完成的,根据name和value进行反射赋值。

你可能感兴趣的:(spring,java,后端)