本文主题
IOC 在我印象中是非常迷人的设计理念。因为相信每一个程序猿在写代码的时候,都想设计出耦合度低,灵活高,复用性高的代码。而 IOC 恰好给予了我们一丝丝在设计代码或框架的灵感,让代码设计变得不那么蹩脚和混乱。所以今天还是重新做一个 IOC 的重温文章,可能更多来说非讲解类型而是总结类型,便于自己日后复习和补充。本文思路:
- IOC 是什么
- IOC 的策略
- IOC 的职责
- IOC 的实现
- IOC In Spring
什么是 IOC ?
IOC 英文名是 Inversion Of Control,意为“控制反转”。相信很多人第一次听到这个单词,应该是学 Spring 的时候。也因为接触 Spring 比较多,后来者甚至认为 IOC 的设计模式就是 Spring Team 首创。其实也挺巧的,在 2004 年,Martin Fowler 就提出了“哪些方面的控制被反转了?”这个问题。而 Spring 也是 2004 年开始建立第一个版本的。Martin Fowler 对于这个问题给出了一个结论依赖对象的获得被反转了
这句话在我看来,算是总结了整个 IOC 的核心思想。为什么呢?多数应用程序都是由两个或是更多的类通过彼此的合作来实现企业逻辑。而假设我们没有使用 IOC。当我们某一个 UserService 类需要一个 UserDao[数据层面操作类]的时候,我们是直接通过 new 的方式来获取对象的
UserDao userDao = new UserDaoImpl();
这种方式对于 UserSerivce 来说,它是依赖了 UserDao。但是直接 new 对象有什么不好吗?好,这是教科书般的写法!但是这种写法会导致高度耦合度。当 UserService 想换一个 UserDao 的实现类的时候,它需要进行代码的更换。如果一个企业级别的项目已经有一百个地方需要更改的话,相信你会疯掉。
回到 Martin Fowler 说的那句精髓。如果依赖的对象被反转了,那么相对 UserService 来说,UserDao 是被反转了。
而怎么反转呢?因为 new 是主动的表现,那么反转你可以理解为被动接收!所谓的被动也就是在某个时刻别人把东西送到我们而不用我们主动去拿。相信大家都听说监听者模式。监听者模式是等待广播器进行广播事件的;又或者是队列,消费者是等待生产者推送消息的。这两个例子都可以理解为被动接收!
或许你会说,那么 IOC 是不是监听者设计模式呢?其实它们仅仅是交集,它们都有被动接收的异曲同工之妙而已。当然你也可以认为,所有这种具有“被动”意义的设计理念都有些 IOC 的影子。
更加,上面的内容说明了 IOC 是实打实一种面向对象编程的一种设计原则,可以用于降低计算机代码之间的耦合度。
IOC 的策略
来自 wiki 的介绍,我们能得到 IOC 实现的策略- service location pattern[服务定位模式]
- dependency injection[依赖注入]
- constructor injection[构造器注入]
- param injection[参数注入]
- Setter injection[函数注入]
- interface injection[接口注入]
- contextualized lookup[上下文的依赖查找]
- template method design pattern[模板方法设计模式]
- strategy dessign pattern[策略设计模式]
其实,对于上面的策略来说,被实现和体现的是我们现在依旧流行的 Spring 和传说中的重量级框架 EJB。现在我们来讲一下 IOC 的依赖注入和依赖查找。依赖查找的意思是,应用通过一个框架性质的上下文来进行 IOC 的实现,然后我们可以通过这个上下文容器去获得查找自己想要的依赖;而依赖注入是说,组建不需要去查找自己需要的依赖,而是上下文容器会通过写入 Java Bean 的参数或构造器来将实现依赖注入。
IOC 的职责
讲完了 IOC 的策略,我们可以讲讲 IOC 在实际应用中的一些职责。- 如上面所说,IOC 是为了解耦代码;
- IOC 可以让开发者关注于设计的最终目标而不是具体的实现,也就是抽象与实现的分离;
- IOC 可以让某个模块进行释放。所谓的释放就是,尽量让模块变得独立,不需要依赖任何的系统或者契约。例如我们传统 Java EE 中有 JNDI 的技术使组件的实现和组件进行解耦,但它的实现却是依赖了 JNDI 这种契约;
- IOC 需要实现的“被动接收”的方式,也就是 IOC 容器会将依赖注入到某个类所需要的依赖
上面是一些抽象性的描述,我们来看一下它比较明确的实现描述。IOC 的职责有以下几点:
- 依赖处理
- 依赖查找
- 依赖注入
- Bean 的生命周期管理
- 容器
- 托管资源[Java Beans 或其他资源]
- 配置
- 容器
- 外部化配置
- 托管资源[Java Beans 或其他资源]
上面有一点需要说明的,就是“Java Beans 或其他资源”。对于 IOC 容器来说,它负责解决的不仅仅是 Bean,还有一些非 Bean,例如说配置资源例如说在 IOC 发生事件的时候外部添加进去的,又或者是 Spring 重的事件监听,这些无法通过依赖注入或查找来进行管理的资源。所以不要把 IOC 容器的职责发布范围仅仅局限于 Java Bean。
相同,在“配置”一栏中,托管资源属于“其他资源”的话,也有可能是 XML 和注解信息等资源信息;而外部化配置呢,是将通过配置文件来影响 IOC 的运行行文的一种方式。
不仅限于 Java Bean 是 IOC 考虑周全的一种表现。
IOC 的实现
或许这 part 才是最激动人心的,因为程序员最喜欢的是看到一个真正的东西呈现在他眼前。IOC 让人最深刻的实现其实就是 Spring。当面试官问“什么是 IOC”,几乎大部分 Java 面试者或多或少都会说到 Spring,几乎来说是“根深蒂固”的感觉。
其实对于有关 IOC 的问题,我觉得应该从其历史演变性来谈的话,才是最迷人的。例如说,IOC 的早期在 Java SE 上的实现已经有了:
- Java Beans
- Java ServiceLoader SPI
- JNDI (Java Naming and Directory Interface)
而在 Java EE 的时候,有了:
- EJB (Enterprose Java Beans)
- Servlet
再到开源企业级应用的时候:
- Apache Avaion
- PicoContainer
- Google Guice
- Spring Framework
我挑几个来回答一下。
Java Beans 是怎么体现的?
- 依赖查找
- 生命周期
- 配置元信息
- 事件机制,传统 Java 事件机制
- 提供相关的自定义,对方法和字段进行描述性说明和类型转换
- Java Beans 可以持久化
- BeanContext
为什么 SPI 也算是实现的一种呢?
我们回想 SPI 的机制,我们会发现其实当我们使用 ServiceLoader.load(Class.class) 进行调用的时候,Java 是不是已经自动搬我们将在 META-INF 下面的 Bean 加载进行来?实质上 Java 已经将实例化与调用进行了分离。而我们“主动去调用”这个动作算是 IOC “依赖查找”的这种方式。
Java EE 的 Servlet 为什么也算?
我问过身边挺多人的,其实很多人都没想过这个问题。其实我在刚开始学习 Java 的时候就想过一个问题:servlet 的 request 和 response 从哪里来的?
但是现在一旦用 IOC 来想的话,是不是有内味儿?有人会说,不是 Tomcat 的容器传进来的吗?对,不够严谨的说就是 Tomcat 传进来的。当时主要是传进来的这个过程,servlet 并不关心于传进来的具体实现,也不用关注需要什么时候去 new 一个。servlet 仅仅需要知道,它被调用的时候有这个对象即可。
♥ 像企业级别的开源 IOC 实现就不需要多说了。从中我们可见这个十几二十年来,IOC 也不算地创新不断地前进,值得我们去学习。
下面我将会讲一下 IOC IN Spring。
IOC In Spring
IOC 策略在 Spring
总结一些比较常见的 IOC 在 Spring 的策略:
| 策略 | 体现 |
|---|---|
| 依赖注入 | @Autowire / 构造器 / Getter |
| 依赖查找 | 直接通过 BeanFactory 进行 getBean 操作 |
| 接口注入 | Aware 接口 / BeanPostProcessor 接口 |
| 服务定位模式 | spring.factories |
看到表格,其实我们能发现我们最常用的是依赖注入和 依赖查找。两者更进一步的话,我们会发现两者很大概率我们会使用依赖注入。或许你会有疑问,既然有了注入为什么还要查找呢?列举一个它们两的区别图[下面使用简称代替中文名称]:
| 类型 | 依赖处理 | 便利性 | 侵入性 | API 依赖性 | 可读性 |
|---|---|---|---|---|---|
| DS | 主动 | 相对繁琐 | 侵入业务逻辑 | 依赖容器 API | 良好 |
| DI | 被动 | 相对便利 | 低侵入性 | 不依赖容器 API | 一般 |
我解释一下,一般来说 DS 一般来说至少有一句代码 container.getBean("beanName") 来获取 Bean,这样子就是算是侵入业务逻辑了,但是这样子可以比较明确清楚我们想要哪个 bean,所以可读性比较好利于排查问题;而 DI 一般来说是 IOC 容器根据规则来注入的,所以如果发生注入前被修改或者注入了其他 Bean,是相对来说比较难定位问题的,所以其虽然便利,低侵入性,但是可读性一般。
IOC 在 Spring 的实现
在 Spring 中,BeanFactory 和 ApplicationContext 是 IOC 容器的实现。如果你看过 BeanFactory 的 API,你会发现它有通过姓名或类型来查找 Bean 的 getBean() / 还有判断是否是单例 isSingleton() / 判断是否匹配 isTypeMatch() 等等方法,是比较典型的一个 IOC 容器。但是 ApplicationContext 为什么也是 IOC 容器呢?在 Spring 官档中,ApplicationContext 被描述为 BeanFactory 的子类,而 ApplicationContext 在 BeanFactory 基础上增多了更多的特性例如 AOP 的整合 / 事务的发布 / 国际化等等。
所以如果当我们被问起 BeanFactory 和 ApplicationContext 的区别或者谁才是Spring IoC容器 的时候,我们可以回答 BeanFactory 是 IOC 容器,但是它仅仅是提供了一个非常基础功能的容器;而 ApplicationContext 是包含了 BeanFactory,而且在此基础上增加了更多符合企业级应用的特性。它们两者是包含关系。但是注意一点,当你的应用并不需要这些特性的时候,你可以直接使用 BeanFactory 来完成你的需求,这样更简洁更轻量,这也是 Spring 在 IOC 的层次组合上的优势。
IOC 在 Spring 的数据来源
对于 IOC 的数据来源,作了以下的总结:
- 自定义 Bean,也就是我们自己定义的 Bean
- 容器内建 Bean 对象,指的是框架运行时内部必要的 Bean 例如 BeanFactory
- 容器内建依赖,这个概念我举个例子例如说 Environment 接口[一个具有环境参数性质的接口],容器内建 Bean 对象是依赖于它的。这个也是比较重要的来源。
Spring IOC 配置元信息
我们说了这么多关于 IOC 的策略和依赖注入类型,但是唯独没有讲到 IOC 容器中的元信息是从哪里来的!下面,我们根据 Spring 的情况来做几个维度的讲述
| 来源方向 | 来源途径 |
|---|---|
| Bean 定义配置 | 基于 XML / Properties / Java 注解/ Java API |
| IOC 容器配置 | 基于 XML / Java 注解 / Java API |
| 外部化属性配置 | 基于 Java 注解 |
上面说明一下, Bean 定义配置讲的是需要进行依赖的 Bean,这个一般会影响到你业务行为;IOC 容器配置是指通过配置参数来影响容器的行为;外部化属性配置这个是指通过外部配置文件来配置元信息,它可以影响 Bean 的行为。