Java 作为流行的开发语言被广大开发者所青睐,在 Java 平台提供丰富的应用程序开发功能的同时,其存在的问题也暴露出来。
图片来自包图网
这个问题就是其缺乏将基础组件构建成完整系统的能力,因此开发者需要通过各种设计模式,将开发的组件进行组合,从而构建成最终的应用。
为了解决这个问题,Spring 架构推出了 IoC 组件,它可以通过正规化的方法来组合不同的组件,让其成为完整的,可以用的应用。
从此开发人员无须手动设置对象的依赖关系,把这一工作交给了 Spring 容器去处理和管理,提升了开发体验。
今天将围绕 Spring IoC 给大家讲解其实现原理,接下来将会学到如下内容:
在介绍 Spring IoC 之前先来看看传统的对象(组件)依赖是怎么做的,假设通过 RESTFUL 的方式访问用户信息(User)。
如图 1 所示,用户请求一个 UserController 获取 User 信息,UserController 会调用 UserService,在 UserService 中会处理关于 User 的业务逻辑。
图 1:例子依赖关系
同时 UserService 会调用 UserDao,UserDao 负责调用数据库返回用户需要的信息。
从这张可以看出 UserController 依赖 UserService、UserService 依赖 UserDao。
如图 2 所示,假设在 UserController 中需要使用 UserService,就需要在其 UserController 构造函数中对 UserService 进行实例化。
图 2:传统的依赖关系需要自己管理对象实例化
这样才能 save 方法中使用 UserService,并且调用其 save 方法。
与传统的依赖方式不同,Spring IoC 会通过一个 XML 文件配置对象之间的关系。
如图 3 所示,在 beans 的标签中,定义了两个 bean,分别是 UserController 和 UserService。在 Class 属性中定义了 Class 的全程(包含 Namespace)。
图 3:Spring IoC 的依赖关系 XML 配置
需要注意的是在 UserController的bean 定义中指定了 contructor-arg 的 ref 为 UserService。
这里的含义是在 UserController 的构造函数中会引入 UserService,从而说明两者之间的依赖关系,也就是 UserController 会依赖 UserService。
看完了 XML 的配置再回头看看代码中有什么改变,如图 4 所示,在 UserController 的构造函数的初始化参数中加入 UserService 作为依赖项。
图 4:Spring IoC 代码中的改变
不过 New UserService 的动作就不再 UserController 中完成了,而是由 Spring 容器完成。
Spring 容器完成 UserService 的初始化之后,在 UserController 需要使用的时候直接使用这个 UserService 实体就行了。
图 5:Spring IoC 的 Spring 容器
这里再将 Spring IoC 做的事情梳理一下,如图 5 所示:
说白了 Spring IoC 做的事情就是管理和创建 Bean 的实例,同时保证 Bean 之间的依赖关系。
这里我们引出 Spring IoC,IoC(Inversion of Control)也称为控制反转,也就是对象定义其依赖关系的控制反转。
原来这个过程是:谁使用谁创建,例如上面的例子中 UserController 需要使用 UserService,于是就由 UserController 创建 UserService 的实例。
引入 IoC 以后,这个创建过程发生的反转,这些 UserController 和 UserService 之间的依赖关系由 XML 文件定义以后由 Spring 容器进行创建。
这个控制权从对象的使用者转换为 Spring 容器,就成为控制反转。也就是对象之间的依赖过程发生了变化,由原来的主动创建,变成了现在被动关联(因为 Spring 容器的参与),这种控制权颠的现象被称为控制反转。
前面说了 IoC 的来历和概念,实际上它是用来管理对象初始化的容器,这里会针对 Spring IoC 容器介绍其主要功能。
Spring IoC 容器将创建对象,通过配置设定它们之间的依赖关系,并管理它们的生命周期(从创建到销毁)。
Spring IoC 容器管理的对象被称为 Spring Beans,也就是上面例子中提到的 UserController 和 UserService。
通过阅读配置文件元数据提供的指令,容器知道对哪些对象进行实例化,配置和组装。
配这里的置元数据就是上面例子的 XML,不过处理 XML 的配置之外还可以通过 Java 注释或 Java 代码来表示,大家可以理解为一种配置对象之间关系的方式。
说了这么多的 Spring IoC 容器的作用,在 Spring 中实现 IoC 容器的实际代表者是谁呢?
这里介绍两类 Spring IoC 容器的代表者,分别是:
它是最简单的容器,用 org.springframework.beans.factory.BeanFactory 接口来定义。
BeanFactory 或者相关的接口,如 BeanFactoryAware,InitializingBean,DisposableBean,在 Spring 中仍然存在具有大量的与 Spring 整合的第三方框架的反向兼容性的目的。
添加了更多的企业特定的功能,例如从一个属性文件中解析文本信息的能力,发布应用程序事件给感兴趣的事件监听器的能力。
该容器是由 org.springframework.context.ApplicationContext 接口定义。
由于 ApplicationContext 容器包括 BeanFactory 容器的所有功能,同时 BeanFactory 适用于轻量级应用。
这里我们将目光放到 ApplicationContext 容器上,看看它是如何实现 Spring IoC 容器的功能的。
由于 ApplicationContext 是一个接口,针对它有几种不同的实现,这些实现会针对不同使用场景,以下列出三种不同实现:
FileSystemXmlApplicationContext:实现了从 XML 文件中加载 bean。初始化该类的时候需要提供 XML 文件的完整路径。
ClassPathXmlApplicationContext:也实现了 XML 文件中加载 bean,与上面一种方式不同的是:不需要提供 XML 文件的完整路径,只需正确配置 CLASSPATH 环境变量即可,容器会从 CLASSPATH 中搜索 bean 配置文件。
WebXmlApplicationContext:实现了在一个 web 应用程序的范围内加载在 XML 文件中已被定义的 bean。
由于篇幅原因,这里我们针对 FileSystemXmlApplicationContext 实现 Spring IoC 容器进行说明。
图 6:FileSystemXmlApplicationContext 实现 Spring IoC 容器
如图 6 所示:
在介绍过 Spring IoC 的原理和容器实现以后,相信大家对 IoC 有所了解了,不过任何技术和架构都有其优缺点 Spring IoC 也不例外在使用它之前,还需要对其有清晰的认识。
首先是优点的部分:
有优点就一定有缺点:
前面说了 IoC 及控制反转,一般来说和 IoC 一同出现的有 DI(Dependency Injection)也就是依赖注入,这两个概念之间有什么关系呢?
在 2004 年 Martin Fowler 在探索 IOC 控制反转问题的时候,提出:“哪些方面的控制被反转了呢?”,经过详细地分析和论证后,他得出了答案:“获得依赖对象的过程被反转了”。
控制被反转之后,获得依赖对象的过程由自身管理变为了由 IOC 容器主动注入。
于是,他给“控制反转”取了一个更合适的名字叫做“依赖注入(Dependency Injection)”。
他的这个答案,实际上给出了实现 IOC 的方法:注入。所谓依赖注入,就是由 IoC 容器在运行期间,动态地将某种依赖关系注入到对象之中。
就好像上面提到的例子一样,将 UserService 注入到 UserController 一样,这个过程是由 Spring IoC 容器来完成的。
因此,依赖注入(DI)和控制反转(IOC)是从不同角度描述同一件事情,就是指通过引入 IOC 容器,利用依赖关系注入的方式,实现对象之间的解耦。
基于对 IoC 和 DI 两个概念的理解,再来看看实现 DI 的两种方式:基于构造函数的 DI 和基于 setter 方法的 DI。
基于构造函数的 DI,在上面的例子中提到过这里进行一下回顾,如图 7 所示,在 XML 的配置文件中定义 UserController 的 bean 同时将 ref 指定 UserService,也就是需要注入的 bean。
图 7:构造函数 DI 的配置文件
需要注意的是,这里通过设置 contructor-arg 指定构造函数注入方式。
如图 8 所示,在类文件中 UserController 的构造函数中传入的参数就是 UserService。
Spring IoC 容器在完成依赖注入和对象初始化以后,在 UserController 中之间使用对象的实例展开后续的业务操作。
图 8:UserController 使用依赖注入和被初始化以后的 UserService 对象
如图 9 所示,在配置 bean 节点中稍微做了调整,将 contructor-arg 修改为了 property,通过 property 属性定义与 UserService 的 setter 方法的依赖关系。
图 9:UserController 定义 setter 方法的依赖关系
再来看看类中的修改,如图 10 所示,与构造函数注入方式不同的是,在 UserController 中加入了一个 setUserService 的方法来设置 UserService 的属性,传入的参数依旧是 UserService。
图 10:setter 方法的依赖注入在类中的实现
上面提到了通过构造函数和 setter 方法来注入备 bean 对象,其分别使用 XML 配置文件中的
为了减少 XML 配置的数量,Spring 容器可以在不使用
下面我们来看看几种自动装配的方式:
如果在类中定义了与其他类的依赖关系,那么 Spring 容器在 XML 配置文件中会通过类型寻找对应依赖关系的 bean,然后与之关联。这个过程容器会尝试匹配和连接属性的类型。
例如 bean A 定义了 X 类型的属性, Spring 会在 ApplicationContext 中寻找一个类型为 X 的 bean,并将其注入 bean A。
如果还是觉得抽象,我们看下面的例子,如图 11 所示,UserController 设置 UserService 属性时定义了与 UserService 的依赖关系。
图 11:定义 UserController 与 UserService 的依赖关系
如图 12 所示,在 XML 配置文件中 UserController 就不需要使用 property 属性定义与 UserService 之间的关系,取而代之的是使用 autowire=“byType” 的方法。
图 12:通过 byType 定义关系
容器通过类中 setUserService 传入的 UserService 类型自动在配置文件中寻找 UserService 对应的类型,从而完成 UserController 和 UserService 依赖关系,也就是依赖注入,这种方式也是基于类型的自动装载。
有了 byType 的基础这个很好理解,例如 bean A 的构造函数接受 X 类型的参数,容器会在 XML 寻找 X 类型的 bean,并将其注入到 bean A 的构造函数中。
如图 13 所示,UserController 在构造函数中定义 UserService 作为初始化参数,确定了 UserController 对 UserService 的依赖。
图 13:UserController 在构造函数中定义 UserService 作为初始化参数
如图 14 所示,在 XML 配置文件中 UserController 只需要设置 autowire=“constructor”。
告诉容器通过 UserController 类中的构造方法将 UserService 注入到 UserController 中,完成 UserController 和 UserService 依赖关系,这种方式也是基于构造器的自动装载。
图 14:通过 constructor 定义关系
例如:bean A 定义了一个名为 X 的属性,容器会在 XML 寻找一个名为 X 的 bean,将其注入到 bean A 中。
如图 15 所示,UserController 中定义了一个名为 myUserService 的成员属性,其类型是 UserService。
图 15:UserController 中定义了一个名为 myUserService 的成员属性
如图 16 所示,在 XML 的配置中 UserController 的 autowire 配置了“byName”。
此时容器会根据类中定义的 myUserService 成员属性(变量)自动关联到 UserService,在 UserController 中 setUserService 时自动装载 UserService 的实例。
图 16:XML 文件中 byName 的定义
本文从 Spring IoC 的由来说起,通过一个简单的对象依赖例子解释了 Spring IoC 解决的问题。
它将对象的依赖关系从对象内部转移到了 IoC 容器中完成,由容器来关系对象的注册和依赖关系。
说起 Spring IoC 容器,由 BeanFactory 和 ApplicationContext 接口完成具体工作。
针对常用的 ApplicationContext 接口的三个实现类,分别实现了根据 XML 加载实例、根据 CLASSPATH 加载实例和根据 Web 应用程序范围加载实例。
在分析完 IoC 的优缺点以后,解释了 IoC 与 DI 之间的关系,DI 从另外一个角度解释了 IoC,它是在 IoC 容器运行期间动态地将依赖关系注入到对象中。
常见的依赖注入方式有:构造函数注入和 setter 方法注入。同时也给大家介绍了 DI 的自动注入(加载),其内容包括 byType、constructor 和 byName 三种。
作者:崔皓
简介:十六年开发和架构经验,曾担任过惠普武汉交付中心技术专家,需求分析师,项目经理,后在创业公司担任技术/产品经理。善于学习,乐于分享。目前专注于技术架构与研发管理。