从2004年开始,有几个用于依赖注入的IoC容器得到了广泛的应用(仅以Spring、Guice和PicoContainer为例)。曾几何时,它们在DI的配置方式上仍然各自为政,这使开发人员很难在不同框架之间迁移。
这一问题直到2009年5月才出现转机,DI社区的两大巨头Bob Lee(Guice)和Rod Johnson(SpringSource)宣布要齐心协力,共同打造一组标准的接口注解1。他们紧接着提出了JSR-330(javax.inject
)规范请求,倡导Java SE的DI标准化。DI社区的各路诸侯纷纷响应,全部表示支持。
1 Bob Lee,“Announcing @javax.inject.Inject”(2009-05-08),www.theserverside.com/news/thread.tss?thread_id=54499。
Java EE中的DI标准化情况如何?
Java企业应用从JEE 6开始构建了自己的依赖注入体系(即CDI),由JSR-299(Java EE平台中的上下文及依赖注入)规范确定,你可在http://jcp.org/中搜索JSR-299了解其详细信息。简言之,JSR-299构建在JSR-330基础之上,旨在为企业应用提供标准化的配置。2
2 JSR-299(Java Contexts and Dependency Injection)目前由Redhat的Gavin King(Hibernate的创建者)主导,因为它比较新,所以设计理念上解决了以前DI框架中的一些问题,而且也不是非得在Java EE容器上才能使用,在Servlet容器上也可以使用。其参考实现为weld,详情请参见官网:http://www.seamframework.org/Weld。——译者注
自从javax.inject
出现在Java中(Java SE 5、6和7都支持)以来,代码中就可以使用标准的依赖注入了,也可以在不同的DI框架中进行迁移。比如,你原来在轻量级的Guice框架中运行的代码,为了利用其丰富的特性,也可以迁移到Spring框架中去。
警告 实际上,代码迁移并不容易。一旦你的代码用到了仅由特定DI框架支持的特性,就不太可能摆脱这一框架了。尽管
javax.inject
包提供了常用DI功能的子集,但是你可能需要使用更高级的DI特性。正如你想象的那样,对于哪些特性应该作为通用的标准也是众说纷纭,很难统一。虽然现状不尽如人意,但Java毕竟朝DI框架的标准化方向迈出了一步。
为了理解最新的DI框架如何应用新标准,我们需要对javax.inject
包进行一番研究。记住,javax.inject
包只是提供了一个接口和几个注解类型,这些都会被遵循JSR-330标准的各种DI框架实现。也就是说,除非你在创建与JSR-330兼容的IoC容器(如果如此,向你致敬),通常不用自己实现它们。
我为什么要知道这东西怎么工作?
优秀的Java开发人员不能满足于只作为类库和框架的使用者,还要明白其内部的基本工作原理。在DI领域,不理解其原理可能会面临各种难缠的问题,比如依赖项配置错误、依赖项诡异地超出作用域、依赖项在不该共享时被共享以及分步调试离奇宕机等。
javax.inject
的文档对这个包的目的做出了精彩的解释,所以我们全盘照搬过来了:
javax.inject
包 3这个包指明了获取对象的一种方式,与传统的构造方法、工厂模式和服务定位器模式(比如JNDI)等相比,这种方式的可重用性、可测试性和可维护性都得到了极大提升。这种方式称为依赖注入,对于大多数非小型应用程序都很有帮助。
3 http://atinject.googlecode.com/svn/trunk/javadoc/javax/inject/package-summary.html 。
javax.inject
包里包括一个Provider<T>
接口和5个注解类型(@Inject
、@Qualifier
、@Named
、@Scope
和@Singleton
),后续章节中会逐一对它们进行介绍。先从@Inject
开始。
3.2.1 @Inject注解
@Inject
注解可以出现在三种类成员之前,表示该成员需要注入依赖项。按运行时的处理顺序,这三种成员类型是:1. 构造器2. 方法3. 属性
在构造器上使用@Inject
时,其参数在运行时由配置好的IoC容器提供。比如在下面的代码中,运行时调用MurmurMessage
类的构造器时,IoC容器会注入其参数Header
和Content
对象。
@Inject public MurmurMessage(Header header, Content content){ this.header = header; this.content = content;}
规范中规定向构造器注入的参数数量为0或多个,所以在不含参数的构造器上使用@Inject
也是合法的。
警告 因为JRE无法决定构造器注入的优先级,所以规范中规定类中只能有一个构造器带
@Inject
注解。
也可以用@Inject
注解方法,与构造器一样,运行时可注入参数的数量也可以是0或多个。但使用参数注入的方法不能声明为抽象方法,也不能声明其自身的类型参数1。下面这段代码在set方法前使用@Inject
,这是注入可选属性的常用技术。
1 即不能使用Oracle网站上的Java教程(http://download.oracle.com/javase/tutorial/extra/generics/methods.html)中所讲的泛型方法技巧。
@Inject public void setContent(Content content){ this.content = content;}
向方法中注入参数的技术对于服务类方法来说非常有用,其所需的资源可以作为参数注入,比如向查询数据的服务方法中注入数据访问对象(DAO)。
提示 向构造器中注入的通常是类中必需的依赖项,而对于非必需的依赖项,通常是在set方法上注入。比如已经给出了默认值的属性就是非必需的依赖项。这一最佳实践已经成了惯例。
也可以直接在属性上注入(只要它们不是final
),虽然这样做简单直接,但你最好不要用。因为这样可能会让单元测试更加困难。直接注入的语法也非常简单。
public class MurmurMessenger{ @Inject private MurmurMessage murmurMessage; ...}
你可以在Javadoc中了解更多关于@Inject
注解的详细内容,可以在其中找到哪些类型的值可以注入以及如何处理依赖循环。
对于@Inject
,现在你应该不再感到陌生了。接下来就该了解如何限定(进一步标识)那些注入到你的代码中的对象了。
3.2.2 @Qualifier注解
支持JSR-330规范的框架要用注解@Qualifier
限定(标识)要注入的对象。比如在IoC容器中有两个类型相同的对象,当把它们注入到你的代码中时,肯定要把它们区别开。图3-1解释了这一概念。
图3-1 用@Qualifier注解区分同一类型MusicGenre的不同bean
如果你用过由框架实现的限定符,应该知道要创建一个@Qualifier
实现必须遵循如下规则。
必须标记为
@Qualifier
和@Retention(RUNTIME)
,以确保该限定注解在运行时一直有效。通常还应该加上
@Documented
注解,这样该实现就能加到API的公共Javadoc中了。可以有属性。
@Target
注解可以限定其使用范围;比如将其使用范围限制为属性,而不是限定为属性的默认值和方法中的参数。
为了让你对上面的规则有直观的感受,下面给出一个@Qualifier
实现。某音乐库框架中的IoC容器提供了一个限定符@MusicGenre
,开发人员在创建MetalRecordAlbumns
类时可以使用该限定符,以确保注入了正确的Genre
。
@Documented@Retention(RUNTIME)@Qualifierpublic @interface MusicGenre{ Genre genre default Genre.TRANCE; public enum GENRE { CLASSICAL, METAL, ROCK, TRANCE }}public class MetalRecordAlbumns{ @Inject @MusicGenre(GENRE.METAL) Genre genre;}
Java开发人员一般不需要自己创建@Qualifier
注解,但要对各种IoC容器如何实现限定有个基本的了解。
JSR-330规范中要求所有IoC容器都要提供一个默认的@Qualifier
注解:@Named
。
3.2.3 @Named注解
@Named
是一个特别的@Qualifier
注解,借助@Named
可以用名字标明要注入的对象。将@Named
和@Inject
一起使用,符合指定名称并且类型正确的对象会被注入。
在下面这个例子中,注入了名称为“murmur”和“broadcast”的两个MurmurMessage
对象。
public class MurmurMessenger{ @Inject @Named(“murmur”) private MurmurMessage murmurMessage; @Inject @Named(“broadcast”) private MurmurMessage broadcastMessage; ...}
尽管还有其他比较常用的限定注解,但最终只有@Named
被选作JSR-330的标准限定注解,所有DI框架都要实现。
发起规范的各方支持者还在另外一个问题上达成了一致——同意用标准化接口来确定注入对象的生命周期。
3.2.4 @Scope注解
@Scope
注解用于定义注入器(即IoC容器)对注入对象的重用方式。JSR-330规范中明确了如下几种默认行为。
如果没有声明任何
@Scope
注解接口的实现,注入器应该创建注入对象并且仅使用该对象一次。如果声明了
@Scope
注解接口的实现,那么注入对象的生命周期由所声明的@Scope
注解实现决定。如果注入对象在
@Scope
实现中要由多个线程使用,则需要保证注入对象的线程安全性。关于线程及线程安全的更多细节,请参阅第4章。如果某个类上声明了多个
@Scope
注解,或声明了不受支持的@Scope
注解,IoC容器应该抛出异常。
DI框架管理注入对象的生命周期时不会超出这些默认行为划定的界限。有些IoC容器支持自己特有的@Scope
,尤其是在Web前端领域,至少在JSR-299被广泛应用之前是这样。因为大家公认的通用@Scope
实现只有@Singleton
一个,所以JSR-330规范中仅确定了它这么一个标准的生命周期注解。
3.2.5 @Singleton注解
@Singleton
注解接口在DI框架中应用广泛。在需要注入一个不会改变的对象时,就要用@Singleton
。
单例模式
单例设计模式是为了确保类仅被实例化一次,详情参见由Erich Gamma, Richard Helm, Ralph Johnson, 和John Vlissides合著的《设计模式:可复用面向对象软件的基础》(Addison-Wesley Professional, 1994)第127页。请谨慎使用单例模式,因为它有时候会变成反模式。
大多数DI框架都将@Singleton
作为注入对象的默认生命周期,无需显式声明。也就是说如果没有明确指定注入对象的生命周期,框架就会认为你想注入一个单例对象。如果你想显式声明它为单例对象,可以用下面这种方式:
public MurmurMessage{ @Inject @Singleton MessageHeader defaultHeader;}
在这个例子中,我们假定defaultHeader
从来不会改变(切实有效的静态数值),所以它可以作为单例对象注入。
最后,我们来讨论一下当你觉得标准注解无法满足你的需求时该怎么办。
3.2.6 接口Provider<T>
如果你想对由DI框架注入代码中的对象拥有更多的控制权,可以要求DI框架将Provider<T>
接口实现注入对象1(T)
。控制对象的好处在于:
- 可以获取该对象的多个实例。
- 可以延迟获取该对象(延迟加载)。
- 可以打破循环依赖。
- 可以定义作用域,能在比整个被加载的应用小的作用域中查找对象。
1 原文如此,应为该类,后面还有几处笔误,请注意。——译者注
该接口仅有一个T get
方法,这个方法会返回一个构造好的注入对象(T)
。例如,在MurmurMessage
需要依赖项Message
对象时,可以向其构造方法中注入对应的Provider<T>
接口实现的实例(Provider<Message>
)。根据限定条件的不同,得到的Message
对象也会不同。请看下面的代码。
代码清单3-6 使用接口Provider<T>
import com.google.inject.Inject;import com.google.inject.Provider;class MurmurMessage{ @Inject MurmurMessage(Provider<Message> messageProvider) { Message msg1 = messageProvider.get;//得到一个Message if (someGlobalCondition) { Message copyOfMsg1 = messageProvider.get;//①得到Message的复本 } ... }}
注意上面的代码中是如何从Provider<Message>
中获取第二个Message
对象的。如果直接注入Message
,就无法获取另外一个Message
实例。在这个例子中,第二个注入对象仅在需要时才会加载①。
我们已经对新javax.inject
包背后的理论做了介绍,还给出了一些例子进行讲解。现在就该挽起袖子,用成熟的DI框架Guice实际操练一把了。