• Spring IOC 原理深层解析


    一、Spring IOC概念认识

    区别IOC与DI

    首先我们要知道IOC(Inverse of Control:控制反转)是一种设计思想,就是 将原本在程序中手动创建对象的控制权,交由Spring框架来管理。这并非Spring特有,在其他语言里面也有体现。IOC容器是Spring用来实现IOC的载体, IOC容器实际上就是个Map(key,value),Map 中存放的是各种对象。

    或许是IOC不够开门见山,Martin Fowler提出了DI(dependency injection)来替代IOC,即让调用类对某一接口实现类的依赖关系由第三方(容器或协作类)注入,以移除调用类对某一接口实现类的依赖。

    所以我们要区别IOC与DI,简单来说IOC的主要实现方式有两种:

    • 依赖查找
    • 依赖注入

    我们DI就是依赖注入,也就是IOC的一种可取的实现方式!对两个概念总结以下:

    • IOC (Inversion of control ) 控制反转/反转控制。是站在对象的角度,对象实例化以及管理的权限(反转)交给了容器。
    • DI (Dependancy Injection)依赖注入。是站在容器的角度,容器会把对象依赖的其他对象注入(送进去)。例如:对象A 实例化过程中因为声明了一个B类型的属性,那么就需要容器把B对象注入到A中。

    通过使用IOC容器可以对我们的对象注入依赖(DI),实现控制反转!

    IOC解决的问题

    通过上面的介绍,我们大概理解了IOC的概念,也知道它的作用。那么也会有疑惑,为什么需要依赖反转呢,有什么好处,解决了什么问题?

    简单来说,IOC 容器就像是一个工厂一样,当我们需要创建一个对象的时候,只需要配置好配置文件/注解即可,完全不用考虑对象是如何被创建出来的。 在实际项目中一个 Service 类可能有几百甚至上千个类作为它的底层,假如我们需要实例化这个 Service,你可能要每次都要搞清这个 Service 所有底层类的构造函数,这可能会把人逼疯。如果利用 IOC 的话,你只需要配置好,然后在需要的地方引用就行了,这大大增加了项目的可维护性且降低了开发难度。

    举个例子:现有一个针对User的操作,利用 Service 和 Dao 两层结构进行开发!

    在没有使用IOC思想的情况下,Service 层想要使用 Dao层的具体实现的话,需要通过new关键字在UserServiceImpl 中手动 new出 IUserDao 的具体实现类 UserDaoImpl(不能直接new接口类)。

    这种方式可以实现,但是如果开发过程中接到新需求,针对IUserDao 接口开发出另一个具体实现类。因为Server层依赖了IUserDao的具体实现,所以我们需要修改UserServiceImpl中new的对象。如果只有一个类引用了IUserDao的具体实现,可能觉得还好,修改起来也不是很费力气,但是如果有许许多多的地方都引用了IUserDao的具体实现的话,一旦需要更换IUserDao的实现方式,那修改起来将会非常的头疼。

    但是如果使用IOC容器的话,我们就不需要操心这些事,只需要用的时候往IOC容器里面“要”就完事。

    二、Spring IOC容器实现

    在IOC容器的设计中,有两个主要的容器系列,一个是实现BeanFactory接口的简单容器系列,这系列容器只实现了容器的最基本功能;另一个是ApplicationContext应用上下文,它作文容器的高级形态而存在。后面作为容器的高级形态,在简单容器的基础上面增加了许多的面向框架的特性,同时对应用环境作了许多适配。

    BeanFactory

    BeanFactory,从名字上也很好理解,生产 bean 的工厂,它负责生产和管理各个 bean 实例。

    我们先来看一下BeanFactory的继承体系

    先介绍一下里面比较重要的一些接口和类

    1. ApplicationContext 继承了 ListableBeanFactory,这个 Listable 的意思就是,通过这个接口,我们可以获取多个 Bean,大家看源码会发现,最顶层 BeanFactory 接口的方法都是获取单个 Bean 的。
    2. ApplicationContext 继承了 HierarchicalBeanFactory,Hierarchical 单词本身已经能说明问题了,意思是分层,也就是说我们可以在应用中起多个 BeanFactory,然后可以将各个 BeanFactory 设置为父子关系。
    3. AutowireCapableBeanFactory 这个名字中的 Autowire 大家都非常熟悉,它就是用来自动装配 Bean 用的(如按名字匹配,按类型匹配等),但是仔细看上图,ApplicationContext 并没有继承它,不过不用担心,不使用继承,不代表不可以使用组合,如果你看到 ApplicationContext 接口定义中的最后一个方法 getAutowireCapableBeanFactory() 就知道了。
    4. ConfigurableListableBeanFactory 也是一个特殊的接口,看图,特殊之处在于它继承了第二层所有的三个接口,而 ApplicationContext 没有。用于扩展IOC容器的定制性!

    ApplicationContext

    ApplicationContext下面有着我们通过配置文件来构建,也是我们的子实现类。先来看一下继承体系

    我们重点了解一下比较主要的实现类:

    • ClassPathXmlApplicationContext从名字可以看出一二,就是在ClassPath中寻找xml配置文件,根据xml文件内容来构件ApplicationContext容器。

    • FileSystemXmlApplicationContext 的构造函数需要一个 xml 配置文件在系统中的路径,其他和 ClassPathXmlApplicationContext 基本上一样。

    • AnnotationConfigApplicationContext 是基于注解来使用的,它不需要配置文件,采用 Java 配置类和各种注解来配置,是比较简单的方式,也是大势所趋。

    • ConfigurableApplicationContext 扩展于 ApplicationContext,它新增加了两个主要的方法: refresh()和 close(),让 ApplicationContext 具有启动、刷新和关闭应用上下文的能力。在应用上下文关闭的情况下调用 refresh()即可启动应用上下文,在已经启动的状态下,调用 refresh()则清除缓存并重新装载配置信息,而调用close()则可关闭应用上下文。

    此外,ApplicationContext还通过其他接口扩展了BeanFactory的功能,如下图

    • ApplicationEventPublisher:让容器拥有发布应用上下文事件的功能,包括容器启动事件、关闭事件等。实现了 ApplicationListener 事件监听接口的 Bean 可以接收到容器事件 , 并对事件进行响应处理 。 在 ApplicationContext 抽象实现类AbstractApplicationContext 中,我们可以发现存在一个 ApplicationEventMulticaster,它负责保存所有监听器,以便在容器产生上下文事件时通知这些事件监听者。
    • MessageSource:为应用提供 i18n 国际化消息访问的功能。
    • ResourcePatternResolver :ApplicationContext 实现类都实现了类似于PathMatchingResourcePatternResolver 的功能,可以通过带前缀的 Ant 风格的资源文件路径装载 Spring 的配置文件。

    WebApplicationContext

    在ApplicationContext下面还有一个实现类是WebApplicationContext,是专门为 Web 应用准备的容器,它允许从相对于 Web 根目录的路径中装载配置文件完成初始化工作。

    从WebApplicationContext 中可以获得 ServletContext 的引用,整个 Web 应用上下文对象将作为属性放置到 ServletContext 中,以便 Web 应用环境可以访问 Spring 应用上下文。 WebApplicationContext 定义了一个常量ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE,在上下文启动时, WebApplicationContext 实例即以此为键放置在 ServletContext 的属性列表中,因此我们可以直接通过以下语句从 Web 容器中获取WebApplicationContext:

    WebApplicationContext wac = (WebApplicationContext)servletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE);
    

    整合图如下,其他不过多介绍:

    三、SpringIOC的启动流程

    Spring IOC的启动时会读取应用程序提供的Bean的配置信息,并在Spring容器中生成一份相应的Bean配置注册表,然后根据注册表加载、实例化bean、建立bean与bean之间的依赖关系。然后将这些准备就绪的bean放到bean缓存池中,等待应用程序调用。

    总结一下,我们可以把IOC的启动流程分为一下两个重要的阶段:

    1. 容器的启动阶段
    2. Bean的实例化阶段

    这里补充一下,在 Spring 中,最基础的容器接口方法是由 BeanFactory 定义的,而 BeanFactory 的实现类采用的是 延迟加载,也就是说,容器启动时,只会进行第一个阶段的操作, 当需要某个类的实例时,才会进行第二个阶段的操作。而 ApplicationContext(另一个容器的实现类)在启动容器时就完成了所有初始化,这就需要更多的系统资源,我们需要根据不同的场景选择不同的容器实现类。我们下面介绍更多是以ApplicationContext为主来介绍!

    IOC容器的启动阶段

    在容器启动阶段,我们的Spring经历了很多事情,具体的话可以分为以下几个步骤:

    1. 加载配置信息
    2. 解析配置信息
    3. 装配BeanDefinition
    4. 后处理

    加载配置信息

    这里我们要先回顾一下之前的beanfactory了,我们说这是一个最基础的bean工厂接口,那么就需要我们的实现类,我们上面虽然说到了ApplicationContext,但是我们再仔细看一下那张图,然后站高处来看。ApplicationContext 继承自 BeanFactory,但是它不应该被理解为 BeanFactory 的实现类,而是说其内部持有一个实例化的 BeanFactory(DefaultListableBeanFactory)。以后所有的 BeanFactory 相关的操作其实是委托给这个实例来处理的。

    我们为什么选择了DefaultListableBeanFactory,可以看到它继承的两个父类,然后继续延伸上去齐全了所有的功能。可以说DefaultListableBeanFactory 基本上是最牛的 BeanFactory 了,这也是为什么这边会使用这个类来实例化的原因。

    好了,我们继续回到加载配置文件信息这个话题。我们Spring加载配置文件最开始图里面也介绍了,有ClassPathXmlApplicationContext 类路径加载和FileSystemXmlApplicationContext 文件系统加载。

    然后就是我们IOC 容器读取配置文件的接口为 BeanDefinitionReader,它会根据配置文件格式的不同给出不同的实现类,将配置文件中的内容读取并映射到 BeanDefinition 中。比如xml文件就会用XmlBeanDefinitionReader

    解析配置信息

    我们解析配置信息就是要将我们读取的配置信息里面的信息转换成一个dom树,然后解析里面的配置信息装配到我们的BeanDefinition。我们在processBeanDefinition中先将解析后的信息封装到一个BeanDefinitionHolder,一个BeanDefinitionHolder其实就是一个 BeanDefinition 的实例和它的 beanName、aliases (别名)这三个信息。

    processBeanDefinition过程可以解析很多的标签,如factory-beanfactory-method<lockup-method /><replaced-method /><meta /><qualifier />,当然最显目的就是<bean/>,例如以下的属性:

    Property
    class 类的全限定名
    name 可指定 id、name(用逗号、分号、空格分隔)
    scope 作用域
    constructor arguments 指定构造参数
    properties 设置属性的值
    autowiring mode no(默认值)、byName、byType、 constructor
    lazy-initialization mode 是否懒加载(如果被非懒加载的bean依赖了那么其实也就不能懒加载了)
    initialization method bean 属性设置完成后,会调用这个方法
    destruction method bean 销毁后的回调方法

    在具体的xml配置文件中可以是这样子的:

    <bean id="exampleBean" name="name1, name2, name3" class="com.javadoop.ExampleBean"
          scope="singleton" lazy-init="true" init-method="init" destroy-method="cleanup">
    
        <!-- 可以用下面三种形式指定构造参数 -->
        <constructor-arg type="int" value="7500000"/>
        <constructor-arg name="years" value="7500000"/>
        <constructor-arg index="0" value="7500000"/>
    
        <!-- property 的几种情况 -->
        <property name="beanOne">
            <ref bean="anotherExampleBean"/>
        </property>
        <property name="beanTwo" ref="yetAnotherBean"/>
        <property name="integerProperty" value="1"/>
    </bean>
    

    装配BeanDefinition

    在上面我们将信息解析后,就会装配到一个BeanDefinitionHolder,里面就包含了我们的BeanDefinition。然后装配BeanDefinition,就是将这些BeanDefinition注册到BeanDefinitionRegistry(说到底核心是一个 beanName-> beanDefinition 的 map)中。我们在获取的BeanDefinition的时候需要通过key(beanName)获取别名,然后通过别名再一次重定向获取我们的BeanDefinition。

    Spring容器的后续操作直接从BeanDefinitionRegistry中读取配置信息。具体注册实现就是在我们上面介绍到的DefaultListableBeanFactory实现类里。

    后处理

    在我们的后续操作,容器扫描BeanDefinitionRegistry中的BeanDefinition,使用Java的反射机制自动识别出Bean工厂后处理后器(实现BeanFactoryPostProcessor接口)的Bean,然后调用这些Bean工厂后处理器对BeanDefinitionRegistry中的BeanDefinition进行加工处理。主要完成以下两项工作:

    • 对使用到占位符的元素标签进行解析,得到最终的配置值,这意味对一些半成品式的BeanDefinition对象进行加工处理并得到成品的BeanDefinition对象;

    • BeanDefinitionRegistry中的BeanDefinition进行扫描,通过Java反射机制找出所有属性编辑器的Bean(实现java.beans.PropertyEditor接口的Bean),并自动将它们注册到Spring容器的属性编辑器注册表中(PropertyEditorRegistry);

    Spring容器从BeanDefinitionRegistry中取出加工后的BeanDefinition,并调用InstantiationStrategy着手进行Bean实例化的工作;在实例化Bean时,Spring容器使用BeanWrapper对Bean进行封装,BeanWrapper提供了很多以Java反射机制操作Bean的方法,它将结合该Bean的BeanDefinition以及容器中属性编辑器,完成Bean属性的设置工作。

    在我们装配好Bean容器后,还要通过方法prepareBeanFactory准备Bean容器,在准备阶段会注册一些特殊的Bean,这里不做深究。在准备容器后我们可能会对bean进行一些加工,就需要用到beanPostProcessor来进行一些后处理。我们利用容器中注册的Bean后处理器(实现BeanPostProcessor接口的Bean)对已经完成属性设置工作的Bean进行后续加工,直接装配出一个准备就绪的Bean。这个在下面实例化阶段后再介绍到!

    这里可能会对BeanPostProcessorBeanFactoryPostProcessor产生混乱,理解不清。总结一下两者的区别:

    • BeanPostProcessor 对容器中的Bean进行后处理,对Bean进行额外的加强,加工。使用点是在我们单例Bean实例化过程中穿插执行的。
    • BeanFactoryPostProcessorSpring容器本身进行后处理,增强容器的功能。是在我们单例实例化之前执行的。

    更加具体的可以参考这一篇博文。点击跳转

    总结

    这里关于容器的启动过程很多细节并不是很详细,因为很多东西都需要配着源码才能分析。关于源码解析推荐这一篇,更加深入(Spring IOC 容器源码分析

    Bean的实例化阶段

    然后就是我们Bean的预先实例化阶段。在ApplicationContext中,所有的BeanDefinition的Scope默认是Singleton,针对Singleton我们Spring容器采用是预先实例化的策略。这样我们在获取实例的时候就会直接从缓存里面拉取出来,提升了运行效率。

    但是如果我们设置了懒加载的话,那么就不会预先实例化。而是在我们第一次getBean的时候才会去实例化。不过我们大部分时候都不会去使用懒加载,除非这个bean比较特殊,例如非常耗费资源,在应用程序的生命周期里的使用概率比较小。在这种情况下我们可以将它设置为懒加载!

    实例化过程

    针对我们的Bean的实例化,具体一点的话可以分为以下阶段:

    1. Spring对bean进行实例化,默认bean是单例;
    2. Spring对bean进行依赖注入,比如有没有配置当前depends-on的依赖,有的话就去实例依赖的bean;
    3. 如果bean实现了BeanNameAware接口,spring将bean的id传给setBeanName()方法;
    4. 如果bean实现了BeanFactoryAware接口,spring将调用setBeanFactory方法,将BeanFactory实例传进来;
    5. 如果bean实现了ApplicationContextAware接口,它的setApplicationContext()方法将被调用,将应用上下文的引用传入到bean中;
    6. 如果bean实现了BeanPostProcessor接口,它的postProcessBeforeInitialization方法将被调用;
    7. 如果bean实现了InitializingBean接口,spring将调用它的afterPropertiesSet接口方法,类似的如果bean使用了init-method属性声明了初始化方法,则再调用该方法;
    8. 如果bean实现了BeanPostProcessor接口,它的postProcessAfterInitialization接口方法将被调用;
    9. 此时bean已经准备就绪,可以被应用程序使用了,他们将一直驻留在应用上下文中,直到该应用上下文被销毁;
    10. 若bean实现了DisposableBean接口,spring将调用它的distroy()接口方法。如果bean使用了destroy-method属性声明了销毁方法,则再调用该方法;

    上面提及到的方法有点多,但是我们可以按照分类去记忆

    分类类型 所包含方法
    Bean自身的方法 配置文件中的init-method和destroy-method配置的方法、Bean对象自己调用的方法
    Bean级生命周期接口方法 BeanNameAware、BeanFactoryAware、InitializingBean、DiposableBean等接口中的方法
    容器级生命周期接口方法 InstantiationAwareBeanPostProcessor、BeanPostProcessor等后置处理器实现类中重写的方法

    循环依赖问题

    关于实例化过程其实是一块比较复杂的东西,如果不去看源码的话,讲个上面的流程也差不多。毕竟完全讲的话,哪能记住那么多。在这里还有一个主要讲的就是在实例化过程中一个比较复杂的问题,就是“循环依赖问题”。这里花点篇幅讲解一下。

    循环依赖问题,举个例子引入一下。比如我们有A,B两个类,A的 构造方法有一个参数是B,B的构造方法有一个参数是A,这种A依赖于B,B依赖于A的问题就是依赖问题。

    @Service
    public class A {  
        public A(B b) {  }
    }
    
    @Service
    public class B {  
        public B(A a) {  
        }
    }
    

    或者说A依赖于B,B依赖于C,C依赖于A也是。

    我们的循环依赖可以分类成三种:

    • 原型循环依赖
    • 单例构造器循环依赖
    • 单例setter注入循环依赖

    我们的Spring是无法解决构造器的循环依赖的,但是可以解决setter的循环依赖。关于这三者的区别,这里给出一篇比较详细的博文可以参考。(循环依赖的三种方式

    循环依赖解决

    构造器依赖问题

    我们说过构造器的循环依赖Spring是无法解决的,那引出另一个问题就是Spirng是如何判断构造器发生了循环依赖呢?

    简单介绍一下,我们在上面介绍的例子,A依赖于B,B依赖于A。在我们A实例化的时候要去实例B,然后B又要去实例A,在我们过程中,我们这将beanName添加到一个set结构中,当第二次添加A的时候,也就是B依赖于A,要去实例化A的时候,因为Set已经存在A的beanName了,所以Spring就会判断发生了循环依赖问题,抛出异常!

    原型依赖问题

    至于原型依赖的判断条件其实和构造器的判断差不多,最主要的区别就是set的类型变成了ThreadLocal类型的。

    Setter是如何具体解决循环依赖问题呢?

    我们的Spring是通过三级缓存来解决的。

    三级缓存呢,其实就是有三个缓存:

    • singletonObjects(一级缓存)
    • earlySingletonObjects(二级缓存)
    • singletonFactories(三级缓存)

    我们以上面A依赖于B,B依赖于A的样例来分析一下setter是如何通过三级缓存解决循环依赖问题。

    1. 首先我们在实例化A的时候,通过beanDifinition定义拿到A class的无参构造方法,通过反射创建了这个实例对象。这个A的实例对象是一个尚未进行依赖注入和init-method方法调用等等逻辑处理的早期实例,是我们业务无法使用的。然后在进行后续的包装处理前,我们会将它封装成一个ObjectFactory对象然后存入到我们的三级缓存中(key是beanName,value是ObjectFactory对象),相当于一个早起工厂提前曝光。
    2. 然后呢我们的会继续实例化A,在实例过程中因为A依赖于B,我们通过Setter注入依赖的时候,通过getBean(B)去获取依赖对象B,但是这个B还没有实例化,所以我们就需要去创建B的实例。
    3. 然后我们就开始创建B的实例,同上A的过程。在实例B的过程中,因为B依赖于A,所以也会调用getBean(A)去获得A的实例,首先就会去一级缓存访问,如果没有就去二级缓存,再没有就去三级缓存。然后在三级缓存中发现我们的早期实例A,不过也拿来用了。然后完成B的依赖,再完成后面B实例化过程的一系列阶段,最后并且存放到Spring的一级缓存中。并将二三级缓存清理掉。
    4. 完成B的实例后,我们就会回到A的实例阶段,我们的A在有了B的依赖后,也继续完成了后续的实例化过程,把一个早期的对象变成一个完整的对象。并将A存进到一级缓存中,清除二三级缓存。

    为什么要有三级缓存?二级缓存不够用吗?

    我们在上面分析的过程中呢,可能会感觉二级缓存的存在感不是特别强。为什么不去掉第二级的缓存然后变成一个二级缓存呢。

    这里呢,解释一下。我们的B在拿到A的早期实例后就会进行缓存升级,将A从从三级缓存移到二级缓存中。之所以需要有三级缓存呢,是因为在这一步,我们的bean可能还需要一些其他的操作,可能会被bean后置处理器进行一些增强之类的啥,或者做一些AOP的判断。如果只有二级缓存的话,那么返回的就是早期实例而不是我们增强的后的实例!

    四、总结

    对启动流程这一块,看了网上的很多资料,也照着看了一下源码。虽然自己总结了,但总感觉哪里会有纰漏。如果哪里有错误的话,还请看官们帮忙指出,或者给我指明一下哪里需要修改的地方,大家一起进步学习!

    五、参考资料

    Spring技术内幕:深入理解Spring架构与设计原理 [机械工业出版社]

    IOC&AOP详解

    Spring IOC 容器源码分析

    Spring IOC原理总结

    Spring IOC 启动过程

    Spring IOC:Spring IOC 的具体过程

    Spring IOC -bean对象的生命周期详解

  • 相关阅读:
    实用小工具 -- 国家地区IP段范围查询工具
    JAVA学习笔记--ClassLoader
    Apache HttpClient之fluent API的使用
    JAVA学习笔记--初探hash与map
    JAVA学习笔记--赋值(“=”)
    JAVA学习笔记--方法中的参数调用是引用调用or值调用
    修改host,访问指定服务器
    VueJS基础框架代码介绍
    Mac下通过npm安装webpack 、vuejs,以及引入jquery、bootstrap等(初稿)
    SSH配置与修改
  • 原文地址:https://www.cnblogs.com/CryFace/p/13462176.html
Copyright © 2020-2023  润新知