6.3.3 Dependency Injection
外部程序把服务对象通过某种方式注入到客户对象供其使用的方法称之为依赖注入。
根据注入方式的不同,在这里把依赖注入分为6类:
Setter注入
Constructor注入
Annotation注入
Interface注入
Parameter注入
其他形式的注入
目前实现依赖注入的框架有很多,下面我们就结合现在非常流行的一些轻量级容器/框架来作介绍。
1.Setter注入
Spring框架是时下被广泛使用的J2EE开源框架,它不只是一款轻量级的依赖注射容器,还包括持久化框架、事务管理框架、Web应用框架(MVC框架)、AOP等框架。它实现了Setter注入等多种注入方式,这里以Spring为例来介绍Setter方式的注入。Setter注入是指外部程序通过调用setting方法为客户对象注入所依赖的对象。
为了给Client注入所依赖的Service1服务对象,我们为该属性提供了一个setting方法,代码如下。
以同样方式为Service1Impl类的依赖对象service2提供一个setting方法,由于非常简单,代码就不再赘述。
接下来需要在Spring配置文件里配置它们之间的依赖关系,如下所示。
配置文件里,节点<bean id="client"class="pattern.part2.chapter6.setter.Client">……</bean>定义了一个名字为"client"的单例服务对象,其实现类是pattern.part2.chapter6.setter.Client,子节点<property name="service1"ref="service1"/>表示对象client的属性service1含有一个setting方法,并且依赖指向了名字为"service1"的对象。同样在"service1"服务对象的配置里,属性service2的依赖指向"service2"对象。
这样,我们就完成了它们之间的依赖关系,为了测试注入效果,我们编写的代码大致如下。
ApplicationContext ctx=new ClassPathXmlApplicationContext(……)加载配置文件,并根据XML配置文件定义好的依赖关系初始化所有实例对象,这样只要我们从Spring容器得到的对象就是通过setting方法装配好依赖的对象,为了演示client对象的doSomething()方法调用,我们首先得到"client"对象,即Client client=(Client)ctx.getBean("client")。
看看执行效果如何,结果如下所示。
Service1 is doing something.
Service2 is doing something.
由于使用了setting方法注入依赖,在单元测试时,我们也非常方便地使用其注入Mock对象,于是Client单元测试代码如下。
在单元测试时,我们手动调用setting方法注入依赖对象,Spring框架是使用反射机制调用setting方法注入依赖的对象,它们之间没有什么本质区别。
因为我们的Mock对象并未替换容器里的对象,所以在单元测试结束之后不必要撰写清理逻辑。
2.Constructor注入
Constructor注入,顾名思义,就是通过带参数的构造方法注入依赖对象。Pico Container是一款非常流行的轻量级DI容器,我们以其为例进行说明。为了给Client对象注入Service1的对象,我们为其创建一个带有参数的构造方法,代码如下所示。
同样我们也为Service1Impl类实现一个带参数的构造函数,如下所示。
Pico Container可以根据构造函数参数的类型,在注册的服务类里查找相应的实现类,这样我们就可以省去一些简单的依赖配置了。
我这里写了一个配置方法来注册服务类,如下所示。
由于Pico Container没有提供像Spring框架通过配置文件定义这些组件类和依赖关系的功能,需要我们手工实现,在实际使用中,大家可以根据各自喜好选择编程配置,也可以写个程序从配置文件读取这些定义。
我们在使用这个容器之前,调用这个配置方法,测试代码如下。
由于Pico Container支持泛型,这里也不需要做强制类型转换就能得到Client对象,得到Client对象的这句代码是:
Client client=pico. getComponent(Client.class)
测试运行结果如下。
Service1 is doing something.
Service2 is doing something.
同样,在单元测试时,我们根据带参数的构造函数,手工注入Service1的Mock对象即可,在测试完成之后,我们也不需要做额外的清理工作,代码如下所示。
3.Annotation注入
在Java5以上版本做过开发的人都应该了解注解(Annotation)[1]。我们知道,Annotation可以注解各种类型,例如属性、类型、方法、本地变量、构造方法等。只要这些Annotation是被@Retention(RUNTIME)注解的,那么,此类型的Annotation注解信息就可以在运行时通过反射读取。自从Java引入Annotation, Java世界就变得更加热闹了。
如果我们把实例化信息和对象之间的依赖关系信息使用Annotation注解,那么,只要在运行时得到它们,就知道如何初始化服务对象了。Guice是Google旗下的一款非常出色的轻量级DI容器,由于使用了Annotation实现注入依赖,加之其优良的性能,得到广大编程爱好者的一致好评,这里我们就以其为例来作说明。
Guice使用@com.google.inject.ImplementedBy指定此类型的具体实现,例如接口Service1实现类是Service1Impl,那么注解的代码就如下所示。
同样,我们可以注解接口Service2,这里不再赘述。
Guice提供Annotation@com.google.inject.Inject表示在实例化该类型的对象时,需要为被注解目标注入依赖,此Annotation可以注解方法,构造函数和属性3种类型。
我们使用它分别注解Service1Impl类的setting方法和Client类的构造函数,代码如下所示。
由于依赖关系比较简单,不需要额外的配置。现在只要从Injector查找我们需要的对象,它就会根据此类型的定义读取注解,实例化我们想要的对象。测试使用Annotation注入的代码如下。
注意:同样由于Guice支持泛型(Generics),我们在使用Injector的getInstance(……)方法时不需要进行强制类型转化。
由于使用@com.google.inject.Inject注解了Client构造函数,我们照样可以使用该构造函数注入我们的Mock对象进行单元测试,代码如下所示。
另外,还需要提醒的是,如果对私有属性进行注解,虽然实现了更加精准的注入,但是会给单元测试造成一定的麻烦,因为私有属性在其他类里是不可见的,所以不能通过直接赋值的方式注入Mock对象(框架使用反射机制,在给私有属性赋值之前,调用field.setAccessible(true),除非你也是用反射初始化你的类),建议大家尽量避免使用。
4.Interface注入
客户程序通过实现容器/框架所规范的某些特殊接口,在为客户对象返回这些依赖的对象之前,容器回调这些接口的方法,注入所依赖的服务对象。
例如很久以前Apache的Avalon框架就使用这种方式注入。现在,Struts2也使用了这种方式注入一些依赖的对象,比如,如果你的Action实现了接口ServletRequestAware、SessionAware等,只要Action类实现这些接口,Struts框架就会回调这些接口的相应方法,注入HttpServletRequest、HttpSession对象。这里给出一个简单的示例,演示如何实现接口注入。
首先我们定义一个ServiceAware接口,如下所示。
Service接口及其实现类的代码如下。
只要服务类实现这个接口,容器就会注入Service对象,我们定义一个Client类实现该接口,代码如下。
我们下一步来创建这个简单的接口注入容器,代码如下所示。
我们使用根据Class类型查找服务对象的方式,第一次查找时采用反射机制,使用默认的构造函数生成对象。代码的核心部分是这句:
if(service instanceof ServiceAware){……}.
通过接口装配依赖的对象:如果对象是ServiceAware的实例,则调用此接口的方法注入ServiceAware对象,即这句:((ServiceAware)service).injectService(new ServiceImpl()),为每个ServiceAware实例注入ServiceImpl对象。
现在,我们看看如何使用,代码如下。
执行效果如下:
Service is doing something……
我们根据接口方法,也可以轻松地手工注入Service的Mock对象完成对Client类的单元测试,同样未影响容器里的对象,在测试结束时也不必做额外的清理操作,代码如下所示。
这种方式不够灵活,容器必须预先定义一些接口实现注入,适合实现少数特定类型的对象注入。
5.Parameter注入
这种方式下,外部程序可以通过函数参数,给客户程序注入所依赖的服务对象,非常简单。Client类的代码如下所示。
方法doSomething(Service service)采用外部传递Service对象方式,至于Service如何是实例化的,它一无所知。
如何使用呢?其实非常简单,代码如下。
外部程序实例化一个Service实例,传递给Client的doSomething(Service service)方法使用。
单元测试代码非常简单,和使用一样,在测试调用方法时传入的是Mock对象,测试结束时当然也不需要做额外的清理操作,代码如下所示。
注意:这种形式的注入比较特殊,Client类的doSomething(Service service)方法不光完成了依赖的装配,而且执行了Service回调的方法execute(),完成了其他逻辑。
如果是一个提供Parameter注入的框架程序,在没有特殊需求的情况下,在实例化的过程中,应该使用Parameter注入完成服务对象的装配逻辑,这里如此举例的目的只是为了简要说明这种方式。
其实,可以看出,Setter注入是一种特殊的参数注入,它规定只能使用setting方法注入依赖,同样,Constructor注入也是一种特殊的参数注入,只能对构造函数实现参数注入。
一些流行DI框架在使用参数注入实例化对象时,往往结合Annotation注入。Interface注入等方式一起使用,但是明显的一条,这些实现方法不应包含除依赖装配之外的其他逻辑。
6.其他形式的注入
很多框架还有其他特点的注入,这些注入形式往往和框架有关。例如,Pico Container和Guice使用Providers的注入方式,Spring框架的lookup方法注入方式,这些都是这些容器/框架所提供的一些高级用法,主要用来解决一些特殊问题,读者以后有机会可以慢慢实践,这里不再赘述。
[1] Java 5及以上版本支持的特性,Annotation能够修饰方法、类、参数、变量、构造方法、包和类型(类、接口、枚举等)的声明。可以在编译、加载、运行过程中读取这些有用信息,根据这些信息执行相应的操作。具体使用请参见附录A推荐的Java相关书籍。