Spring:IOC

IOC背景

提出问题

IOC的理论背景

我们知道在面向对象设计的软件系统中,它的底层都是由N个对象构成的,各个对象之间通过相互合作,最终实现系统地业务逻辑。

img

如果我们打开机械式手表的后盖,就会看到与上面类似的情形,各个齿轮分别带动时针、分针和秒针顺时针旋转,从而在表盘上产生正确的时间。图1中描述的就是这样的一个齿轮组,它拥有多个独立的齿轮,这些齿轮相互啮合在一起,协同工作,共同完成某项任务。我们可以看到,在这样的齿轮组中,如果有一个齿轮出了问题,就可能会影响到整个齿轮组的正常运转。

齿轮组中齿轮之间的啮合关系,与软件系统中对象之间的耦合关系非常相似。对象之间的耦合关系是无法避免的,也是必要的,这是协同工作的基础。现在,伴随着工业级应用的规模越来越庞大,对象之间的依赖关系也越来越复杂,经常会出现对象之间的多重依赖性关系,因此,架构师和设计师对于系统的分析和设计,将面临更大的挑战。对象之间耦合度过高的系统,必然会出现牵一发而动全身的情形。

img

耦合关系不仅会出现在对象与对象之间,也会出现在软件系统的各模块之间,以及软件系统和硬件系统之间。如何降低系统之间、模块之间和对象之间的耦合度,是软件工程永远追求的目标之一。为了解决对象之间的耦合度过高的问题,软件专家Michael Mattson 1996年提出了IOC理论,用来实现对象之间的“解耦”,目前这个理论已经被成功地应用到实践当中。

应用

适用性

  • 依赖注入。
  • 依赖检查。
  • 自动装配。
  • 支持集合。
  • 指定初始化方法和销毁方法。
  • 支持某些回调方法。

IOC使得对象只需要发挥自己的特长即可,让你脱离对依赖对象的维护,只需要随用随取,不需要关心依赖对象的任何过程。

img

应用场景

基础概述

什么是IOC

IOC涉及代码解耦、设计模式、代码优化等问题的考量。

IOC是Inversion of Control的缩写,多数书籍翻译成“控制反转”。

1996年,Michael Mattson在一篇有关探讨面向对象框架的文章中,首先提出了IOC 这个概念。对于面向对象设计及编程的基本思想,前面我们已经讲了很多了,不再赘述,简单来说就是把复杂系统分解成相互合作的对象,这些对象类通过封装以后,内部实现对外部是透明的,从而降低了解决问题的复杂度,而且可以灵活地被重用和扩展。

IOC理论提出的观点大体是这样的:借助于“第三方”实现具有依赖关系的对象之间的解耦。如下图:

img

大家看到了吧,由于引进了中间位置的“第三方”,也就是IOC容器,使得A、B、C、D这4个对象没有了耦合关系,齿轮之间的传动全部依靠“第三方”了,全部对象的控制权全部上缴给“第三方”IOC容器,所以,IOC容器成了整个系统的关键核心,它起到了一种类似“粘合剂”的作用,把系统中的所有对象粘合在一起发挥作用,如果没有这个“粘合剂”,对象与对象之间会彼此失去联系,这就是有人把IOC容器比喻成“粘合剂”的由来。

我们再来做个试验:把上图中间的IOC容器拿掉,然后再来看看这套系统:

img

我们现在看到的画面,就是我们要实现整个系统所需要完成的全部内容。这时候,A、B、C、D这4个对象之间已经没有了耦合关系,彼此毫无联系,这样的话,当你在实现A的时候,根本无须再去考虑B、C和D了,对象之间的依赖关系已经降低到了最低程度。所以,如果真能实现IOC容器,对于系统开发而言,这将是一件多么美好的事情,参与开发的每一成员只要实现自己的类就可以了,跟别人没有任何关系!

我们再来看看,控制反转(IOC)到底为什么要起这么个名字?我们来对比一下:

软件系统在没有引入IOC容器之前,如图1所示,对象A依赖于对象B,那么对象A在初始化或者运行到某一点的时候,自己必须主动去创建对象B或者使用已经创建的对象B。无论是创建还是使用对象B,控制权都在自己手上。

软件系统在引入IOC容器之后,这种情形就完全改变了,如图3所示,由于IOC容器的加入,对象A与对象B之间失去了直接联系,所以,当对象A运行到需要对象B的时候,IOC容器会主动创建一个对象B注入到对象A需要的地方。

通过前后的对比,我们不难看出来:对象A获得依赖对象B的过程,由主动行为变为了被动行为,控制权颠倒过来了,这就是“控制反转”这个名称的由来。

Spring IOC

Spring框架的核心是Spring容器。容器创建对象,将它们装配在一起,配置它们并管理它们的完整生命周期。Spring 容器使用依赖注入来管理组成应用程序的组件。容器通过读取提供的配置元数据来接收对象进行实例化,配置和组装的指令。该元数据可以通过XML,Java注解或Java代码提供。

IOC容器支持加载服务时的饿汉式初始化和懒加载。

从代码角度来理解

依赖注入:让调用类对某一接口实现类的依赖关系由第三方注入,以移除调用类对某一接口实现类的依赖。

以使用演员编排剧本来理解。

不使用IOC,即直接使用演员编排剧本:

1
2
3
4
5
6
public class MoAttack{
public void cityGeteAsk(){
LiuDeHua ldh = new LiuDeHua();
ldh.responseAsk("墨者A");
}
}

但是演员刘德华直接侵入了剧本,而使得剧本与演员直接耦合。而明智的设定应该基于抽象,即应该围绕故事的角色,而不是具体的演员。

因此为角色定义一个接口:

1
2
3
4
5
6
7
8
public class MoAttack{
public void cityGeteAsk(){
//引入角色接口。
GeLi geli = new LiuDeHua();
//使用接口展开剧情
geli.responseAsk("墨者A")
}
}

但这个版本依然存在问题,因为MoAttack同时依赖于接口与LiuDeHua,而不仅仅是依赖于角色。但是角色又必须最终通过具体的演员而实现。

那么如何让LiuDeHua最终与剧本无关而又能完成GeLi的具体动作,则是导演在开演时,将LiuDeHua的角色安排在GeLi角色上,导演负责剧本、角色、饰演者三者的协调控制。

1566475154937

IOC关键

控制反转:

  • 控制。指选择GeLi角色扮演者的控制权。
  • 反转。这种控制权从剧本中移除,移交到导演手中,即某一接口的具体实现类的选择控制权从调用类中移除,转交给第三方决定,即由Spring容器借Bean配置来进行控制。

即类本身只需要关注它自己的职责。

权衡

优点

  • 它将最小化应用程序中的代码量。
  • 它将使您的应用程序易于测试,因为它不需要单元测试用例中的任何单例或JNDI查找机制。
  • 它以最小的影响和最少的侵入机制促进松耦合。
  • 它支持即时的实例化和延迟加载服务。

缺点

使用IOC框架产品能够给我们的开发过程带来很大的好处,但是也要充分认识引入IOC框架的缺点,做到心中有数,杜绝滥用框架。

  1. 软件系统中由于引入了第三方IOC容器,生成对象的步骤变得有些复杂,本来是两者之间的事情,又凭空多出一道手续,所以,我们在刚开始使用IOC框架的时候,会感觉系统变得不太直观。所以,引入了一个全新的框架,就会增加团队成员学习和认识的培训成本,并且在以后的运行维护中,还得让新加入者具备同样的知识体系。
  2. 由于IOC容器生成对象是通过反射方式,在运行效率上有一定的损耗。如果你要追求运行效率的话,就必须对此进行权衡。
  3. 具体到IOC框架产品(比如:Spring)来讲,需要进行大量的配制工作,比较繁琐,对于一些小的项目而言,客观上也可能加大一些工作成本。
  4. IOC框架产品本身的成熟度需要进行评估,如果引入一个不成熟的IOC框架产品,那么会影响到整个项目,所以这也是一个隐性的风险。

我们大体可以得出这样的结论:

  • 一些工作量不大的项目或者产品,不太适合使用IOC框架产品。
  • 如果团队成员的知识能力欠缺,对于IOC框架产品缺乏深入的理解,也不要贸然引入。
  • 最后,特别强调运行效率的项目或者产品,也不太适合引入IOC框架产品,像WEB2.0网站就是这种情况。

Spring IOC

IOC为控制反转,把传统意义上由程序代码直接操控的对象的调用权交给容器,通过容器来实现对象组件的装配和管理。由容器动态地将某种依赖关系注入到组件中。

概述

Spring实现

spring的IOC容器种类:

  • BeanFactory - BeanFactory就像一个包含bean集合的工厂类。它会在客户端要求时实例化bean。
  • ApplicationContext - ApplicationContext接口扩展了BeanFactory接口。它在BeanFactory基础上提供了一些额外的功能。
BeanFactory ApplicationContext
它使用懒加载 它使用即时加载
它使用语法显式提供资源对象 它自己创建和管理资源对象
不支持国际化 支持国际化
不支持基于依赖的注解 支持基于依赖的注解

实现

构造函数注入

即通过类的构造函数,将接口实现类通过构造函数变量传入:

1
2
3
4
5
6
7
public class MoAttack{
private GeLi geli;

public MoAttack(GeLi geli){
this.geli = geli;
}
}

构造函数不需要关心谁去扮演该角色,但是在类被构造出来后,一定存在这样一个演员。

1
2
3
4
5
6
7
8
public class Director{
public void direct(){
GeLi geli = new LiuDeHua();
//注入具体的饰演者
MoAttack mo = new MoAttack(geli);
mo.cityGateAsk();
}
}

setter注入

在剧本执行过程当中,可以动态的注入相应的角色。

1
2
3
4
5
6
7
public class MoAttack{
private GeLi geli;

public setGeli(GeLi geli){
this.geli = geli;
}
}

接口注入

将调用类所有依赖注入的方法抽取到一个接口当中,调用类通过实现该接口提供相应的注入方法,通过接口注入必须先声明一个ActorArrangable。

这种方式下多出一个新的接口,增加一个新类,并不提倡。

1
2
3
public interface ActorArrangable{
void injectGeli(GeLi geli);
}

MoAttack实现接口提供具体的实现:

1
2
3
4
5
6
7
8
9
10
public class MoAttack implements ActorArrangable{
private GeLi geli;

public void injectGeli(GeLi geli){
this.geli = geli;
}
public void cityGateAsk(){
geli.responseAsk("墨者A");
}
}

客户端的实现:

1
2
3
4
5
6
7
8
public class Director{
public void direct(){
MoAttack mo = new MoAttack();
GeLi geli = new LiuDeHua();
mo.injectGeli(geli);
mo.cityGateAsk();
}
}

Spring实现

前面的实现只是将实例化工作移动到了Director类,而这些代码依然存在。而我们希望使用第三方代理机构来实现这一工作,将剧本、演员、导演解耦。

Spring中的IoC的实现原理就是工厂模式加反射机制。

  • 通过反射创造实例。
  • 获取需要注入的接口实现类并将其赋值给该接口。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
interface Fruit {
public abstract void eat();
}
class Apple implements Fruit {
public void eat(){
System.out.println("Apple");
}
}
class Orange implements Fruit {
public void eat(){
System.out.println("Orange");
}
}
class Factory {
public static Fruit getInstance(String ClassName) {
Fruit f=null;
try {
f=(Fruit)Class.forName(ClassName).newInstance();
} catch (Exception e) {
e.printStackTrace();
}
return f;
}
}
class Client {
public static void main(String[] a) {
Fruit f=Factory.getInstance("io.github.dunwu.spring.Apple");
if(f!=null){
f.eat();
}
}
}

IOC容器启动流程

refresh()

参见Spring:Bean生命周期

IOC底层原理

Spring中的IOC的实现原理就是工厂模式加反射机制。

IOC中最基本的技术就是“反射(Reflection)”编程,目前.Net C#、Java和PHP5等语言均支持,其中PHP5的技术书籍中,有时候也被翻译成“映射”。有关反射的概念和用法,大家应该都很清楚,通俗来讲就是根据给出的类名(字符串方式)来动态地生成对象。这种编程方式可以让对象在生成时才决定到底是哪一种对象。反射的应用是很广泛的,很多的成熟的框架,比如象Java中的Hibernate、Spring框架,.Net中 NHibernate、Spring.Net框架都是把“反射”做为最基本的技术手段。

概述

Bean工厂是Spring框架最核心的接口,提供了高级IOC的配置机制,使得管理不同类型的Java对象称为可能。

应用上下文ApplicationContext建立在BeanFactory基础上提供了更多面向应用的功能,提供了国际化支持和框架事件体系,更易于创建实际应用。

  • BeanFactory一般称为IOC容器。是Spring框架的基础设施,面向Spring本身。
  • ApplicationContext一般称为应用上下文。面向使用Spring框架开发者,几乎所有场景都可以使用ApplicationContext而不是BeanFactory。

IOC容器

BeanFactory

BeanFactory是类的通用工厂,可以创建并管理各种类的对象,Spring称这些被创建和管理的Java对象为Bean。

BeanFactory的类体系结构

BeanFactory由多种实现,最常用的是XmlBeanFactory,但在Spring当中已经废弃,建议使用XmlBeanDefinitionReader、DefaultListableBeanFactory代替。

1566483720864

可见BeanFactory位于结构树的顶层,其功能通过其他接口得到不断扩展。

子接口

  • ListableBeanFactory。该接口定义了访问容器中Bean基本信息的若干方法,如查看Bean的个数,获取某一类型Bean的配置名、查看容器中是否包含某一Bean等。
  • HierarchicalBeanFactory。父子级联Ioc容器的接口,子容器可以通过接口方法访问父容器。
  • ConfigurableBeanFactory。该接口增强了IOC容器的可定制性,定义了设置类装载器、属性编辑器、容器初始化后置处理器等方法。
  • AutowireCapableBeanFactory。定义了将容器中的Bean按某种规则(名字匹配,类型匹配)进行自动装配的方法。
  • SingletonBeanRegistry。定义了允许在运行期间向容器注册单例Bean的方法。
  • BeanDefinitionRegistry。Spring配置文件中的每个<bean>节点元素在Spring容器里都通过一个BeanDefinition对象标识,它描述了Bean的配置信息,而BeanDefinition Registry提供了向容器手工注册BeanDefinition对象的方法。

方法

  • getBean(String beanName)。

从容器中返回特定名称的Bean。

ApplicationContext

BeanFactory是Spring的心脏,ApplicationContext是完整的身体,其由BeanFactory派生而来,提供了更多面向实际应用的功能。

在BeanFactory当中很多功能需要以编程的方式实现,而在ApplicationContext当中可以通过配置的方式实现。

类体系结构

ApplicationContext的主要实现类是:

  • ClassPathXmlApplicationContext。默认从类路径加载配置文件。
  • FileSystemXmlApplicationContext。默认从文件系统中装载配置文件。

1566485398765

ConfigurableApplicationContext扩展于ApplicationContext,增加了refresh()与close()使得ApplicationContext具有启动、刷新和关闭应用上下文的能力,即可清除缓存并重新装载配置信息。

接口

ApplicationContext功能集成自HierarchicalBeanFactory、ListableBeanFactory,并在此基础上通过多个其他接口扩展了BeanFactory的功能,接口包括:

  • ApplicationEventPublisher。
    • 容容器具有发布应用上下文事件的功能,包括容器启动事件、关闭事件等。
    • 实现了ApplicationListener事件监听接口的Bean可以接收到容器事件并对事件进行响应处理。
    • 在ApplicationContext抽象实现类当中存在一个ApplicationEventMulticaster,它负责保存所有的监听器,以便在容器产生上下文事件时通知这些事件监听者。

让容器具有发布应用上下文事件的功能,包括容器启动事件、关闭事件等。实现了ApplicationListener事件监听接口的Bean可以

  • MessageSource。
    • 为应用提供国际化消息访问的功能。
  • ResourcePatternResolver。
    • 可以通过带前缀的Ant风格的资源文件路径装载Spring的配置文件。
  • LifeCycle。
    • 提供了start()stop()两个方法,主要用于控制异步处理过程。
    • 在具体使用时,该接口会同时被ApplicationContext实现以及具体的Bean实现,ApplicationContext将start/stop信息传递给容器中所有实现了该接口的Bean,以达到管理和控制JMX、任务调度等目的。

WebApplicationContext

专门为Web应用准备,允许从相对于Web根目录的路径中装载配置文件完成初始化工作。从WebApplicationContext中可以获得ServletContext中,以便Web应用环境可以访问Spring应用上下文。

Spring为此提供了一个工具类WebApplicationContextUtils,通过该类的getWebApplicationContext(ServletContext sc)可以从ServletContext中获取WebApplicationContext实例。

在非Web应用下,Bean只有Singleton和prototype两种作用域,在WebApplicationContext为Bean增添了新的作用域:request、session、global session。

1566523105743

为了Web容器的上下文应用与Spring的Web应用上下文互相访问,WebApplicationContext定义了一个常量ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE,在上下文启动时,WebApplication即以此为键放置在ServletContext的属性列表当中。通过以下语句从Web容器中获取WebApplicationContext:

1566523731977

初始化

初始化需要有ServletContext实例,即必须在用有Web容器的前提下才能完成启动工作。

父子容器

通过HierarchicalBeanFactory接口,Spring的IOC容器可以建立父子层级关联的容器体系,子容器可以访问父容器中的Bean,但芙蓉区不能访问子容器的Bean。

在容器内,Bean的id必须唯一,但子容器可以拥有一个与父容器id相同的Bean,该体系增强了Spring容器架构的扩展性与灵活性。

MVC当中,展现层的Bean位于子容器当中,而业务层与持久层Bean位于父容器中,这样展现层Bean可以引用业务层与持久层Bean,但反过来却无法看到。

IOC容器的实现

需要实现的两个关键技术, 以明确服务的对象是谁、需要为服务对象提供什么样的服务:

  • 对象的构建。
  • 对象的绑定。

实现方式:

  • 硬编码。
  • 配置文件。
  • 注解。

Spring通过一个配置文件来描述Bean以及Bean之间的依赖关系。利用Java语言的反射功能实例化Bean并建立Bean之间的依赖关系。

BeanFactory

参见Spring:Bean生命周期

ApplicationContext

ApplicationContext 容器建立BeanFactory之上,拥有BeanFactory的所有功能,但在实现上会有所差别。

我认为差别主要体现在两个方面:

  • bean的生成方式;
  • 扩展了BeanFactory的功能,提供了更多企业级功能的支持。
  • ApplicationContext采用的非懒加载方式。它会在启动阶段完成所有的初始化,并不会等到getBean()才执行
    • 所以,相对于BeanFactory来 说,ApplicationContext要求更多的系统资源,同时,因为在启动时就完成所有初始化,容 器启动时间较之BeanFactory也会长一些。在那些系统资源充足,并且要求更多功能的场景中, ApplicationContext类型的容器是比较合适的选择。

bean的加载方式

BeanFactory提供BeanReader来从配置文件中读取bean配置。相应的ApplicationContext也提供几个读取配置文件的方式:

  • FileSystemXmlApplicationContext:该容器从 XML 文件中加载已被定义的 bean。在这里,你需要提供给构造器 XML 文件的完整路径

  • ClassPathXmlApplicationContext:该容器从 XML 文件中加载已被定义的 bean。在这里,你不需要提供 XML 文件的完整路径,只需正确配置 CLASSPATH 环境变量即可,因为,容器会从 CLASSPATH 中搜索 bean 配置文件。

  • WebXmlApplicationContext:该容器会在一个 web 应用程序的范围内加载在 XML 文件中已被定义的 bean。

  • AnnotationConfigApplicationContext

  • ConfigurableWebApplicationContext

    img

    ApplicationContext 还额外增加了三个历能:

  • ApplicationEventPublisher

  • ResourceLoader

  • MessageResource

资源访问

ResourceLoader

ResourceLoader并不能将其看成是Spring独有的功能,spring Ioc只是借助于ResourceLoader来实现资源加载。也提供了各种各样的资源加载方式:

  • DefaultResourceLoader 首先检查资源路径是否以classpath:前缀打头,如果是,则尝试构造ClassPathResource类 型资源并返回。否则, 尝试通过URL,根据资源路径来定位资源

  • FileSystemResourceLoader 它继承自Default-ResourceLoader,但覆写了getResourceByPath(String)方法,使之从文件系统加载资源并以 FileSystemResource类型返回

    • ResourcePatternResolver 批量查找的ResourceLoader

      img

      spring与ResourceLoader之间的关系

      img

      所有ApplicationContext的具体实现类都会直接或者间接地实现AbstractApplicationContext,AbstactApplicationContext 依赖了了DeffaultResourceLoader, ApplicationContext 继承了ResourcePatternResolver,所到头来ApplicationContext的具体实现类都会具有DefaultResourceLoader 和 PathMatchingResourcePatterResolver的功能。这也就是会什么ApplicationContext可以实现统一资源定位。

ApplicationEventPublisher(在介绍spring事件的时候再详细讲)

  1. ApplicationEvent:继承自EventObject,同时是spring的application中事件的父类,需要被自定义的事件继承。
  2. ApplicationListener:继承自EventListener,spring的application中的监听器必须实现的接口,需要被自定义的监听器实现其onApplicationEvent方法
  3. ApplicationEventPublisherAware:在spring的context中希望能发布事件的类必须实现的接口,该接口中定义了设置ApplicationEventPublisher的方法,由ApplicationContext调用并设置。在自己实现的ApplicationEventPublisherAware子类中,需要有ApplicationEventPublisher属性的定义。
  4. ApplicationEventPublisher:spring的事件发布者接口,定义了发布事件的接口方法publishEvent。因为ApplicationContext实现了该接口,因此spring的ApplicationContext实例具有发布事件的功能(publishEvent方法在AbstractApplicationContext中有实现)。在使用的时候,只需要把ApplicationEventPublisher的引用定义到ApplicationEventPublisherAware的实现中,spring容器会完成对ApplicationEventPublisher的注入。

MessageSource

提供国际化支持,不讲了,有需要请转至:blog.sina.com.cn/s/blog_85d7…

# 四、最佳实践
注解扫描

1
2
3
4
5
6
7
8
9
10
11
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-2.5.xsd">
<context:component-scan base-package="org.spring21"/>


</beans>

component/service/controller注解

1
2
3
4
5
6
7
8
9
@Component
public class Person {
@Resource
private Food food;

public void setFood(Food food) {
this.food = food;
}
}

bean的前置后置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Component
public class Person {
@Resource
private Food food;

public setFood(Food food) {
this.food = food;
}

@PostConstruct
public void wash() {
System.out.println("饭前洗手");
}

@PreDestroy
public void brush() {
System.out.println("饭后刷牙");
}
}

注解加载

基于类注解的配置方式。

该功能来自于JavaConfig的子项目,已成为Spring核心框架一部分。标注@Configuration注解的POJO即可提供Spring需要的Bean配置信息。

IOC容器的初始化过程

Resource 定位:我们一般使用外部资源来描述 Bean 对象,所以 IOC 容器第一步就是需要定位 Resource 外部资源 。Resource 的定位其实就是 BeanDefinition 的资源定位,它是由 ResourceLoader 通过统一的 Resource 接口来完成的,这个 Resource 对各种形式的 BeanDefinition 的使用都提供了统一接口 。

载入:第二个过程就是 BeanDefinition 的载入 ,BeanDefinitionReader 读取 , 解析 Resource 定位的资源,也就是将用户定义好的 Bean 表示成 IOC 容器的内部数据结构也就是 BeanDefinition, 在 IOC 容器内部维护着一个 BeanDefinition Map 的数据结构,通过这样的数据结构, IOC 容器能够对 Bean 进行更好的管理 。 在配置文件中每一个都对应着一个 BeanDefinition 对象 。

注册:第三个过程则是注册,即向 IOC 容器注册这些 BeanDefinition ,这个过程是通过 BeanDefinitionRegistery 接口来实现的 。

设计思想

进阶

深度理解依赖注入(Dependence Injection)

依赖在哪里

老马举了一个小例子,是开发一个电影列举器(MovieList),这个电影列举器需要使用一个电影查找器(MovieFinder)提供的服务,伪码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/*服务的接口*/
public interface MovieFinder {
ArrayList findAll();
}

/*服务的消费者*/
class MovieLister
{
public Movie[] moviesDirectedBy(String arg) {
List allMovies = finder.findAll();
for (Iterator it = allMovies.iterator(); it.hasNext();) {
Movie movie = (Movie) it.next();
if (!movie.getDirector().equals(arg)) it.remove();
}
return (Movie[]) allMovies.toArray(new Movie[allMovies.size()]);
}


/*消费者内部包含一个将指向具体服务类型的实体对象*/


private MovieFinder finder;
/*消费者需要在某一个时刻去实例化具体的服务。这是我们要解耦的关键所在,
*因为这样的处理方式造成了服务消费者和服务提供者的强耦合关系(这种耦合是在编译期就确定下来的)。
**/
public MovieLister() {
finder = new ColonDelimitedMovieFinder("movies1.txt");
}
}

从上面代码的注释中可以看到,MovieLister和ColonDelimitedMovieFinder(这可以使任意一个实现了MovieFinder接口的类型)之间存在强耦合关系,如下图所示:

img

这使得MovieList很难作为一个成熟的组件去发布,因为在不同的应用环境中(包括同一套软件系统被不同用户使用的时候),它所要依赖的电影查找器可能是千差万别的。所以,为了能实现真正的基于组件的开发,必须有一种机制能同时满足下面两个要求:

(1)解除MovieList对具体MoveFinder类型的强依赖(编译期依赖)。

(2)在运行的时候为MovieList提供正确的MovieFinder类型的实例。

换句话说,就是在运行的时候才产生MovieList和MovieFinder之间的依赖关系(把这种依赖关系在一个合适的时候“注入”运行时),这恐怕就是Dependency Injection这个术语的由来。再换句话说,我们提到过解除强依赖,这并不是说MovieList和MovieFinder之间的依赖关系不存在了,事实上MovieList无论如何也需要某类MovieFinder提供的服务,我们只是把这种依赖的建立时间推后了,从编译器推迟到运行时了。

依赖关系在OO程序中是广泛存在的,只要A类型中用到了B类型实例,A就依赖于B。前面笔者谈到的内容是把概念抽象到了服务使用者和服务提供者的角度,这也符合现在SOA的设计思路。从另一种抽象方式上来看,可以把MovieList看成我们要构建的主系统,而MovieFinder是系统中的plugin,主系统并不强依赖于任何一个插件,但一旦插件被加载,主系统就应该可以准确调用适当插件的功能。

其实不管是面向服务的编程模式,还是基于插件的框架式编程,为了实现松耦合(服务调用者和提供者之间的or框架和插件之间的),都需要在必要的位置实现面向接口编程,在此基础之上,还应该有一种方便的机制实现具体类型之间的运行时绑定,这就是DI所要解决的问题。

DI的实现方式

和上面的图1对应的是,如果我们的系统实现了依赖注入,组件间的依赖关系就变成了图2:

img

说白了,就是要提供一个容器,由容器来完成(1)具体ServiceProvider的创建(2)ServiceUser和ServiceProvider的运行时绑定。下面我们就依次来看一下三种典型的依赖注入方式的实现。特别要说明的是,要理解依赖注入的机制,关键是理解容器的实现方式。本文后面给出的容器参考实现,均为黄忠成老师的代码,笔者仅在其中加上了一些关键注释而已。

Constructor Injection(构造器注入)

我们可以看到,在整个依赖注入的数据结构中,涉及到的重要的类型就是ServiceUser, ServiceProvider和Assembler三者,而这里所说的构造器,指的是ServiceUser的构造器。也就是说,在构造ServiceUser实例的时候,才把真正的ServiceProvider传给他:

1
2
3
4
5
6
7
8
class MovieLister
{
//其他内容,省略
public MovieLister(MovieFinder finder)
{
this.finder = finder;
}
}

接下来我们看看Assembler应该如何构建:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private MutablePicoContainer configureContainer() {
MutablePicoContainer pico = new DefaultPicoContainer();


//下面就是把ServiceProvider和ServiceUser都放入容器的过程,以后就由容器来提供ServiceUser的已完成依赖注入实例,
//其中用到的实例参数和类型参数一般是从配置档中读取的,这里是个简单的写法。
//所有的依赖注入方法都会有类似的容器初始化过程,本文在后面的小节中就不再重复这一段代码了。


Parameter[] finderParams = {new ConstantParameter("movies1.txt")};
pico.registerComponentImplementation(MovieFinder.class, ColonMovieFinder.class, finderParams);
pico.registerComponentImplementation(MovieLister.class);
//至此,容器里面装入了两个类型,其中没给出构造参数的那一个(MovieLister)将依靠其在构造器中定义的传入参数类型,在容器中
//进行查找,找到一个类型匹配项即可进行构造初始化。
return pico;
}

需要在强调一下的是,依赖并未消失,只是延后到了容器被构建的时刻。所以正如图2中您已经看到的,容器本身(更准确的说,是一个容器运行实例的构建过程)对ServiceUser和ServiceProvoder都是存在依赖关系的。所以,在这样的体系结构里,ServiceUser、ServiceProvider和容器都是稳定的,互相之间也没有任何依赖关系;所有的依赖关系、所有的变化都被封装进了容器实例的创建过程里,符合我们对服务应用的理解。而且,在实际开发中我们一般会采用配置文件来辅助容器实例的创建,将这种变化性排斥到编译期之外。

即使还没给出后面的代码,你也一定猜得到,这个container类一定有一个GetInstance(Type t)这样的方法,这个方法会为我们返回一个已经注入完毕的MovieLister。 一个简单的应用如下:

1
2
3
4
5
6
7
public void testWithPico() 
{
MutablePicoContainer pico = configureContainer();
MovieLister lister = (MovieLister) pico.getComponentInstance(MovieLister.class);
Movie[] movies = lister.moviesDirectedBy("Sergio Leone");
assertEquals("Once Upon a Time in the West", movies[0].getTitle());
}

上面最关键的就是对pico.getComponentInstance的调用。Assembler会在这个时候调用MovieLister的构造器,构造器的参数就是当时通过pico.registerComponentImplementation(MovieFinder.class, ColonMovieFinder.class, finderParams)设置进去的实际的ServiceProvider–ColonMovieFinder。下面请看这个容器的参考代码:

Setter Injection(设值注入)

这种注入方式和构造注入实在很类似,唯一的区别就是前者在构造函数的调用过程中进行注入,而它是通过给属性赋值来进行注入。无怪乎PicoContainer和Spring都是同时支持这两种注入方式。Spring对通过XML进行配置有比较好的支持,也使得Spring中更常使用设值注入的方式:

1
2
3
4
5
6
7
8
9
10
11
12
 <beans>
     <bean id="MovieLister" class="spring.MovieLister">
         <property name="finder">
             <ref local="MovieFinder"/>
         </property>
     </bean>
     <bean id="MovieFinder" class="spring.ColonMovieFinder">
         <property name="filename">
             <value>movies1.txt</value>
        </property>
    </bean>
</beans>

下面也给出支持设值注入的容器参考实现,大家可以和构造器注入的容器对照起来看,里面的差别很小,主要的差别就在于,在获取对象实例(GetInstance)的时候,前者是通过反射得到待创建类型的构造器信息,然后根据构造器传入参数的类型在容器中进行查找,并构造出合适的实例;而后者是通过反射得到待创建类型的所有属性,然后根据属性的类型在容器中查找相应类型的实例。

Interface Injection (接口注入)

这是笔者认为最不够优雅的一种依赖注入方式。要实现接口注入,首先ServiceProvider要给出一个接口定义:

1
2
3
public interface InjectFinder {
void injectFinder(MovieFinder finder);
}

接下来,ServiceUser必须实现这个接口:

1
2
3
4
5
6
class MovieLister: InjectFinder
{
public void injectFinder(MovieFinder finder) {
this.finder = finder;
}
}

容器所要做的,就是根据接口定义调用其中的inject方法完成注入过程,这里就不在赘述了,总的原理和上面两种依赖注入模式没有太多区别。

除了DI,还有Service Locator

上面提到的依赖注入只是消除ServiceUser和ServiceProvider之间的依赖关系的一种方法,还有另一种方法:服务定位器(Service Locator)。也就是说,由ServiceLocator来专门负责提供具体的ServiceProvider。当然,这样的话ServiceUser不仅要依赖于服务的接口,还依赖于ServiceContract。仍然是最早提到过的电影列举器的例子,如果使用Service Locator来解除依赖的话,整个依赖关系应当如下图所示:
img

用起来也很简单,在一个适当的位置(比如在一组相关服务即将被调用之前)对ServiceLocator进行初始化,用到的时候就直接用ServiceLocator返回ServiceProvider实例:

1
2
3
4
5
6
7
8
9
//服务定位器的初始化
ServiceLocator locator = new ServiceLocator();
locator.loadService("MovieFinder", new ColonMovieFinder("movies1.txt"));
ServiceLocator.load(locator);

//服务定义器的使用
//其实这个使用方式体现了服务定位器和依赖注入模式的最大差别:ServiceUser需要显示的调用ServiceLocator,从而获取自己需要的服务对象;
//而依赖注入则是隐式的由容器完成了这一切。
MovieFinder finder = (MovieFinder) ServiceLocator.getService("MovieFinder");

正因为上面提到过的ServiceUser对ServiceLocator的依赖性,从提高模块的独立性(比如说,你可能把你构造的ServiceUser或者ServiceProvider给第三方使用)上来说,依赖注入可能更好一些,这恐怕也是为什么大多数的IOC框架都选用了DI的原因。ServiceLocator最大的优点可能在于实现起来非常简单,如果您开发的应用没有复杂到需要采用一个IOC框架的程度,也许您可以试着采用它。

3.广义的服务
文中很多地方提到服务使用者(ServiceUser)和服务提供者(ServiceProvider)的概念,这里的“服务”是一种非常广义的概念,在语法层面就是指最普通的依赖关系(类型A中有一个B类型的变量,则A依赖于B)。如果您把服务理解为WCF或者Web Service中的那种服务概念,您会发现上面所说的所有技术手段都是没有意义的。以WCF而论,其客户端和服务器端本就是依赖于Contract的松耦合关系,其实这也从另一个角度说明了SOA应用的优势所在。

IOC扩展

Spring IOC还提供了Bean实例缓存、生命周期管理、Bean实例代理、事件发布、资源装载等高级服务。

反省总结

参考

  1. 深度理解依赖注入(Dependence Injection)
  2. 浅谈IOC–说清楚IOC是什么
  3. JAVA关于Spring 面试题汇总
  4. IoC-spring 的灵魂(带你轻松理解IOC思想及bean对象的生成过程)
  5. Spring 源码浅析——容器刷新流程概览