原文:Dependency Injection in Java EE 6 – Part 1
作者:Reza Rahman
出处:http://www.theserverside.com/tt/articles/article.tss?l=DependencyInjectioninJavaEE6
依赖注入的基本原理
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之间的交互、拦截器、装饰器以及更多的功能来轻量级化事务的管理。
对于这个在这一点上做出重大改变的游戏来说,现在是有点晚了,不过仍然欢迎你通过发送邮件到[email protected]送来你的关于JSR299的建议。你还可以把关于Java EE 6的各方面建议发送到[email protected]邮箱中,不要介意给我们发邮件,邮箱地址是[email protected]或者[email protected]。加油,下次见!
参考资料
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服务器。