目录
简略介绍
理解介绍
IoC的优点
Spring的IoC
IoC是如何实现的
IoC能做什么
IoC和DI
Spring IoC过程
为什么框架需要反射
Spring Bean的循环依赖问题
IoC简单实现
-
简略介绍
- 所谓的IoC(inversion of control),就是控制反转的意思
- 何为控制反转?
- 可以根据字面意思理解,就是对于某个东西A,原来的控制权在使用方B,B想用就能用,不想用就不用
- 现在把控制权交还给了A,只有A给了才能用,这样就是控制反转了
- 可能说的有点抽象,更具体一点呢
- 拿代码来说话:
- 下面是一个没有IoC的例子:
- 当有了IoC之后:
- 也就是说,没有Spring的话,我们要使用的对象,需要我们自己创建
- 而有了Spring的IoC之后,对象由IoC容器创建并管理,我们只需要在想要使用的时候从容器中获取就行了
- 值得说明的是,IoC只是一种思想和理念,可以有不同的实现方式
-
理解介绍
- IoC—Inversion of Control,即“控制反转”,不是什么技术,而是一种设计思想
- 在Java开发中,IoC意味着将你设计好的对象交给容器控制,而不是传统的在你的对象内部直接控制
- (1)IoC 就是控制反转,是指创建对象的控制权的转移
- 以前创建对象的主动权和时机是由自己把控的,而现在这种权力转移到 Spring 容器中,并由容器根据配置文件去创建实例和管理各个实例之间的依赖关系
- 对象与对象之间松散耦合,也利于功能的复用
- DI 依赖注入,和控制反转是同一个概念的不同角度的描述,即 应用程序在运行时依赖 IoC 容器来动态注入对象需要的外部资源
- (2)最直观的表达就是,IoC 让对象的创建不用去 new 了,可以由 Spring自动生产,使用 java 的反射机制,根据配置文件在运行时动态的去创建对象以及管理对象,并调用对象的方法的
- (3)Spring的 IoC 有三种注入方式:构造器注入,setter 方法注入,根据注解注入
- IoC让相互协作的组件保持松散的耦合,而 AOP 编程允许你把遍布于应用各层的功能分离出来形成可重用的功能组件
-
IoC的优点
- 使用IoC,有最少三个好处:
- 1-使用者不用关心引用bean的实现细节
- 譬如对于 B b = new A(c,d,e,f); 来说,如果B要使用A,那还要把c,d,e,f多个类全都感知一遍,这显然是非常麻烦且不合理的
- 2-不用创建多个相同的bean导致浪费
- 仍然是:
- 如果B和Z都引用了A,那么B和Z就可能new 两个A实例,实际上,我们只需要一个就好了
- 3-Bean的修改使用方无需感知
- 同样是上面的例子,假如说Bean A需要修改,如果没有IoC的话,所有引用到A的其他bean都需要感知这个逻辑,并且做对应的修改
- 但是如果使用了IoC,其他bean就完全不用感知到
- 1-使用者不用关心引用bean的实现细节
- 列举一些 IoC 的一些好处:
- 资源集中管理配置
- 它将最小化应用程序中的代码量
- 它以最小的影响和最少的侵入机制促进松耦合
- 它支持即时的实例化和延迟加载 Bean 对象
- 它将使您的应用程序易于测试,因为它不需要单元测试用例中的任何单例或 JNDI 查找机制
-
Spring的IoC
- 对于Spring的IoC来说,它是IoC思想的一种实现方式
- 在容器启动的时候,它会根据每个bean的要求,将bean注入到Spring Container中
- 如果有其他bean需要使用,就直接从容器中获取即可,如下图所示:
- Spring 框架的核心是 Spring IoC 容器
- 容器创建 Bean 对象,将它们装配在一起,配置它们并管理它们的完整生命周期
- Spring 容器使用依赖注入来管理组成应用程序的 Bean 对象
- 容器通过读取提供的配置元数据 Bean Definition 来接收对象进行实例化,配置和组装的指令
- 该配置元数据 Bean Definition 可以通过 XML,Java 注解或 Java Config 代码提供
-
IoC是如何实现的
- 使用Spring的IoC容器能力,非常简单,如下代码所示:
- 从上面的代码中,我们也能看出来Spring的IoC是如何实现的:
- 1-从配置元数据中获取要DI的业务POJO(这里的配置元数据包括xml,注解,configuration类等)
- 2-将业务POJO形成BeanDefinition注入到Spring Container中
- 3-使用方通过ApplicationContext从Spring Container直接获取即可
- 如下图所示:
-
IoC能做什么
- 所谓IoC,对于Spring框架来说,就是将对象的创建和对象间的依赖关系交给IoC容器管理
- 将对象的控制权由业务对象转移到IoC容器,所以叫控制反转
- Spring所倡导的开发方式就是如此,所有的类都会在Spring容器中登记,告诉Spring你是个什么东西,你需要什么东西,然后Spring会在系统运行到适当的时候,把你要的东西主动给你,同时也把你交给其他需要你的东西
- 所有的类的创建、销毁都由Spring来控制,也就是说控制对象生存周期的不再是引用它的对象,而是Spring
- 对于某个具体的对象而言,以前是它控制其他对象,现在是所有对象都被Spring控制,所以这叫控制反转
- IoC不是一种技术,只是一种思想,一个重要的面向对象编程的法则,它能指导我们如何设计出松耦合、更优良的程序
- 传统应用程序都是由我们在类内部主动创建依赖对象,从而导致类与类之间高耦合,难于测试
- 有了IoC容器后,把创建和查找依赖对象的控制权交给了容器,由容器进行注入组合对象,所以对象与对象之间是松散耦合,这样也方便测试,利于功能复用,更重要的是使得程序的整个体系结构变得非常灵活
- 其实IoC对编程带来的最大改变不是从代码上,而是从思想上,发生了“主从换位”的变化
- 应用程序原本是老大,要获取什么资源都是主动出击,但是在IoC/DI思想中,应用程序就变成被动的了,被动的等待IoC容器来创建并注入它所需要的资源
-
IoC和DI
- DI—Dependency Injection,即“依赖注入”:组件之间依赖关系由容器在运行期决定,形象的说,即由容器动态的将某个依赖关系注入到组件之中
- 理解DI的关键是:“谁依赖谁,为什么需要依赖,谁注入谁,注入了什么”
- 来深入分析一下:
- 谁依赖于谁:当然是应用程序依赖于IoC容器
- 为什么需要依赖:应用程序需要IoC容器来提供对象需要的外部资源
- 谁注入谁:很明显是IoC容器注入应用程序某个对象,应用程序依赖的对象
- 注入了什么:就是注入某个对象所需要的外部资源(包括对象、资源、常量数据)
- IoC和DI有什么关系呢?
- 其实它们是同一个概念的不同角度描述,由于控制反转概念比较含糊(可能只是理解为容器控制对象这一个层面,很难让人想到谁来维护对象关系)
- 所以2004年大师级人物Martin Fowler又给出了一个新的名字:“依赖注入”,相对IoC而言,“依赖注入”明确描述了“被注入对象依赖IoC容器配置依赖对象”
-
Spring IoC过程
- Spring 的 IoC 设计支持以下功能:
- 依赖注入
- 依赖检查
- 自动装配
- 支持集合
- 指定初始化方法和销毁方法
- 支持回调某些方法(但是需要实现 Spring 接口,略有侵入)
- 对于 IoC 来说,最重要的就是容器
- 容器管理着 Bean 的生命周期,控制着 Bean 的依赖注入
- 那么, Spring 如何设计容器的呢?
- Spring 作者 Rod Johnson 设计了两个接口用以表示容器
- BeanFactory
- ApplicationContext
- BeanFactory 粗暴简单,可以理解为就是个 HashMap,Key 是 BeanName,Value 是 Bean 实例
- 通常只提供注册(put),获取(get)这两个功能,可以称之为“低级容器”
- ApplicationContext 可以称之为“高级容器”
- 因为他比 BeanFactory 多了更多的功能
- 他继承了多个接口,因此具备了更多的功能
- 例如资源的获取,支持多种消息(例如 JSP tag 的支持),对 BeanFactory 多了工具级别的支持等待
- 所以你看他的名字,已经不是 BeanFactory 之类的工厂了,而是“应用上下文”,代表着整个大容器的所有功能
- 该接口定义了一个 refresh 方法,此方法是所有阅读 Spring 源码的人的最熟悉的方法,用于刷新整个容器,即重新加载/刷新所有的bean
- 当然,除了这两个大接口,还有其他的辅助接口,但我今天不会花太多篇幅介绍他们
- 为了更直观的展示 “低级容器” 和 “高级容器” 的关系,我这里通过常用的 ClassPathXmlApplicationContext类,来展示整个容器的层级 UML 关系
- 最上面的 BeanFactory,下面的 3 个绿色的,都是功能扩展接口,这里就不展开讲
- 看下面的隶属 ApplicationContext 粉红色的 “高级容器”,依赖着 “低级容器”,这里说的是依赖,不是继承
- 他依赖着 “低级容器” 的 getBean 功能
- 而高级容器有更多的功能:支持不同的信息源头,可以访问文件资源,支持应用事件(Observer 模式)
- 通常用户看到的就是“高级容器”
- 但 BeanFactory 也非常够用;左边灰色区域的是 “低级容器”,只负载加载 Bean,获取 Bean
- 容器其他的高级功能是没有的
- 例如上图画的 refresh 刷新 Bean 工厂所有配置,生命周期事件回调等
- 解释了低级容器和高级容器,我们可以看看一个 IoC 启动过程是什么样子的
- 说白了,就是 ClassPathXmlApplicationContext 这个类,在启动时,都做了啥
- 1-用户构造 ClassPathXmlApplicationContext(简称 CPAC)
- 2-CPAC 首先访问了 “抽象高级容器” 的 final 的 refresh 方法,这个方法是模板方法
- 所以要回调子类(低级容器)的 refreshBeanFactory 方法,这个方法的作用是使用低级容器加载所有 BeanDefinition 和 Properties 到容器中
- 3-低级容器加载成功后,高级容器开始处理一些回调,例如 Bean 后置处理器
- 回调 setBeanFactory 方法;或者注册监听器等,发布事件,实例化单例 Bean 等等功能
- 这些功能,随着 Spring 的不断升级,功能越来越多,很多人在这里迷失了方向
- 简单说就是:
- 1-低级容器 加载配置文件(从 XML,数据库,Applet),并解析成 BeanDefinition 到低级容器中
- 2-加载成功后,高级容器启动高级功能,例如接口回调,监听器,自动实例化单例,发布事件等等功能
- 好,当我们创建好容器,就会使用 getBean 方法,获取 Bean,而 getBean 的流程如下:
- 从图中可以看出,getBean 的操作都是在低级容器里操作的
- 其中有个递归操作,这个是什么意思呢?
- 假设:当 Bean_A 依赖着 Bean_B,而这个 Bean_A 在加载的时候,其配置的 ref = “Bean_B” 在解析的时候只是一个占位符,被放入了 Bean_A 的属性集合中,当调用 getBean 时,需要真正 Bean_B 注入到 Bean_A 内部时,就需要从容器中获取这个 Bean_B,因此产生了递归
- 为什么不是在加载的时候,就直接注入呢?
- 因为加载的顺序不同,很可能 Bean_A 依赖的 Bean_B 还没有加载好,也就无法从容器中获取,你不能要求用户把 Bean 的加载顺序排列好,这是不人道的
- 所以,Spring 将其分为了 2 个步骤:
- 1-加载所有的 Bean 配置成 BeanDefinition 到容器中,如果 Bean 有依赖关系,则使用占位符暂时代替
- 2-然后,在调用 getBean 的时候,进行真正的依赖注入,即如果碰到了属性是 ref 的(占位符),那么就从容器里获取这个 Bean,然后注入到实例中 —— 称之为依赖注入
- 可以看到,依赖注入实际上,只需要 “低级容器” 就可以实现
- 这就是 IoC
- 所以 ApplicationContext refresh 方法里面的操作不只是 IoC,是高级容器的所有功能(包括 IoC),IoC 的功能在低级容器里就可以实现
- 小结:
- IoC 在 Spring 里,只需要低级容器就可以实现,2 个步骤:
- 加载配置文件,解析成 BeanDefinition 放在 Map 里,如果 Bean 有依赖关系,则使用占位符暂时代替
- 调用 getBean 的时候,从 BeanDefinition 所属的 Map 里,拿出 Class 对象进行实例化,同时,如果有依赖关系,将递归调用 getBean 方法 —— 完成依赖注入
- 上面就是 Spring 低级容器(BeanFactory)的 IoC
- 至于高级容器 ApplicationContext,他包含了低级容器的功能,当他执行 refresh 模板方法的时候,将刷新整个容器的 Bean
- 同时其作为高级容器,包含了太多的功能
- 一句话,他不仅仅是 IoC
- 他支持不同信息源头,支持 BeanFactory 工具类,支持层级容器,支持访问文件资源,支持事件发布通知,支持接口回调等等
- 可以预见,随着 Spring 的不断发展,高级容器的功能会越来越多
-
为什么框架需要反射
- IoC容器的作用需求就是在框架运行后通过读取类定义的配置文件去帮我们创建任意对象,执行任意方法
- 因为框架先于客户编写程序运行,无法事先得到程序类,只能通过反射
-
Spring Bean的循环依赖问题
- 循环依赖其实就是循环引用,也就是两个或者两个以上的bean互相持有对方,最终形成闭环
- 比如A依赖于B,B依赖于C,C又依赖于A
- 如下图:
- 注意,这里不是函数的循环调用,是对象的相互依赖关系
- 循环调用其实就是一个死循环,除非有终结条件
- Spring中循环依赖场景有:
- (1)构造器的循环依赖
- (2)field属性的循环依赖
- 第一种:构造器参数循环依赖
- Spring容器会将每一个正在创建的 Bean 标识符放在一个“当前创建Bean池”中,Bean标识符在创建过程中将一直保持在这个池中,因此如果在创建Bean过程中发现自己已经在“当前创建Bean池”里时将抛出BeanCurrentlyInCreationException异常表示循环依赖
- 而对于创建完毕的Bean将从“当前创建Bean池”中清除掉
- 如果大家理解开头那句话的话,这个报错应该不惊讶
- Spring容器先创建单例StudentA,StudentA依赖StudentB,然后将A放在“当前创建Bean池”中,此时创建StudentB,StudentB依赖StudentC,然后将B放在“当前创建Bean池”中,此时创建StudentC,StudentC又依赖StudentA,但是,此时StudentA已经在池中,所以会报错,因为在池中的Bean都是未初始化完的,所以会依赖错误(初始化完的Bean会从池中移除)
- 第二种:setter方式单例,默认方式(三级缓存可以解决)
- 当然,Spring对于循环依赖的解决不是无条件的,首先前提条件是针对scope单例并且没有显式指明不需要解决循环依赖的对象
- 而且要求该对象没有被代理过
- 如图中前两步骤得知:Spring是先将Bean对象实例化之后再设置对象属性的
- 为什么用set方式就不报错了呢
- “三级缓存”主要是指
- 让我们来分析一下“A的某个field或者setter依赖了B的实例对象,同时B的某个field或者setter依赖了A的实例对象”这种循环依赖的情况
- A首先完成了初始化的第一步,并且将自己提前曝光到singletonFactories(三级缓存)中
- 此时进行初始化的第二步,发现自己依赖对象B,此时就尝试去get(B),发现B还没有被create,所以走create流程,B在初始化第一步的时候发现自己依赖了对象A,于是尝试get(A),尝试一级缓存singletonObjects(肯定没有,因为A还没初始化完全),尝试二级缓存earlySingletonObjects(也没有),尝试三级缓存singletonFactories,由于A通过ObjectFactory将自己提前曝光了,所以B能够通过ObjectFactory.getObject拿到A对象(虽然A还没有初始化完全,但是总比没有好呀)
- B拿到A对象后顺利完成了初始化阶段1、2、3,完全初始化之后将自己放入到一级缓存singletonObjects中
- 此时返回A中,A此时能拿到B的对象顺利完成自己的初始化阶段2、3,最终A也完成了初始化,长大成人,进去了一级缓存singletonObjects中,而且更加幸运的是,由于B拿到了A的对象引用,所以B现在hold住的A对象也蜕变完美了!一切都是这么神奇
- Spring通过三级缓存加上“提前曝光”机制,配合Java的对象引用原理,比较完美地解决了某些情况下的循环依赖问题
- 第三种:setter方式原型,prototype
- 为什么原型模式就报错了呢?
- 对于“prototype”作用域Bean,Spring容器无法完成依赖注入,因为“prototype”作用域的Bean,Spring容器不进行缓存,因此无法提前暴露一个创建中的Bean
-
IoC简单实现
- 先从简单的 IOC 容器实现开始,最简单的 IOC 容器只需4步即可实现,如下:
- 1-加载 xml 配置文件,遍历其中的标签
- 2-获取标签中的 id 和 class 属性,加载 class 属性对应的类,并创建 bean
- 3-遍历标签中的标签,获取属性值,并将属性值填充到 bean 中
- 4-将 bean 注册到 bean 容器中