谈谈Spring的IoC(控制反转、依赖注入)

今天来谈谈Spring的核心之一——依赖注入。
依赖注入(Dependency Injection)也称为控制反转(Inverse of Control),直接叫IoC可能会更耳熟点。

什么是IoC

IoC,即控制反转,就是将传统的程序代码对对象的创建和管理的操控权交给外部容器负责,由容器实现对象的装配和管理,由容器来维护对象组件之间的依赖关系。
我们日常的 java 项目开发都是由两个或多个类的彼此合作来实现业务逻辑的,这使得每个对象都需要与其合作的对象的引用(称为所依赖的对象),如果合作的对象的引用或依赖关系由具体的对象来实现,这对复杂的面向对象系统的设计与开发是非常不利的,由此,我们如果能把这些依赖关系和对象的注入交给框架来实现,让具体对象交出手中对于依赖对象的控制,那么就能很大程度上解耦代码,这显然是极有价值的。而这,就是“依赖反转”,即反转对依赖的控制,把控制权从具体的对象中转交到平台或者框架。
这么说可能不够形象,下面用一个比喻来说明它的作用。

IoC的作用

在说IoC的作用之前,得先说说什么是耦合:
我们都知道,在采用面向对象方法设计的软件系统中,它的底层实现都是由N个对象组成的,所有的对象通过彼此的合作,最终实现系统的业务逻辑。


齿轮耦合

如果我们打开机械式手表的后盖,就会看到上图描述的场景,就是这样的一个齿轮组,它拥有多个独立的齿轮,这些齿轮相互啮合在一起,协同工作,共同完成某项任务。如果有一个齿轮出了问题,就可能会影响到整个齿轮组的正常运转。
齿轮组中齿轮之间的啮合关系,与软件系统中对象之间的耦合关系非常相似。对象之间的耦合关系是无法避免的,也是必要的,这是协同工作的基础。现在,伴随着工业级应用的规模越来越庞大,对象之间的依赖关系也越来越复杂,经常会出现对象之间的多重依赖性关系。对象之间耦合度过高的系统,必然会出现牵一发而动全身的情形。

对象之间复杂的依赖关系

而IoC正是为了解决对象之间的耦合度过高这一问题出现的。软件专家Michael Mattson提出了IOC理论,用来实现对象之间的“解耦”。
IoC解耦

可见,由于引进了中间位置的“第三方”,也就是IOC容器,使得A、B、C、D这4个对象没有了耦合关系,齿轮之间的传动全部依靠“第三方”了,全部对象的控制权全部上缴给“第三方”IOC容器,所以,IOC容器成了整个系统的关键核心。这时候,A、B、C、D这4个对象之间已经没有了耦合关系,这样的话,当你在实现A的时候,根本无须再去考虑B、C和D了,对象之间的依赖关系已经降低到了最低程度。
以上是通俗易懂的讲解了IoC的作用,接下来在这里强烈推荐一篇文章,它生动地从实际开发角度来体现使用了SpringIoC后web项目的代码和功能一步步发生的变化:用小说的形式讲解Spring(1) —— 为什么需要依赖注入,作者写了一个小系列,我个人感觉前几篇写的很棒!

Spring IoC的概述

实现依赖反转的实现有很多种,在 Spring 中,IoC 容器就是实现这个模式的载体,它可以在对象生成或初始化的过程中,直接将数据或者依赖对象的引用注入到对象的数据域中从而来实现方法调用的依赖。而这种依赖注入是递归的,依赖对象会被逐层注入,从而建立起一套有序的对象依赖关系,简化了对象依赖关系的管理,把面向对象过程中需要执行的如何新建对象、为对象引用赋值等操作交由容器统一管理,极大程度上减低了面向对象编程的复杂性。
Spring IoC 提供了一个基本 JavaBean 容器,通过 IoC 模式管理依赖关系,并通过依赖注入和 AOP 切面增强了为 JavaBean 这样的 POJO 对象提供了事务管理、声明周期管理等功能。
在应用管理依赖关系时,如果在 IOC 实现依赖反转的过程中,能通过可视化的文本来完成配置,并且通过工具对这些配置信息进行可视化的管理和浏览,那么肯定能提高依赖关系的管理水平,而且如果耦合关系变动,并不需要重新修改和编译 Java 代码,这符合在面向对象过程中的开闭原则。

Spring IoC容器系列的实现

在IoC模式中,被注入对象是通过哪些方式来通知IoC容器为其提供适当服务的呢?
常用的有两种方式:构造方法注入和setter方法注入,还有一种已经退出历史舞台的接口注入方式,下面就比较一下三种注入方式:

接口注入。从注入方式的使用上来说,接口注入是现在不甚提倡的一种方式,基本处于“退
役状态”。因为它强制被注入对象实现不必要的接口,带有侵入性。而构造方法注入和setter
方法注入则不需要如此。
构造方法注入。这种注入方式的优点就是,对象在构造完成之后,即已进入就绪状态,可以
马上使用。缺点就是,当依赖对象比较多的时候,构造方法的参数列表会比较长。而通过反
射构造对象的时候,对相同类型的参数的处理会比较困难,维护和使用上也比较麻烦。而且
在Java中,构造方法无法被继承,无法设置默认值。对于非必须的依赖处理,可能需要引入多个构造方法,而参数数量的变动可能造成维护上的不便。
setter方法注入。因为方法可以命名,所以setter方法注入在描述性上要比构造方法注入好一些。 另外,setter方法可以被继承,允许设置默认值,而且有良好的IDE支持。缺点当然就是对象无法在构造完成后马上进入就绪状态。
其实,这些操作都是由IoC容器来做的,我们所要做的,就是调用IoC容器来获得对象而已。

Spring中提供了两种IoC容器:

BeanFactory和ApplicationContext


IoC容器接口设计

我们可以看到,ApplicationContext是BeanFactory的子类,所以,ApplicationContext可以看做更强大的BeanFactory,他们两个之间的区别如下:

  • BeanFactory。基础类型IoC容器,提供完整的IoC服务支持。如果没有特殊指定,默认采用延迟初始化策略(lazy-load)。只有当客户端对象需要访问容器中的某个受管对象的时候,才对该受管对象进行初始化以及依赖注入操作。所以,相对来说,容器启动初期速度较快,所需要的资源有限。对于资源有限,并且功能要求不是很严格的场景,BeanFactory是比较合适的IoC容器选择。

  • ApplicationContext。ApplicationContext在BeanFactory的基础上构建,是相对比较高级的容器实现,除了拥有BeanFactory的所有支持,ApplicationContext还提供了其他高级特性,比如事件发布、国际化信息支持等,ApplicationContext所管理的对象,在该类型容器启动之后,默认全部初始化并绑定完成。所以,相对于BeanFactory来说,ApplicationContext要求更多的系统资源,同时,因为在启动时就完成所有初始化,容器启动时间较之BeanFactory也会长一些。在那些系统资源充足,并且要求更多功能的场景中,ApplicationContext类型的容器是比较合适的选择。

下面我们来对这张接口设计图做一下解析:

第一条接口设计主线是:BeanFactory -> HierarchicalBeanFactory -> ConfigureBeanFactory。在这条设计路径中,BeanFactory 定义了 IoC 容器的基本规范,包括了例如 getBean()(从容器中取得 Bean)这样的 IoC 容器基本方法。而 HierarchicalBeanFactory 接口则在继承 BeanFactory 基础上,增加了 getParentBeanFactory() 的接口功能,使得 BeanFactory 具备了双亲 IoC容器的管理功能。接下来的 ConfigureBeanFactory 接口中,主要定义了一些对 BeanFactory 的配置功能,如通过 setParentBeanFactory() 设置双亲容器和通过 addBeanPostProcessor() 设置 Bean 后置处理器等。通过这些接口的叠加,定义了 BeanFactory 就是简单 IoC 容器的基本功能。

第二条设计主线是以 ApplicationContext 为核心的路径:BeanFactory -> ListableBeanFactory -> ApplicationContext 再到我们常用的 WebApplicationContext 或 ConfigureApplicationContext。在这条路径中,ListableBeanFactory 和 HierarchicalBeanFactory 连接了 BeanFactory 接口和 ApplicationContext 应用上下文,ListableBeanFactory 细化了 BeanFactory 接口功能,HierarchicalBeanFactory 上文已经提到了。对于 ApplicationContext,它通过集成 MessageSource、ResourceLoader、ApplicationEventPublisher 接口,在 BeanFactory 简单的 IoC 容器的基础上添加了许多对高级容器的特性和支持。

这里主要涉及的是接口关系,而具体的容器是在这个接口体系下实现的,必须 DefaultListableBeanFactory 这个基本的 IoC 容器就实现了 ConfigureListableBeanFactory,从而成为一个简单的 IoC 容器的实现。而其他的 BeanFactory 必须 XmlBeanFactory 都是在 DefaultListableBeanFactory 的基础上做扩展,同样的 ApplicationContext 体系也是一样。

我们通过以上的接口设计图跟分析可以看出,整个 Spring IoC 容器就是以 BeanFactory 和 ApplicationContext 作为核心的。BeanFactory 定义了 IoC 容器的基本功能,而 ApplicationContext 体系则在 BeanFactory 基础上通过继承其他接口来实现高级容器特征。下面我们来看一下这两个体系的应用场景:

BeanFactory 的应用场景

BeanFactory 提供的是最基本的 IoC 容器的功能,关于这些功能,我们可以在接口中看到:


BeanFactory接口

BeanFactory 接口定义了 IoC 容器最基本的形式,并且提供了 IoC 容器所应该遵循的最基本服务契约,同时也是我们使用 IoC 容器所应该遵守的最底层和最基本的编程规范,这些接口方法定义勾画出了 IoC 容器的基本轮廓。而在 Spring 的的代码实现中,BeanFactory 只是一个接口类,而后面的 DefaultListableBeanFactory、XmlBeanFactory、ApplicationContext 等都可以看成是容器附加了某种功能的实现。
BeanFactory 中定义了 getBean 方法,其参数类型有 Bean 的名字和其他参数,这些都是对 IoC 容器中存在的 Bean 进行索引。同时还定义了其他方法,方法的目的可以通过名字很明显的看出来,这里就不一一说明了。
可以看到,这里定义的只是接口方法,而这一系列的接口,使用不同的 Bean 的检索方法,很方便的从 IoC 容器中得到所需要的 Bean,从这个角度来看,这些方法代表的是最基本的 IoC 容器入口。

BeanFacory 的设计原理

BeanFactory 接口提供了使用 IoC 容器的基本规范,而在这个规范之上, Spring 还提供了符合这个 IoC 容器接口的一系列容器的实现来供开发人员使用,下面我们以 XmlBeanFactory 为例来简单说明一下 IoC 容器的实现原理。

XmlBeanFactory类的继承关系

可以看到,作为最简单 IoC 容器系列最底层实现的 XmlBeanFactory,与我们 Spring 应用中用到的那些上下文相比,有一个明显的特点:它只提供最基本的 IoC 容器的功能,理解这一点有助于帮我我们理解 ApplicationContext 和 BeanFactory 之间的区别与联系。

XmlBeanFactory 在继承 DafaultListableBeanFactory 类的基础上,新增了新的功能。从类的名字上可以看出应该是与读取 xml 配置文件相关的 IoC 容器。那么该读取过程是怎么实现的?

我们先来看一下 XmlBeanFactory 的源码:

@Deprecated
public class XmlBeanFactory extends DefaultListableBeanFactory {

    private final XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(this);
    
    public XmlBeanFactory(Resource resource) throws BeansException {
        this(resource, null);
    }   
    public XmlBeanFactory(Resource resource, BeanFactory parentBeanFactory) throws BeansException {
        super(parentBeanFactory);
        this.reader.loadBeanDefinitions(resource);
    }
}

可以看出,在 XmlBeanFactory 中,初始化了一个 XmlBeanDefinitionReader 对象,实际上,该对象就是处理以 XML 方式定义的 BeanDefinition 的地方。

在构造 XmlBeanFactory 这个 IoC 容器,需要指定 BeanDefinition 的信息来源,而这个来源需要封装成 Spring 中的 Resource 类(Spring 用来封装 I/O 操作的类)。将 Resource 作为构造参数传递给 XmlBeanFactory 构造函数。这样, IoC 容器就可以方便的定位到需要的 BeanDefinition 信息来对 Bean 完成容器的初始化和依赖注入的过程。至于如何获取 Resource 和将 Resouce 转化为 BeanDefinition,这以后再细说。(待补链接)

我们看到 XmlBeanFactory 使用 DefaultListableBeanFactory 作为基类,该类非常重要,是我们经常用到的一个 IoC 容器的实现,必须在设计应用上下文 ApplicationContext 就会用到它,我们可以看到这个类包含了基本 IoC 容器所具有的重要功能,也是在很多地方都会用到的容器系列中的一个基本产品,其他很多的 IoC 容器都是通过持有和扩展 DefaultListableBeanFactory 来获得特性功能的 IoC 容器的。

参考 XmlBeanFactory 的实现,我们使用编程的方式来使用 DefaultListableBeanFactory。从中我们可以看到 IoC 容器使用的一些基本过程,尽管实际开发中我们很少用到这种方式,但这对于我们理解 IoC 容器的工作原理是非常有帮助的。因为在这个编程式使用容器的过程中,很清楚的揭示了 IoC 容器实现中的那些关键类(Resource、DefaultListableBeanFactory、BeanDefinitionReader)之间的相互关系:

// 编程式使用容器的代码:
ClassPathResource res = new ClassPathResource("bean.xml") ;
DefaultListableBeanFactory factory = new DefaultListableBeanFactory() ;
XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(factory) ;
reader.loadBeanDefinitions(res) ;

通过上面的代码,我们可以看到使用 IoC 容器大概有以下四个步骤:

1、创建 IoC 配置文件的 Resource 资源,这个资源包含了 BeanDefinition 的定义信息。
2、创建一个 BeanFactory,这里使用 DefaultListableBeanFactory。
3、创建一个载入 BeanDefinition 的解读器,这里使用 XmlBeanDefinitionReader 来载入 XML 文件形式 BeanDefinition,通过一个回调配置给 factory。
4、从定义好的资源位置读入配置信息,具体的解析过程由 XmlBeanDefinitionReader 的 loadBeanDefinitions() 方法来完成。完成整个载入和注册 Bean 定义之后,需要的 IoC 容器就建立起来就可以使用了。

ApplicationContext 的应用场景

在 Spring 中,系统已经为用户提供了很多定义好的容器实现。而相比那些简单扩展 BeanFactory 的基本 IoC 容器,开发人员常使用的 ApplicationContext 除了能够提供前面容器介绍的基本功能之外,还为用户提供了以下附加服务,可以让用户更加方便的使用。所以说 ApplicationContext 是一个高级形态的 IoC 容器,如下图所示可以看到 ApplicationContext 在 BeanFactory 的基础上所提供的附加服务:


ApplicationContext 类继承关系
  • 支持不同的信息源。我们看到 ApplicationContext 扩展了 MessageSource 接口,这些信息源的扩展功能可以支持国际化的实现,为开发多语言版本的应用提供服务。
  • 访问资源。这一点体现在对 ResourcePatternResolver 的继承上。这样我们可以从不同的地方得到 Bean 定义资源。这种抽象使用户程序可以灵活地定义 Bean 信息,尤其是从不同的 I/O 途径得到 Bean 的定义信息。关于 Resource 在 IoC 容器中的使用,我们后面会详细介绍。
  • 支持应用事件。继承了接口 ApplicationEventPublisher,从而在上下文引入了事件机制,这些事件和 Bean 的生命周期的结合为 Bean 的管理提供了便利。
  • 在 ApplicationContext 中提供的附加服务,使得基本 IoC 容器的功能更加丰富,对它的使用是一种面向框架的使用风格,所以建议在开发应用时使用 ApplicationContext 作为 IoC 容器的基本形式

ApplicationContext 的设计原理

我们通过 FileSystemXmlApplicationContext 为例子来分析 ApplicationContext 的设计原理。
作为具体的应用上下文,我们只需要关注和它自身设计相关的功能,而其主要功能已经在其基类 AbstractXmlApplicationContext 中实现了。

  • 一个功能是启动 IoC 容器的 refresh() 过程:
public FileSystemXmlApplicationContext(String[] configLocations, boolean refresh, ApplicationContext parent)
   throws BeansException {

   super(parent);
   setConfigLocations(configLocations);
   if (refresh) {
       refresh();
   }
}

这个 refresh() 涉及到 IoC 容器启动时候一系列复杂的操作,而不同的容器,其操作都是类似的,因此将其封装在基类 AbstractApplicationContext 中。在 FileSystemXmlApplicationContext 中仅仅涉及到简单的调用而已。关于 refresh() 在 IoC 容器启动时的表现,该方法可以说是所有 IoC 容器的入口,这是一个很重要的方法。

  • 另外一个功能是与这个独特的应用上下文的设计有关,即从文件系统中加载 XML 的 Bean 定义资源。
protected Resource getResourceByPath(String path) {
    if (path != null && path.startsWith("/")) {
        path = path.substring(1);
    }
    return new FileSystemResource(path);
}

可以看到,通过调用这个方法,可以得到 FileSystemResouce 的资源定位。
到此为止,我们大概了解了 IoC 容器的概念跟 Spring 中对于 IoC 容器的设计跟应用。
其他细节(此处待具体填充),将在其他文章写出。

参考:
https://www.cnblogs.com/superjt/p/4311577.html

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,186评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,858评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,620评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,888评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,009评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,149评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,204评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,956评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,385评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,698评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,863评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,544评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,185评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,899评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,141评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,684评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,750评论 2 351

推荐阅读更多精彩内容