spring5源码 -- IOC容器设计理念和核心注解的作用

时间:2022-07-25
本文章向大家介绍spring5源码 -- IOC容器设计理念和核心注解的作用,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

可以学习到什么?

0. spring整体脉络

1. 描述BeanFactory

2. BeanFactory和ApplicationContext的区别

3. 简述SpringIoC的加载过程

4. 简述Bean的生命周期

5. Spring中有哪些扩展接口及调用机制

一. spring源码整体脉络介绍及源码编译

1.1. 什么是IOC

ioc是控制反转, 这是一种设计理念, 用来解决的是层和层之间, 类和类之间的耦合问题.

比如,现在有A, B两个类, 在A类中引用了B类. 那么如果有一天, B类要被替换掉, 我们会怎么办呢?如果B类被引用了100次, 我们要替换100次?

现在呢, A是直接调用B, 如果我们间接的调用B, 将B包装起来, 如果以后将B换成C, 只需要在包装类里面替换就可以了. 我们不需要修改A类. 这就是控制反转.

Spring使用了ioc, Spring.ioc(A, B) 将A和B的引用都存在ioc中, spring会帮我们维护好, 完全不用担心.

当我们在A中要使用B的时候, 使用B对应的接口, 然后使用@Autowired注解

A {
   @Autowired
   private IB b;  
}

什么时候把B换掉了, 不痛不痒的, 只需要把新的类放到IoC中就可以了.

1.2. Spring源码的整体脉络梳理

Spring IoC是一个容器, 在Spring Ioc中维护了许多Bean

那这些bean是如何被注册到IoC中的呢? 换句话说, 我们自定义的类, 是如何作为一个bean交给IoC容器去管理的呢?

先来回忆,我们在开发spring的时候的步骤:

第一步: 配置类. 配置类可以使用的方式通常由
      1) xml配置
      2) 注解配置
      3) javaconfig方式配置
第二步: 加载spring上下文
      1) 如果是xml, 则new ClassPathXmlApplicationContext("xml");
      2) 如果是注解配置: 则new AnnotationConfigApplicationContext(config.class)

第三步: getBean() 
我们会讲自定义的类, 通过xml或者注解的方式注入到ioc容器中.

在这一步, 会将xml或注解中指定的类注入到IoC容器中.

1.2.1 那么, 到底是如何将一个类注入到ioc中的呢?

下面就来梳理一下整个过程.

第一问: 一个类要生产成一个Bean, 最重要最核心的类是什么?

是BeanFactory

第二问: BeanFactory是什么呢?

BeanFactory是Spring顶层的核心接口--使用了简单工厂模式. 通常都是根据一个名字生产一个实例, 根据传入的唯一的标志来获得bean对象, 但具体是穿入参数后创建, 还是穿入参数前创建, 这个要根据 具体情况而定, 根据名字或类型生产不同的bean.

一句话总结: BeanFactory的责任就是生产Bean

来看下面这段代码:

public static void main( String[] args ) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
        context.getBean("***");
    }

这段代码实现的功能是, 读取当前文件所在目录及其子目录中的文件, 然后获取指定名称的bean, 整个流程如下图所示:

首先, 通过ClassPathXmlApplicationContext或者AnnotationConfigApplicationContext去读取配置,

然后将其交给BeanFactory.

第三. BeanFactory调用getBean()方法, 将Bean注入到IoC容器中

我们发现, 配置的读取, 可能是xml方式, 也可能是annotation的方式, 不同的方式读取应该使用的是不同的工具. 那么这些工具读取的结果应该是统一的, 然后才能交给BeanFactory去处理.

因为在BeanFactory中是不会对这些异同点进行处理的. BeanFactory的作用只有一个, 就是个生产Bean.

1.2.2 那么, 不同的工具读取配置是如何统一的呢?

我们知道,读取配置这一块, 应该会有一个不同的实现. 将xml和注解方式读取成统一的东西, 放入到beanFactory中. 这个东西是谁呢?就是BeanDefinition(Bean定义)

什么意思呢? 如下图:

看绿色框框住的部分. 这个含义是: 通过不同的工具, 可能是xmlApplicationContext, 可能是annotationApplicationContext工具 读取的配置, 最后都会构造成BeanDefinition对象. 然后将BeanDefinition传递给BeanFactory, BeanFactory统一处理BeanDefinition对象, 调用getBean()方法, 将其放入IoC容器中.

1.2.3 那么又是是如何读取配置统一构造成BeanDefinition的呢?

我们来举个例子, 现在有一个人, 比如说我刚买了一个房子, 我要装修. 需要一个衣柜, 这时候, 我会找到一个衣柜店. 然后告诉他我的需求, 柜子的颜色, 款式格式什么样. 然后衣柜店记录我的需求, 这个时候, 他不会自己生产, 他会通知工厂, 让工厂来生产. 工厂按照什么生产呢, 衣柜店有一个设计师, 他们的设计师. 会按照我的需求设计出一张图纸. 然后将图纸交给工厂. 工厂按照图纸要求生产Bean.

整个过程如下图:

入口是"我"

1. 我有一个需求, 打一个柜子, 找到衣柜店

2. 我告诉衣柜店我的需求, 柜子的颜色, 款式, 然后衣柜店的设计师按照我的要求 ,设计出一张图纸

3. 衣柜店将图纸给到工厂, 工厂按照图纸生产柜子

这是制造衣柜的过程. 其中在画图纸的时候, 画一张就给工厂给一张, 这样效率太低了. 我们可以画了n张, 一起给工厂. 所以, 在设计图纸这块是一个容器, 存放多张图纸

后面,如果我还想定制一个橱柜店. 那么, 就告诉设计师我的橱柜的颜色,款式, 就可以了. 流程和上面都是一样的.

整个这个过程, 就类似于我们的bean生产的过程

1. 定义了一个带有@Component注解的类, 我找到衣柜店, 衣柜店就类似于ApplicationContext.

2. 我告诉ApplicationContext我的需求, 我要懒加载@Lazy, 设置单例模式还是多例模式@Scope. 对应的就是定制柜子的颜色,款式. 然后衣柜店里的设计师BeanDefinitionRegistry根据我的需求设计出图纸, 也就是构造成BeanDefinition. 不同的BeanDefinitionRegistry设计出不同的BeanDefinition, 然后将他们都放在容器中.

3. 衣柜店ApplicationContext统一将一摞图纸BeanDefinitionMap交给工厂, 然后工厂按照要求生产Bean, 然后将生成的bean放入到IoC容器中.

这是一个带有@Component的类被加载的过程.

衣柜店要要想生意好, 那么他要去拉活呀, 所以还需要好的销售. 销售要去扫楼盘, 去联系, 哪些人有装修的需求. 挨个询问.

可是问了100个人, 可能只有10个人有装修的需求. 于是还要有一个接待, 这个接待要联系客户, 看看哪些是有意向的客户, 将其筛选出来. 然后定制家具.

这里多了两类人: 销售和接待. 具体工作如下.

销售就相当于我们的BeanDefinitionReader, 他的作用是去扫楼盘, 找到潜在客户. 对应的就是BeanDefinitionReader去读取xml配置或者Annotation注解.

xml中的配置有很多, 注解也有很多, 并不都是我们的目标. 于是有了接待

接待要去扫描所有潜在客户. 将有意向的客户扫描出来. 这就类似于我们的BeanDefinitionScanner, 去扫描潜在客户, 最后将带有@Component注解的类筛选出来

这就是后面需要定制家具的客户了

BeanDefinitionReader对应的就去读取配置类, 看看有哪些需求需要搞装修.


它本身也是一个抽象类, 可以看到他有AnnotationBeanDefinitionReader和XmlBeanDefinitionReader


我们配置了配置包, 去扫描这个包下所有的类, 然后将扫描到的所有的类交给BeanDefinitionScanner, 它会去过滤带有@Component的类. 

在和上面的流程连接起来, 就是整个配置文件被加载到IoC的过程了.

1.3. ApplicationContext和FactoryBean的区别

1. FactoryBean的功能就是生产bean. 他生产bean是根据BeanDefinition来生产的. 所以, 一次只能生产一个

2. ApplicationContext有两种. 一种是xmlApplicationContext, 另一种是annotationApplicationContext, 他传入的参数是一个配置文件. 也就是可以加载某个目录下所有带有@Component的类

他们两个都各有使用场景. 使用ApplicationContext的居多.

另一个区别: 就是后面会说到的, ApplicationContext有两个扩展接口, 可以用来和外部集成. 比如和MyBatis集成.

1.4. Bean的生命周期

如上图, beanFactory拿到BeanDefinition, 直接调用getBean()就生产Bean了么?

不是的, 生产Bean是有一个流程的. 下面我们来看看Bean的生命周期

第一步: 实例化. bean实例化的时候从BeanDefinition中得到Bean的名字, 然后通过反射机制, 将Bean实例化. 实例化以后, 这是还只是个壳子, 里面什么都没有.

第二步: 填充属性. 经过初始化以后, bean的壳子就有了, bean里面有哪些属性呢? 在这一步填充


第三步: 初始化. 初始化的时候, 会调用initMethod()初始化方法, destory()初始化结束方法

这个时候, 类就被构造好了.

第四步: 构造好了的类, 会被放到IoC的一个Map中. Map的key是beanName, value是bean实例. 这个Map是一个单例池, 也就是我们说的一级缓存


第五步: 我们就可以通过getBean("user"), 从单例池中获取雷鸣是user的类了.

在构造bean的过程中, 还会有很多细节的问题, 比如循环依赖.

A类里面调用了B类, 所以BeanFactory在构造A的时候, 会去构造B. 然后在构造B的时候, 发现, B还依赖了A. 这样, 就是循环依赖. 这是不可以的.

Spring是如何解决循环依赖的问题的呢?

设置出口. 比如A在构造的过程中, 那么设置一个标记, 正在构造中. 然后构造B, B在构造的过程中应用了A, 这时候, 有趣构造A, 然后发现A正在构造中, 那么, 就不会再次构造A了.

后面还会详细讲解Spring是如何解决循环引用的. 这里我们需要知道的是: Spring使用的是三级缓存来解决循环引用的问题

其实, bean是存在一级缓存里面, 循环引用使用的是三级缓存来解决的. 其实, 一、二、三级缓存就是Map。

1.5. Spring中的扩展接口

有两个非常重要的扩展接口. BeanFactoryPostProcessor(Bean工厂的后置处理器) 和 BeanDefinitionRegistryPostProcessor

这两个接口是干什么的呢?

我们在这个图里面, 看到了设计师要设计出图纸, 然后把图纸交给工厂去生产. 那么设计师设计出来的图纸, 有没有可能被修改呢?

当然是可以被修改的. 只要还没有交给工厂, 就可以修改.

BeanFactoryPostProcessor(Bean工厂的后置处理器)的作用就是修改BeanDefinition.

1. BeanFactoryPostProcessor: 修改BeanDefinition.

是一个接口, 我们的类可以实现这个接口, 然后重写里面的方法

public class DefinedPost implements BeanFactoryPostProcessor {

    /**
     * 重写Bean工厂的后置处理器
     * @param beanFactory
     * @throws BeansException
     */
    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        // beanFactory 拿到工厂了, 就可以获取某一个Bean定义了
        GenericBeanDefinition car = (GenericBeanDefinition) beanFactory.getBeanDefinition("Car");
        // 拿到了car, 然后修改了Car的类名为com.example.tulingcourse.Tank. 那么后面在获取的Bean里面, 将其转换为Car, 就会报错了
        car.setBeanClassName("com.example.tulingcourse.Tank");
    }
}

第一步: 实现了BeanFactoryPostProcessor接口, 然后需要重写里面的方法

第二步: 我们发现重写方法直接给我们了beanFactory, bean工厂

第三步: 拿到bean工厂, 我们就可以根据名称获取BeanDefinition, 也就是bean定义了.

第四步: 我们修改了bean定义中的类名为Tank.

这时候会发生什么呢? 从bean工厂中构建的car, 取出来以后转换成Car对象, 会报错,

public static void main(String[] args) {
    

        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Application.class);
    
        Car car = context.getBean("car", Car.class); // 这里会报错, 因为已经被修改
        System.out.println(car.getName());
    }

执行流程: 当spring启动的时候, 就会去执行AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(TulingCourseApplication.class);

然后ApplicationContext回去扫描所有实现了BeanFactoryPostProcessor对象的类, 然后执行postProcessBeanFactory方法.

BeanFactoryPostProcessor被使用到的场景非常多, 在集成其他组件的时候, 比如集成mybatis

2. BeanDefinitionRegistryPostProcessor 注册BeanDefinition

这是一个Bean定义注册的后置处理器.BeanDefinitionRegistryPostProcessor本事是实现了BeanFactoryPostProcessor 接口

我们来看个demo

public class DefinedPost implements BeanDefinitionRegistryPostProcessor {

    /**
     * 重写Bean工厂的后置处理器
     * @param beanFactory
     * @throws BeansException
     */
    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        // beanFactory 拿到工厂了, 就可以获取某一个Bean定义了
        GenericBeanDefinition car = (GenericBeanDefinition) beanFactory.getBeanDefinition("Car");
        // 拿到了car, 然后修改了Car的类名为com.example.tulingcourse.Tank. 那么后面在获取的Bean里面, 将其转换为Car, 就会报错了
        car.setBeanClassName("com.example.tulingcourse.Tank");
    }

    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry beanDefinitionRegistry) throws BeansException {

    }
}

一个类实现了BeanDefinitionRegistryPostProcessor, 需要重写postProcessBeanDefinitionRegistry方法, 这个方法直接将BeanDefinitionRegistry就给我们了.

然后使用beanDefinitionRegistry.registerBeanDefinition(); 就可以添加图纸了

在这里可以注册新的bean, 也可以删除注册的bean. 多注册一个, bean工厂就要多构建一个.

总结:

BeanFactoryPostProcessor和BeanDefinitionRegistryPostProcessor这两个扩展类是很重要的类, 这对于向外部扩展起到了很大的的作用, 比如: 集成mybatis

BeanFactoryPostProcessor和BeanDefinitionRegistryPostProcessor是在ApplicationContext中的两个扩展接口. 这也是ApplicationContext和BeanFactory的区别之一, 因为有了这两个扩展节点, 就可以和外部做集成. 比如Mybatis集成. 比如: 扫描配置类, 就是通过 这两个扩展点的方式实现的.

这个扩展点的作用:

1. 除了IoC, 其他的扩展,比如AOP, 和MyBatis集成, 都要用到这两个扩展点. 之所以Spring能够有序不乱的和很多外部组件整合, 都是这两个扩展点的功能

1.6 Bean的扩展点

除了ApplicationContext有扩展点, 在Spring IoC中的bean也有扩展点. BeanPostProcessor(Bean的后置处理器). 如果使用在getBean()之前, 那么可以阻止构建Bean, 还可以自定义构建Bean.

BeanPostProcessor使用的场景有很多. 在Bean实例化之前和之后会被调用. 在填充属性之前和之后也会被调用, 初始化之前和之后也会调用. 有些过程不只调用一次. 整个过程一共会调用9次. 在每一个过程都可以扩展Bean.

思考: Spring加入AOP是如何实现呢?

集成AOP肯定不会和IoC糅合在一块了. AOP就是通过BeanPostProcessor(Bean后置处理器)整合进来的.

AOP的实现方式有两种: 一种是CGLIB, 另一种是JDK.

假如说要进行集成, 会在那个步骤继承呢? 比如要加日志, 使用AOP的方式加. 我们通常是在初始化之后加AOP. 在这里将AOP集成进来.

如上图: 当面试的时候面试官问你, Bean的生命周期, 我们不能只说实例化-->填充属性-->初始化. 还需要说初始化的时候, 还有一些列的aware.

1.7. Spring IOC的加载过程

对照上图, 我们来简述ioc的加载过程

我们将一个类加载成Bean, 不是一步到位的,需要经历一下的过程.

1. 首先, 我们要将类加载成BeanDefinition(Bean定义)

  加载成bean定义, 有以下几个步骤:

  1) 使用BeanDefinitionReader加载配置类, 此时是扫描所有的xml文件或者项目中的注解. 这里面有些使我们的目标类, 有些不是

  2) 使用BeanDefinitionScanner扫描出我们的目标类.

  3) 使用BeanDefinitionRegistry注册bean到BeanDefinitionMap中.

2. 然后, ApplicationContext可以调用BeanFactoryPostProcessor修改bean定义, 还可以调用BeanDefinitionRegistryPostProcessor注册bean定义

3. 将BeanDefinition交给BeanFactory处理, BeanFactory调用getBean()生成Bean或者调用Bean(getBean()有两个功能).

4. 成产bean的时候, 首先会实例化, 然后填充属性(主要是读取@Autowire, @Value等注解). 在初始化Bean, 这里会调用initMethod()方法和初始化销毁方法destroy(). 初始化的时候还会调用一堆的Aware, 而且在bean生成的过程中 会有很多扩展点, 供我们去扩展.

5. 将生产出的Bean放入到Map中, map是一个一级缓存池. 后面, 我们可以通过getBean("user")从缓存池中获取bean