Spring IOC 和 DI 的概念和原理

前言:

前几天看博客的时候,看到了网上对于Spring IOC 和 DI 的概念的解析,觉得很不错,然后提取了其中一部分精华内容并做了一些自己的理解。

1.1.IOC 是什么?

IOC – Inversion of Control,即 “控制反转” ,不是什么技术,而是一种设计思想。这是 Spring 的核心,贯穿始终。所谓 IOC,对于 Spring 框架来说, 就是由 Spring 来负责控制对象的生命周期和对象间的关系。而在Java开发中,IOC 意味着将你设计好的对象交给容器控制,而不是传统的在你的对象内部直接控制。

  • 谁控制谁,控制什么:传统的Java SE 程序设计,我们直接在对象内部通过 new 进行创建对象,是程序主动去创建依赖对象;而 IOC 是有专门一个容器来创建这些对象,即由 IOC 容器来控制对象的创建。

注解:提到控制,我们就需要理解什么是控制,所谓控制,就是对象的创建、初始化、销毁。

  • 为何是反转,哪些方面反转了:有反转就有正转,传统应用程序是由我们自己在对象中主动控制去直接获取依赖对象,也就是正转;而反转则是由容器来帮忙创建及注入依赖对象;为何是反转?因为由容器帮我们查找及注入依赖对象,对象只是被动的接受依赖对象,所以是反转;哪些方面反转了?依赖对象的获取被反转了。

注解:反转的是什么?反转的是控制权,前面提到 Spring 控制了对象的生命周期,那么对象的控制就完全脱离了我们的控制,交给了 Spring。这个反转是指,我们由对象的控制着变成了 IOC 的被动接受者。我们无法决定对象生命周期的任何一个阶段,最多是借助于 Spring 的扩展机制做一些微笑的动作,我们甚至无法预判依赖的对象真正被注入的是哪一个。

1.2.IOC 能做什么?

IOC 不是一种技术,只是一种思想,一个重要的面向对象编程的法则,它能知道我们设计出松耦合、更优良的程序。把创建和查找依赖对象的控制权交给了容器,由容器进行注入组合对象,所以对象与对象之间是松散耦合,利于功能复用,更重要的是使得程序的震哥哥体系结构变得非常灵活。

其实 IOC 对编程带来的最大改变不是从代码上,而是从思想上,发生了 “主从换位” 的变化。应用程序原本是老大,要获取什么资源都是主动出击,但是在 IOC/DI 思想中,应用程序就变成了被动的,被动的等待 IOC 容器来创建并注入它所需要的资源了。

1.3.IOC 和 DI

DI-Dependency Injection,即 “依赖注入”:组件之间依赖关系由容器在运行期决定,形象地说,即由容器动态的将某个依赖关系注入到组件中。依赖注入的目的并非为软件系统带来更多功能,而是为了提升组件重用的频率,并为系统搭建一个灵活、可扩展的平台。通过依赖注入机制,我们只需要通过简单的配置,而无需任何代码就可指定目标需要的资源,完成自身的业务逻辑,而不需要关心具体的字源来自何处,由谁实现。

注解:依赖注入是一种实现,而 IOC 是一种设计思想。从 IOC 到 DI,就是从理论到实践。你把依赖交给了容器,而容器帮你管理依赖,这就是依赖注入的核心。越是大型的项目,越难以管理依赖关系,开发工作逐渐变化为一个个节点的开发,而这些节点通过依赖注入关联起来。依赖注入降低了开发的成本、提高了代码的复用率、提高了软件的灵活性,也给软件开发带来了挑战,你根本不知道运行时容器会给你什么。

理解 DI 的关键是:“谁依赖谁,为什么需要依赖,谁注入谁,注入了什么”,那我们来深入分析一下:

  • 谁依赖于谁:当然是应用程序依赖于 IOC 容器;
  • 为什么需要依赖:应用程序需要 IOC 容器来提供对象需要的外部资源;
  • 谁注入谁:很明显是 IOC 容器注入应用程序某个对象,应用程序依赖的对象;
  • 注入了什么:就是注入某个对象所需要的外部资源(包括对象、资源、常量数据)。

注解:DI 即依赖注入,重点在于 “依赖”、“注入” 两个概念。什么是依赖?对象运行所需要的外部数据、资源就是依赖,没有这些东西对象就不能完成业务处理,必须拿到才能运行。什么是注入?注入这个词真的很形象,就像打针一样,从外部注入到内部,容器加载了外部的文件、URL、配置和对象,然后把这些数据、对象按需注入给对象。

IOC 和 DI 有什么关系呢?其实它们是同一个概念的不同角度描述,由于控制反转概念比较合理,所以 2004 年大师级人物 Matin Fowler 又给出了一个新的名字:“依赖注入”,相对于 IOC 而言,“依赖注入” 明确描述了 “被注入对象依赖 IOC 容器配置依赖对象”。

注解:IOC 和 DI 是同一概念的不同角度描述,但实际上又是有区别的。IOC 强调的是容器和对象的控制权发生了反转,而 DI 强调的是对象的依赖由容器进行注入,大部分情况下说两者相同也不算错。但是广义上 IOC 是一种软件开发模式,也就是说还可以通过别的方式实现,而 DI 只是其中一种,Spring 选择了 DI 从而使 DI 在 Java 开发中深入人心。

2.1.Spring 的 IOC 容器

在 Spring IOC 容器的设计中,容器有两个系列,可以看出是容器的具体表现形式:

  • BeanFactory 简单容器:实现了容器的基本功能,典型方法如 getBean、containsBean、isSingleton;
  • ApplicationContext 应用上下文:在简单容器的基础上,增加上下文的特性。

注解:为什么要设计两个系列,而不是一个?这就涉及到架构设计的模式了,底层定义核心流程,上层扩展功能实现,高内聚、低耦合。在架构设计中,这样的分层很有必要,可以随时替换掉一个抽象层。

Spring 通过定义 BeanDefinition 来管理基于 Spring 的应用中的各种对象以及它们之间的相互依赖关系。BeanDefinition 抽象了我们对 Bean 的定义,是让容器起作用的主要数据类型。IOC 容器是用来管理对象依赖关系的,BeanDefinition 就是对依赖反转模式中管理的对象依赖关系的数据抽象,也是容器实现依赖反转功能的核心数据结构,依赖反转功能都是围绕这个 BeanDefinition 的处理来完成的。

注解:BeanDefinition 事实上就是 Bean 的定义在运行时的表现。无论是 XML 配置的 Bean,还是注解定义的 Bean,又或者是自定义扫描出来的 Bean,最终都通过 BeanDefinition 来承载。如果有自定义的 XMl 标签,解析后也是生成 BeanDefinition 注册到 IOC 中。这样设计,IOC 只需要关心 BeanDefinition 即可,极大增加了扩展性和灵活性。当我们 getBean 的时候,如果 Bean 还没有初始化,容器就会找到 BeanDefinition 初始化 Bean 及其依赖。

2.2.Sping IOC 容器的设计

IOC 容器的接口设计如图所示:

IOC容器接口设计图

BeanFactory 定义了基本的 IOC 容器的规范,包括了 getBean 方法。HierarchicalBeanFactory 接口在继承了 BeanFactory 后,增加了 getParentBeanFactory 方法,使 BeanFactory 具备了双亲 IOC 容器的管理功能。在接下来的 ConfigurableBeanFactory 中,定义了一些对 BeanFactory 的配置功能,比如通过 setParentBeanFactory 方法设置双亲 IOC 容器,通过 addBeanPostProcessor 方法配置 Bean 后置处理器。

注解:可以看到 BeanFactory 只定义了基本功能,是一个最核心的容器接口定义。在 BeanFactory 的基础上 Spring 通过继承逐层扩充容器的能力。理解如此多的工厂接口就是在理解 Spring 的设计模式,正如前面所说,这里的继承关系也是架构分层的体现。而通过继承和扩充,在 ConfigurableBeanFactory 中基本完成了 BeanFactory 系列的接口定义。当然了,接口分层后,BeanFactory 的每一层具体实现也是分层的,后面会具体解读。这里可以看到面向接口开发极大提高了扩展性和灵活性。

以 ApplicationContext 为核心的接口系列中,ListableBeanFactory 和 HierarchicalBeanFactory 两个接口链接了 BeanFactory 接口定义和 ApplicationContext 应用上下文的接口定义。

在 ListableBeanFactory 接口中,细化了很多 BeanFactory 的接口功能,比如定义了 getBeanDefinitionNames 接口方法。对于 ApplicationContext 接口,它通过继承 MessageSource、ResourceLoader、ApplicationEventPublisher 接口,在 BeanFactory 的基础上添加了对高级容器特性的支持。

注解:ApplicationContext 继承了 BeanFactory 接口,具有了容器的基本功能,同时根据上下文的特点,也用 ListableBeanFactory 接口做了功能扩展。上下文与容器的主要区别,还是体现在容器高级特性上,比如 MessageSource 实现了国际化、ResourceLoader 实现了资源加载、ApplicationEventPublisher 实现了事件机制。因此平时工作中使用上下文会多一点,一般不直接使用 BeanFactory 简单容器。

2.3.FactoryBean

在 BeanFactory 中,Bean 是通过 FactoryBean 来获取的。FactoryBean 是一个工厂 Bean,可以生成某一类型 Bean 的实例,它最大的一个作用是:可以让我们自定义 Bean 的创建过程。可以使用转义符 “&” 得到 FactoryBean 本身,用来区分通过容器来获取 FactoryBean 产生的对象和获取 FactoryBean 本身。

注解:FactoryBean 和 BeanFactory,一个是 Factory,也就是 IOC 容器工厂,一个是特色类型的 Bean。所有的 Bean 都是由 BeanFacory 管理。FactoryBean 是一个能产生或者修饰对象生成的工厂 Bean,它的实现与设计模式中的工厂模式和修饰器模式类似。这两个类型名称比较接近,很多人容易混淆,只要记住结尾区分即可,一个是工厂,一个是 Bean。

2.4.BeanFactory 容器的设计原理

BeanFactory 提供了使用 IOC 容器的规范,在这个基础上,Spring 还提供了符合这个 IOC 容器接口的一系列容器的实现供开发人员使用,以 XmlBeanFactory 的实现为例来说明简单 IOC 容器的设计原理。

XmlBeanFactory实现原理

注释:这里的 XmlBeanFactory 是一个基于 XML 的容器实现。从类图可以看到,容器的实现也是分层的,每一层接口都有对应的实现,每一个实现都只做自己职责范围内的事情,通过继承形成了一个多层次的容器结构。如果我们要定义自己的容器实现,只需要像它一样按需继承实现即可。

DefaultListableBeanFactory 实际上包含了基本 IOC 容器所具有的重要功能,在 Spring 中,实际上是把 DefaultListableBeanFactory 作为一个默认的功能完整的 IOC 容器来使用。XmlBeanFactory 在继承了 DefaultListableBeanFactory 容器的功能的同时,增加了新的功能,是一个可以读取以 XML 文件方式定义的 BeanDefinition 的 IOC 容器。

注释:在继承体系中,DefaultListableBeanFactory 实现了容器的重要功能。XmlBeanFactory 解决了 XML 文件的解析,并把解析出来的 Bean 定义注册到容器中。这就是一个逐层实现的设计,继承了默认的实现后只需要根据自己的场景做定制即可,每一层的实现都不算复杂。

在 XmlBeanFactory 中,初始化了一个 XmlBeanDefinitionReader,用来读取以 XML 方式定义的 BeanDefinition。而 XML 作为资源文件,通过 Resource 类来封装 I/O 操作。XmlBeanDefinitionReader 初始化后,调用 loadBeanDefinitions 方法从 Resource 中载入 BeanDefinition。

注释:一个真正完整的容器在启动阶段主要做几件事:

  1. 找到 Bean 定义,如 xml、注解等,如果是资源文件可以用 Resource 类来封装,支持 ClassPath、jar、URL 等;
  2. 初始化 Reader 注入 Resource,BeandefinitionReader 接口定义了解析相关的方法,Spring 默认提供了很多实现类;
  3. Reader 解析 BeanDefinition,初始化后注册到容器中。

2.5.ApplicationContext 容器的设计原理

以常用的 FileSystemXmlApplicationContext 的实现为例。主要功能已经在 AbstractXmlApplicationContext 中实现了,在 FileSystemXmlApplicationContext 中,作为一个具体的应用上下文,只需要实现和它自身设计相关的两个功能。

FileSystemXmlApplicationContext接口继承关系

如果应用直接使用 FileSystemXmlApplicationContext,对于实例化这个应用上下文的支持,同时启动 IOC 容器的 refresh() 过程。这个 refresh() 过程会牵涉 IOC 容器启动的一系列复杂操作,同时,对于不同的容器实现,这些操作都是类似的,因此在基类中将它们封装好。所以,我们在 FileSystemXml 的设计中看到的只是一个简单的调用。

注释:refresh 是一个上下文的很重要的操作。Spring 容器的启动,初始化一些容器启动必要的资源,BeanFactory 的创建、初始化,Bean 的创建、初始化、注册、非懒加载,注册和设置国际化工具类 MessageSource,注册和设置事件,等一系列过程在这个 refresh 方法里面进行调用。

FileSystemXmlApplicationContext 是一个从文件系统加载 XML 的上下文实现,因此设计了从文件系统中加载 XML 的功能。