原文:Dependency Injection in Java EE 6 – Part 1

作者:Reza Rahman


 

这一文章系列介绍了Java EE的上下文和依赖注入(Contexts and Dependency Injection for Java EE,CDI),CDI是即将完成的Java EE 6平台的关键组成部分,经由JSR 299进行标准化。CDI是Java EE整个下一代类型安全的依赖注入的事实上的API。JSR 299由Gavin King领导,其目标是综合来自诸如Seam、Guice和Spring一类的解决方案的最好的依赖注入功能,同时加入许多自己的有用创新。

本文是文章系列的第一篇,我们打算从一个高层面来研究CDI,看看它是如何与Java EE整体相配合的,并讨论基础的依赖管理及作用域。在这一系列文章的介绍过程中,我们会涉及组件命名、版型(stereotype)、生产者(producer)、处置者(disposer)、装饰器(decorator)、拦截器(interceptor)、事件、用于可移植扩展的CDI API等,以及更多诸如此类的功能。我们还将讨论CDI如何与Seam、Spring以及Guice保持一致,并通过一些使用CanDI的实现细节来补充这一讨论。CanDI是Caucho对JSR299的独立实现,被收入到Resin应用服务器中。

 

快速回顾

 

Java EE 5的主要关注点是凭借POJO编程、注解和约定高于配置(convention-over-configuration)等方面来使得自身易于使用,Java EE 5中确实有依赖注入的基本形式,也许对其最恰当的术语称谓是资源注入(resource injection。具体来说,你可以借助@Resource、@PersistenceContext、@PersistenceUnit和@EJB等注解把JMS连接工厂、数据源、队列、JPA实体管理器、实体管理器工厂和EJB一类的容器资源注入到Servlet、JSF后台bean(JSF backing bean)和其他的EJB中。这种模型适用于包含了被写成EJB和JSF的JPA领域对象、服务和DAO的应用。

不过,为了一些用例,你不得不求助于像Seam、Spring或Guice一类的更通用的依赖注入技术,例如,你不能把EJB注入到Struts Action或者JUnit测试中,以及不能注入因为不需要事务而没有被编写成EJB的DAO或者助手(helper)类中;另外,很难整合第三方/内部的API或者是使用Java EE 5作为构建这种并非仅仅是严格的业务组件的API的基础。而这些正好是CDI旨在解决的那一类问题,CDI以一种很好地吻合了Java理念的高度类型安全、稳定及可移植的方式来解决这些问题。实际上,Resin容器本身的许多部分都是利用JSR 299这一对Java EE 5来说是难以想象的功能标准来编写的!

如果你很熟悉Spring IoC的话,那么CDI给人的感觉可能是更加类型安全、更未来派的和更加的注释驱动;Seam开发人员则会发现CDI具有更多先进的功能;与Guice比起来,CDI可能更加适合于企业级的开发。最后一点, Java EE 5开发人员一开始可能会觉得CDI更复杂,不过会发现绝大多数的复杂性都是有充分理由的,而且在不需要时可以忽略掉。

严格地说,除了依赖注入的中心功能之外,CDI从两个更重要的方面增强了Java EE的编程模型——两方面都来自Seam。首先,其允许直接使用EJB作为JSF后台bean,这一次我们不会涉及这一主题,不过在这一系列接下来的文章中我们会研究这一做法如何能够从表现层中除去一些常见的粘合代码,并大大增强了Java EE API的凝聚力。其次,CDI允许以一种更声明化的方式来管理对象的作用域、状态、生命周期和上下文,而不是以大多数面向web框架处理请求、会话及应用范围内的管理对象的编程方式,我们会在这一系列接下来的文章的展示CDI与JSF交互的行为过程中看到这一点。

 


名称的含义

    JSR 299的最后这两个方面是其原来的范围,这就是它为什么最初被称作“WebBeans”的缘故。正如其结果证明,目前形式的JSR 299,其包含的远远超出了这个最初的、涵盖了一般意义上的整合和依赖注入关注的范围。这种范围的逐渐变化反映在其目前的、更适合的名称上:Java EE的上下文和依赖注入(Contexts and Dependency Injection for Java EE),CDI中的这一“范围蔓延”一直是个存在争议的话题,但在事物发展的过程中,这可能也算不了什么大的事情……


 

零件是如何被拼装在一起的

 

重要的是要了解CDI在哪些方面与Java EE进行了整体的配合,以便正确地把握它的作用以及它是如何实现这一作用的。CDI没有自己的组件模型,它实际上是一组被诸如受管bean、Servlet以及EJB一类的Java EE组件消费的服务。

受管bean(managed bean是Java EE 6引入的一个关键概念,用以解决一些我们在前一节中讨论到的Java EE 5风格的资源注入的局限。受管bean仅仅是Java EE环境中一个极简单的Java对象,不同于Java对象的语义,它有明确定义的创建/销毁生命周期,因此可以借助@PostConstruct和@PreDestroy注解来获取生命周期的回调方法。受管bean可通过@ManagedBean注解来显式地进行标记,但这并不总是需要的,特别是对CDI来说。从CDI的角度来看,这意味几乎任何的Java对象都可以当作受管bean来看待,因此这些对象完全可以参与依赖注入。

实际上,受管bean的目标是成为所有Java EE组件的基石,传统的JSF后台bean现在是受管bean(可能是使用@ManagedBean来注释)。所有的EJB会话bean现在也被重新定义成提供额外服务(缺省是线程安全和事务性的)的受管bean。Servlet尚未被重新定义成受管bean,不过这很有可能会在不久的将来实现。大体来讲,所有其他的Java EE API将会添加基础受管bean概念的各类服务,主要是借助声明式注解的方式。

 


EJB组件模型是必需的吗?

    询问EJB是否需要它自己的、形式如在@Stateless、@Stateful、@Singleton和@MessageDriven等注解中选一的这一组件模型,这无疑是一个有趣的问题。毕竟,不能也不会像位于受管bean之上的业务组件服务那样简单地进行定位不是?Resin赞同这一观点,允许在非EJB的受管bean上使用诸如@TransactionAttribute、@Remote、@DeclareRoles、@RolesAllowed、@Asynchronous、@Schedule和@Lock一类的EJB声明式服务注解(当然你可以以一种标准已定义的方式来使用EJB)。我们相信这是EJB规范在未来能够很容易往前走的一个方向。


 

正如我们之前提到的,除了为各种类型的受管bean提供依赖注入服务之外,CDI还通过Facelet和JSP这一类视图技术的EL bean名称解析以及自动化的作用域管理来集成JSF。CDI与JPA的集成除了把@EJB和@Resource包括进来之外,还支持使用@PersistenceContext和@PersistenceUnit注入注解。需要注意的是,CDI并不直接支持诸如事务、安全、远程、消息以及其他类似的EJB规范范围内的业务组件服务。

JSR 299利用Java依赖注入(Dependency Injection for Java, JSR330)规范作为它的基础API,主要是使用JSR 330的注解,例如@Inject、@Qualifier和@ScopeType等。JSR 330由Rod Johnson和Bob Lee领导,其极简地定义了依赖注入解决方案的API,主要着眼于非Java EE环境。比较特别的地方是,它没有定义服务器端Java常见的作用域类型,例如请求、会话(session)以及业务会话(conversation)等。它也没有定义与JPA、EJB和JSF这一类Java EE API之间具体的整合语义。CDI基本上是使JSR 330能够适应于Java EE环境,同时还添加了一些对企业级应用有用的附加功能。图1展示了CDI是如何与Java EE平台上的主要API协调工作的。

 



Java resource注解 循环依赖 javaee依赖注入_bean


图1:CDI和Java EE

 

依赖注入的基本原理

 

CDI中真正基本的依赖注入概念相当简单但也很强大,对于大多数做了几年企业级Java开发的人来说,应该是熟悉的,其只是在以Java为中心的类型安全和元数据注解方面多拐了一个弯而已。下面的示例展示了CDI注入最基本的形式(该例子来自EJB 3 in Action的ActionBazaar应用)

 

@Stateless

public class BidService {

    @Inject

    private BidDao bidDao;

 

    public void addBid (Bid bid) {

        bidDao.addBid(bid);

    }

}

 

public class DefaultBidDao implements BidDao {

    @PersistenceContext

    private EntityManager entityManager;

 

    public void addBid (Bid bid) {

        entityManager.persist(bid);

    }

}

 

public interface BidDao {

    public void addBid (Bid bid);

}

 

在上面的例子中,借助@Inject注解把bid DAO受管bean注入到bid service这一EJB会话bean中,CDI通过查找任何实现了BidDao接口的类来解决这一依赖。当CDI发现DefaultBidDao这一实现时,就实例化它,解决它的所有依赖(比如通过@PersistenceContext注入的JPA实体管理器),并把它注入到EJB bid service中。由于没有为服务或者是DAO指定明确的bean作用域,它们被假定成是在隐式的依赖范围之内。我们简单地讨论一下依赖范围,它主要是指被注入对象属于其将被注入到的对象实例。需要注意的是,任一个对象的字符串名称都不能被错误输入,而且所有的代码都是用Java编写的,所以在编译时就会被检查,这有可能是通过一个IDE来实现,当我们在寻找合格者的时候,这种方式的威力甚至会变得更加的显而易见。

合格者(Qualifier)是指附加的两个元数据,当存在多个注入候选者时,其作用是缩小特定类的范围。假设有两个版本的ActionBazaar DAO——使用JPA的缺省版本和使用JDBC的遗留版本,就bid service来说,假定我们需要使用遗留的JDBC DAO来代替使用JPA的DAO,那么下面这个例子展示的就是你会使用qualifier的方式:

 

@Stateless

public class BidService {

    @Inject @JdbcDao

    private BidDao bidDao;

    ...

}

 

@JdbcDao

public class LegacyBidDao implements BidDao {

    @Resource(name="jdbc/ActionBazaarDB")

    private DataSource dataSource;

    ...

}

 

@Qualifier

@Retention(RUNTIME)

@Target({TYPE})

public @interface JdbcDao {}

 

public class DefaultBidDao implements BidDao {

    @PersistenceContext

    private EntityManager entityManager;

    ...

}

 

在这一例子中,有两个类LegacyBidDao和DefaultBidDao是注入到bid service EJB的候选者,CDI通过查找放在bidDao变量之上的jdbcDao合格者(qualifier)来确定基于JDBC的遗留DAO应该被注入。可以注意到,合格者(qualifier)本身是一个在其之上使用@Qualifier标记的自定义注解,因此,相对于较旧的@Resource注解企图通过值“jdbc/ActionBazaarDB”来解析数据源依赖时使用的字符串值来说,其是类型安全的。

这些就是在其之上创建出了更多先进的CDI功能的基础概念,我们在之后的系列文章中会研究更多先进的CDI依赖注入功能,比如版型、生产者、处置者和事件等。

 

上下文管理的基本原理

 

每个由CDI管理的对象都有明确定义的作用域和与某个特定上下文绑定的生命周期。当CDI遇到注入/访问对象的请求时,它会从与为该对象声明的作用域相匹配的上下文中查找检索该对象,如果该对象还未处于上下文中的话,那么CDI会获取到该对象的引用,并在把引用传递给目标时把该对象放入上下文中。当相应于该上下文的作用域到期时,上下文中的所有对象都被清除,表1描述了由CDI定义的上下文:

 

作用域

范围

依赖(Dependent)

依赖引用每次在其被注入时创建,当注入目标被清除时该引用被清除,这是CDI的缺省作用域,在大多数情况下都有意义。

应用范围

(ApplicationScoped)

对象引用在应用运行期间只被创建一次,当应用停止时该引用被丢弃。该作用域对服务对象、助手类API或者是那些存储被整个应用共享的数据的对象有意义。

请求范围

(RequestScoped)

对象引用在HTTP请求期间被创建,当请求结束时该引用被丢弃。该作用域对诸如数据传输对象/模型和JSF后台bean这一类只在HTTP请求期间需要的事物有意义。

会话范围

(SessionScoped)

对象引用在HTTP会话期间被创建,当会话结束时该引用被丢弃。该作用域对诸如可能是用户登录凭据一类的整个会话期间都需要的对象有意义。

业务会话范围

(ConversationScoped)

业务会话是CDI引入的一个新概念,对于Seam用户来说,这是一个熟悉的概念,但对大多数人来说却是新事物。相对于由浏览器、服务器和会话超时控制的会话(session)来说,业务会话(conversation)基本上是一个使用由应用决定的起始点和终止点来截取后的会话(session)。CDI中有两种类型的业务会话——瞬态的(transient的和长期运行的(long-running),瞬态的基本上对应于一个JSF请求周期,长期运行的则由你通过对Conversion.begin和Conversion.end的调用来控制。在需要的时候,瞬态的业务会话可以转变成长期运行的业务会话。对于那些被用在作为多步骤工作流程组成部分的多个页面中的对象来说,业务会话是非常有用的,订单或者购物车就是一个很好的例子。业务会话很值得我们详细讨论,因此在后继的系列文章中我们会对于其进行更加深入的研究。

表1:CDI中的作用域

 

除了上述的内置作用域之外,还可以借助@Scope注解来创建自定义的作用域,这一功能的主要用处是以一种标准的方式来扩展CDI。例如,我们正考虑为后台组件的Resin CanDI实现添加@TransactionScoped,这些后台组件只存在于事务的上下文中,例如一组发送消息给JMS队列或者是执行基本的JDBC操作的抽象API,我们把这作为一个思考实验留给你,当我们在后继的文章中涉及生产者和处置者的时候,这一功能可能会显得更有意义一些,同样,工作流程、BPM等的自定义作用域也可能是有价值的。

让我们通过快速浏览一个代码示例来帮助我们明确某些这一方面的功能,我们把之前的bit service例子拿过来,增强少许,加入适当的作用域类型,然后看看它是如何能用在使用了CDI的视图层中的:

 

@Named

@RequestScoped

public class BidManager {

  @Inject

  private BidService bidService;

  ...

}

 

@Stateless

public class BidService {

    @Inject

    private BidDao bidDao;

 

    @Inject

    private BiddingRules biddingRules;

    ...

}

 

public class DefaultBidDao implements BidDao {

    @PersistenceContext

    private EntityManager entityManager;

    ...

}

 

@ApplicationScoped

public class DefaultBiddingRules implements BiddingRules {

    ...

}

 

在上述例子中,bid manager是一个后台bean/控制器,用于处理来自JSP或者Facelet的请求,因此,对它来说,HTTP请求是最适当的上下文。bid service EJB则与前面一样属于依赖范围这一作用域,这对于一个无状态的会话bean来说,是唯一允许的以及合理的作用域。DAO实例也保持在依赖范围之内,因此其被单独绑定到有可能被池化的bid service EJB实例上,这是合理的,因为既然DAO受管bean实例有一个到非线程安全的实体管理器的引用,那么它确实不应该是可共享的。由于EJB保证是线程安全的和事务性的(这对于Resin来说不是个什么问题,因为所有被注入的实体管理器都被封装在线程安全的代理内,这样的话就不需要EJB的线程安全,并且在需要的时候可以使用事务性的、应用范围内的DAO受管bean),那么只要是在EJB bid service实例的作用域之内使用它,那都是没有问题的。不过被注入到bid service中的Bidding rules策略对象的作用域是整个应用范围,这是因为其只是简单地封装了一些共享的业务规则,这些业务规则是只读的,不会修改任何数据,因此能够被整个应用安全地共享。CDI只是为整个应用创建一个bidding rules实例,并在任何需要的地方注入它。大多数像这样的助手类API和EJB单例会话bean都很适合于选用应用范围的作用域。

CDI的这些上下文管理功能与JSF一起合作,很有效地消除了许多为了维护典型web框架的状态而需要做的样板化的编程工作。

关于这一点,很值得进行详细的讨论,因此我们在后继的系列文章中会展示这样的一些功能。现在,需要注意的是,@Named这一注解使得bid manager可被EL访问,这也是一个在JSF整合方面最值得一谈的话题。

 

更多陆续有来

 

我们在这第一篇文章中谈到的CDI功能实在只是一个相当大的冰山的一角,随着这一文章系列的推进,我们将展示如何在版型中分组元数据,如何借助生产者/处置者方法来创建对象工厂,如何通过依赖注入,通过使用新的业务会话作用域来管理应用状态,通过使用EL名称解析、CDI与JSF之间的交互、拦截器、装饰器以及更多的功能来轻量级化事务的管理。

对于这个在这一点上做出重大改变的游戏来说,现在是有点晚了,不过仍然欢迎你通过发送邮件到jsr-299-comments@jcp.org送来你的关于JSR299的建议。你还可以把关于Java EE 6的各方面建议发送到jsr-316-comments@jcp.org邮箱中,不要介意给我们发邮件,邮箱地址是reza@caucho.com或者ferg@caucho.com。加油,下次见!

 

参考资料

 

1.         JSR 299:Java EE的上下文和依赖注入,http://jcp.org/en/jsr/detail?id=299

2.         Weld,JSR 299的JBoss参考实现,http://seamframework.org/Weld

3.         CanDI,Caucho Resin的JSR 299实现,http://caucho.com/projects/candi/

4.         OpenWebBeans,JSR 299的Apache实现,http://incubator.apache.org/openwebbeans/

 

关于作者

 

Reza Rahman是Resin团队成员,重点研究Resin的EJB 3.1精简版容器。Reza是Manning出版社出版的EJB 3 in Action一书的作者,也是Java EE 6和EJB 3.1专家组的独立成员。他经常在研讨会、各类会议和Java用户组上发言,其中包括JavaOne大会。

Scott Ferguson是Resin的首席架构师和Caucho Technology的总裁,Scott是JSR299专家组的成员,除了创建Resin和Hessian之外,他的工作还包括领导JavaSoft的WebTop服务器,以及创建NFS、DHCP和DNS的Java服务器,他是Sun Web Server 1.0的主创者,这是Solaris上最快的Web服务器。