java ee cdi
CDI最令人兴奋的功能之一是允许任何人编写对Java EE核心平台的强大扩展,甚至更改核心行为本身。 这些扩展可以在支持CDI的任何环境中完全移植。 本文概述了主要的CDI功能,探讨了示例Web应用程序,并概述了该框架的一些基本机制。
当前有三种CDI实现: JBoss Weld (参考实现), Apache OpenWebBeans和Caucho CanDI 。 一些库已经提供了CDI扩展,例如Apache DeltaSpike,JBoss Seam 3和Apache MyFaces CODI。
一点历史
CDI规范最初以“ Web Beans”的名称开发,旨在填补后端的Enterprise Java Bean(EJB)和视图层的JavaServer Faces(JSF)之间的空白。 该初稿仅针对Java Enterprise Edition(Java EE),但是在创建规范期间,很明显,大多数功能对于包括Java SE在内的任何Java环境都非常有用。
同时, Guice和Spring社区都开始努力将注入的基础指定为“ JSR-330:Java依赖注入”(昵称为“ AtInject”)。 考虑到在不与实际的注入API协作的情况下提供新的依赖项注入容器是没有意义的,因此AtInject和CDI专家组密切合作以确保跨依赖项注入框架的通用解决方案。 因此,CDI使用AtInject规范中的注释,这意味着每个CDI实现都像Guice和Spring一样完全实现AtInject规范。 CDI和AtInject都包含在Java Enterprise Edition 6(JSR-316)中,因此几乎是每个Java Enterprise Edition Server的组成部分。
基本CDI
在继续研究一些代码之前,让我们快速看一下一些关键的CDI功能:
- 类型安全性 :CDI不用Java语言类型来注入,而不是使用(字符串)名称注入对象。 当类型不足时, 可以使用 限定符 注释。 这使编译器可以轻松检测错误,并提供轻松的重构。
- POJO :几乎每个Java对象都可以由CDI注入! 这包括EJB,JNDI资源,持久性单元和持久性上下文,以及以前由工厂方法创建的任何对象。
- 可扩展性 :每个CDI容器都可以通过使用便携式“扩展”来增强其功能。 “便携式”属性意味着这些CDI扩展可以在每个CDI容器和Java EE 6服务器上运行,而不管哪个供应商。 这是通过良好指定的SPI(服务提供商接口)来完成的,该接口是JSR-299规范的一部分。
- 拦截器 :编写自己的拦截器从未如此简单。 由于JSR-299的可移植行为,它们现在还可以在每台经过EE 6认证的服务器和所有独立CDI容器上运行。
- 装饰器 :这些装饰器允许动态扩展现有的接口实现与业务方面。
- 事件 :CDI指定一种类型安全的机制,以通过松散耦合发送和接收事件。
- 统一的EL集成 :EL-2.2在灵活性和功能性方面开辟了新的视野。 CDI为此提供了现成的支持!
进入CDI
让我们从探索示例Web应用程序开始。 该应用程序允许您通过Web表单发送电子邮件-非常简单。 我们仅提供代码片段,但它们应足以了解如何使用CDI。 显示了应用程序的每个部分之后,我们将在下一章中讨论详细信息。对于我们的邮件应用程序,我们需要一个“应用程序范围内”的 MailService 。 应用程序范围的对象本质上是单例–容器将确保您每次将其注入到应用程序中时始终获得相同的实例。 该 地址 的replyTo 从 它也是应用程序范围内 的 ConfigurationService 拍摄 。 在这里,我们看到了第一次注入–“配置”字段不是由应用程序的代码设置的,而是由CDI注入的。 该 @Inject 注解告诉CDI进行注射。
@ApplicationScoped
public class MyMailService implements MailService {
private @Inject ConfigurationService configuration;
public send(String from, String to, String body) {
String replyTo = configuration.getReplyToAddress();
... // send the email
}
} 我们的应用程序还会标识当前用户(请注意,它不会尝试执行任何身份验证或授权,我们只是相信所有人!),并且当前用户的范围是HTTP会话。 CDI提供了会话作用域,以确保每个HTTP会话(在Web应用程序中)将获得相同的对象实例。
默认情况下,CDI bean无法通过统一表达式语言在JSF中使用。 为了公开它供JSF和EL使用,我们添加了 @Named 批注:
清单2
@SessionScoped
@Named
public class User {
public String getName() {..}
..
}
The web page is implemented with JSF 2. We suggest you use a controller class:
@RequestScoped
@Named
public class Mail {
private @Inject MailService mailService;
private @Inject User user;
private String text; // + getter and setter
private String recipient; // + getter and setter
public String sendMail() {
mailService.send(user.getName(), recipient, text);
return "messageSent"; // forward to 'message sent' JSF2 page
}
}
The only missing part now is the JSF page itself, sendMail.xhtml:
<h:form>
<h:outputLabel value="Username" for="username"/>
<h:outputText id="username" value="#{user.name}"/><br/>
<h:outputLabel value="Recipient" for="recipient"/>
<h:inputText id="recipient" value="#{mail.recipient}"/><br/>
<h:outputLabel value="Body" for="body"/>
<h:inputText id="body" value="#{mail.body}"/><br/>
<h:commandButton value="Send" action="#{mail.send}"/>
</h:form>
现在我们有了一个可以正常工作的应用程序,让我们探究一些正在使用的CDI功能。
基本机制
有时,回顾框架的幕后至少了解非常基本的机制非常有帮助。 因此,我们想首先解释其中的一些技术。
依赖注入 :DI有时被描述为“好莱坞原则” –“不要给我们打电话,我们给您打电话”。 这意味着我们让容器管理实例的创建并注入实例,而不是使用new运算符自己创建实例。 当然,即使在DI容器中,没有任何人触发此过程也不会发生任何事情。 对于JSR-299容器,此触发器是对以下方法之一的调用
T BeanManager#getReference(Bean<T> type, Qualifier... qualifiers);
它返回给定类型T的实例。这样做不仅将创建返回的实例,而且还将 以递归的方式 创建其所有 @Inject 注释的子代。
通常, getReference(Bean <T>) 方法不是手动调用的,而是由表达式语言解析器在内部调用的。 通过写类似
<h:inputText value="#{mailForm.text}"/>
ELResolver将查找名称为“ mailForm”的Bean <T>并解析该实例。
范围,上下文,单例 :每个CDI容器的管理对象都是Ward Cunningham所指定的原始“单例”,Ward Cunningham不仅发明了Wiki,而且还与Kent Beck一起在1987年将设计模式引入了计算机科学。从这个意义上讲,“单例”意味着在特定条件下恰好有一个实例。 Ward在Joshua Kerievsky对“重构模式”的评论中指出:
每个计算都有适当的上下文。 面向对象程序设计的许多内容都涉及建立上下文,平衡变量的生存期,使它们存活正确的时间长度然后优雅地消失。
CDI就是要在指定的环境中创建单例。 在我们的案例中,实例的生命周期由其范围定义。 一个 @SessionScoped 注释的bean在每个会话中只存在一次。 我们也可以将其命名为“会话单例”。 如果用户 第一次 访问我们的 @SessionScoped bean,它将被创建并存储在会话中。 以后的每次访问都将返回完全相同的实例。 会话结束时,存储在其中的所有CDI管理实例也将被正确销毁。
如果我们有一个@RequestScoped bean,我们可以将其称为“请求单例”, @ ConversationScoped bean是一个“会话单例”, 依此类推 。
终端“ Managed Bean” :CDI规范中使用了一些术语,需要简要说明。 Java中的“ Bean”一词已经很成熟,表示带有getter和setter的POJO(普通的旧Java对象)。 术语“ Managed Bean”现在意味着完全不同的东西。 它不是指类的实例,而是指可用于创建这些实例的元信息。 它由 Bean <T> 接口表示,并将 在容器启动时通过类路径扫描收集。
终端“上下文实例” :上下文实例恰好是我们每个作用域的单例实例,“会话单例”,“请求单例”等。通常,用户从不直接使用上下文实例,而只能通过其“上下文引用”使用
终端“上下文引用” :默认情况下,CDI容器通过代理包装所有上下文实例,并且仅注入那些代理而不是真实实例。 在CDI规范中,这些代理称为“上下文引用”。 CDI默认使用代理的原因有很多:
- 序列化 :我们不需要序列化整个对象,而只需要序列化代理。 反序列化时,它将自动再次“连接”到正确的上下文实例。
- 范围差异 :使用代理,可以将 @SessionScoped UserSettings 注入到 @ApplicationScoped MailService中, 因为代理将“连接”到正确的UserSettings本身。
- 拦截器和装饰器 :代理是以非侵入方式实现拦截器和装饰器的完美方法。
CDI容器的生命周期
让我们看一个简单的场景,在普通的Servlet引擎(例如Apache Tomcat)中使用CDI容器。 如果 启动 WebApplication ,则 ServletFilter 还将自动启动您的CDI容器,该容器将首先注册 ClassPath 上可用的所有CDI-Extensions ,然后从类扫描开始。 用 META-INF / beans.xml中 所有 的类路径 条目 将被扫描和所有的类将被解析并存储为“管Bean”( 接口豆 <T>)的CDI容器内的元信息。 在启动时扫描此信息的原因是:首先。 尽早发现错误,大大提高了运行时的性能。
为了能够正确处理所有CDI范围,CDI容器仅使用标准的Servlet回调(如 ServletRequestListener 和 HttpSessionListener) 。
标准范围
JSR-299定义了构建经典Web应用程序的最重要范围:
- @ApplicationScoped
- @SessionScoped
- @ConversationScoped
- @RequestScoped
这些作用域用元 标记 为 @NormalScope ,这意味着它们具有明确定义的生命周期。
除了这些之外,还有另一个非常 规 范围: @Dependent 。 如果一个类没有任何显式的CDI范围注释或使用 @Dependent 显式注释, 则将为每个 InjectionPoint 创建一个实例 ,并将共享它们所注入的上下文实例的生命周期。
例如:如果将 @Dependent MySecurityHelper 注入 @RequestScoped MyBackingBean中 ,则 MySecurityHelper 实例将 在请求结束时 与 MyBackingBean 实例 一起销毁 。 如果 将 MySecurityHelper @Inject 注入 @SessionScoped UserSettings 对象,则它将也被视为 @SessionScoped 。
资格赛
如果一个应用程序需要一个接口和一个接口的多个实现,以前可以通过给它们提供不同的名称来解决。 这种方法的问题在于,这种基于字符串的解决方案不是类型安全的,并且很容易导致 ClassCastExceptions 。 CDI规范引入了一种类型安全的方法,以使用 @Qualifier 元注释 实现相同的结果 。
一个小示例:应用程序需要使用JPA访问两个不同的数据库。 因此,我们需要两个不同的 EntityManagers 。 为了区分它们,我们仅创建两个 @Qualifier 批注 @CustomerDb 和 @AdminDb (以类似方式):
@Target( { TYPE, METHOD, PARAMETER, FIELD })
@Retention(RUNTIME)
@Documented
@Qualifier
public @interface CustomerDb {}
这些限定符现在可以轻松用于注入适当的 EntityManager :
public @ApplicationScoped class MyService {
private @Inject @CustomerDb EntityManager customerEm;
private @Inject @AdminDb EntityManager adminEm;
...
如果未使用任何限定符,则将使用预定义的 @Default 限定符。
生产者方法
在前面的示例中,我们有意识地省略了如何 创建 这些 EntityManager 。 一种可能性是使用生产者方法:
public class MyEntityManagerProducers {
@Produces @RequestScoped @CustomerDb
public EntityManager createCustomerDbEm() {
return Persistence.createEntityManagerFactory(„customerDb“).
createEntityManager();
}
@Produces @RequestScoped @AdminDb
public EntityManager createAdminEm() {
return Persistence.createEntityManagerFactory("adminDb").
createEntityManager();
}
...
我们创建 @RequestScoped EntityManagers ,因为 根据定义 , EntityManager 不可序列化–因此我们无法将其存储在会话中。 我们还需要实现一种 在每个请求结束时 正确清理 EntityManager 的方法。 这可以通过使用@Disposes批注的处置方法来完成:
// class MyEntityManagerProducers continued
public void disposeUdEm(@Disposes @UserData EntityManager em) {
em.close();
}
public void disposeBoEm(@Disposes @BackOffice EntityManager em) {
em.close();
}
}
CDI规范基于Observer / Observable模式定义了一种灵活但非常易于使用的事件机制。 在许多EE应用程序中,有必要在会话中“缓存”某些信息。 此类信息的一个示例是用户角色和特权以及基于这些权限的菜单树。 通常不必为每个请求执行此昂贵的计算。 相反,它可以简单地存储在会话中。
这种方法的问题是,在运行时更改用户设置(例如,当用户以管理员身份临时登录或更改其查看语言时)并不容易。 通过使用CDI事件系统,我们可以以一种非常优雅的方式来实现它。 我们只发送一个 UserSettingsChanged 事件 ,而不是手动清除所有相关信息 -感兴趣的每个人都可以做出相应React。 事件本身由类安全地表示,该类还可能包含有效载荷数据:
public class UserSettingsChanged {
public UserSettingsChanged(String userName) {
this.userName = userName;
}
private String userName; // + getter und setter
...
}
现在让我们集中讨论事件源。 为了触发 UserSettingsChangedEvent ,我们首先需要注入一个事件源:
public class MyLoginBean {
private @Inject Event<UserSettingsChanged> userChangedEvent;
...
public boolean login(String username, String password) {
.. do the login stuff
userChangedEvent.fire(new UserSettingsChanged(username));
...
}
} 现在,任何需要对此事件做出React的类都可以通过观察者方法轻松地对其进行观察:
public @SessionScoped class MyBackingBean {
Locale userLanguage;
...
public void refreshLanguage(@Observes UserSettingsChanged usc) {
userLanguage = getDefaultLanguageOfUser(usc.getUserName());
}
...
}如果 触发 UserSettingsChange 事件,则将调用当前活动范围内所有bean的观察者方法。
CDI提供了一种创建自己的自定义拦截器的简便方法,正如我们将通过创建自己的 @Transactional 拦截器进行展示。 不必手动管理事务,我们的小型拦截器将为我们执行此操作,如以下用法示例所示:
@ApplicationScoped
public class MyUserService {
private @Inject EntityManager em;
@Transactional
public storeUser(User u) {
em.persist(u);
}
}
为了实现此功能,我们需要提供两个部分。 第一个显然是注释本身。 它用 @InterceptorBinding 进行 元注释,将 其标记为打算用于拦截器的注释:
@Retention(RetentionPolicy.RUNTIME)
@Target( { ElementType.TYPE, ElementType.METHOD })
@InterceptorBinding
public @interface Transactional {}
第二部分是拦截器实现本身。 此类必须注释为 @Interceptor, 并带有其预期的拦截器绑定。 拦截器功能本身是通过一种方法实现的,该方法被注释为 @AroundInvoke:
清单4
@Interceptor @Transactional
public class TransactionalInterceptor {
private @Inject EntityManager em;
@AroundInvoke
public Object invoke(InvocationContext context) throws Exception{
EntityTransaction t =em.getTransaction();
try {
if(!t.isActive())
t.begin();
return context.proceed();
} catch(Exception e) {
.. rollback and stuff
} finally {
if(t != null && t.isActive())
t.commit();
}
}
}
经过两年的可用性,CDI已经被广泛采用。 它已经在众多项目中证明了自己的实力,从提供小型初创公司的生产力到提供每天有数百万用户的可靠性和可扩展性网站。 专家组目前正在积极研究CDI 1.1,该规范将带来一些小的修正和改进,包括对非Web应用程序要求的Java SE引导程序功能的标准化要求。 关于作者的更多信息: Mark Struberg是一位拥有20多年编程经验的软件架构师。 他自1996年以来一直从事Java的工作,并积极参与Java和Linux领域的开源项目。 他是Apache软件基金会的成员,并担任Apache OpenWebBeans,MyFaces,Maven,OpenJPA,BVal,DeltaSpike和其他项目的PMC和委员会。 他还是积极致力于该规范的CDI专家组成员。 Mark在维也纳工业大学的工业软件研究组(INSO)工作。 Pete领导Seam,Weld和CDI TCK项目,是RichFaces项目的顾问,也是Arquillian项目的创始人。 他从事过许多规范的工作,包括JSF 2.0,AtInject和CDI。 他经常在JUG和会议上演讲,例如Devoxx(Javapolis),JAX,JavaBlend,JSFDays和JBoss World。 Pete目前受雇于Red Hat Inc.,从事JBoss开源项目。 在为Red Hat工作之前,他曾在Seam任职并为Seam做出了贡献,当时他在一家英国的人员中介公司担任IT开发经理。 本文最初发表于2012年3月的Java Tech Journal:CDI中– 在此处查找有关该问题的更多信息 :
java ee cdi



所有评论(0)