首页 » Java程序员修炼之道 » Java程序员修炼之道全文在线阅读

《Java程序员修炼之道》3.2 Java中标准化的DI

关灯直达底部

从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容器会注入其参数HeaderContent对象。

@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实际操练一把了。