CDI Features

Posted chhhh

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了CDI Features相关的知识,希望对你有一定的参考价值。

Essential CDI

在我们继续深入研究一些代码之前,让我们快速了解一些关键的CDI功能:

  • 类型安全:CDI使用Java类型来解析注入,而不是通过(字符串)名称注入对象。当类型不足时, 可以使用限定符 注释。这允许编译器轻松检测错误,并提供简单的重构。
  • POJO:几乎每个Java对象都可以由CDI注入!这包括EJB,JNDI资源,持久性单元和持久性上下文,以及之前由工厂方法创建的任何对象。
  • 可扩展性:每个CDI容器都可以通过使用便携式“扩展”来增强其功能。属性“portable”意味着无论哪个供应商,这些CDI Extensions都可以在每个CDI容器和Java EE 6服务器上运行。这是通过一个明确指定的SPI(服务提供者接口)实现的,该接口是JSR-299规范的一部分。
  • 拦截器:编写自己的拦截器从未如此简单。由于JSR-299的可移植性,它们现在也可以在每个经过EE 6认证的服务器和所有独立CDI容器上运行。
  • 装饰器:这些允许动态扩展现有的接口实现与业务方面。
  • 事件:CDI指定一种类型安全机制,用于发送和接收具有松散耦合的事件。
  • 统一EL集成:EL-2.2在灵活性和功能方面开辟了新的视野。CDI为它提供开箱即用的支持!

Diving into CDI

让我们从探索示例Web应用程序开始。该应用程序允许您通过Web表单发送电子邮件 - 相当简单。我们只提供代码片段,但它们应足以让我们了解CDI的使用方法。在显示应用程序的每个部分后,我们将在下一章中讨论详细信息。

对于我们的邮件应用程序,我们需要一个“应用程序范围”的 MailService 应用程序范围的对象本质上是单例 - 容器将确保您在将其注入应用程序时始终获得相同的实例。 的replyTo 地址取自ConfigurationService 这也是应用程序范围。在这里我们看到第一次注入 - “配置”字段不是由应用程序的代码设置,而是由CDI注入。 @Inject 注解告诉CDI进行注射。

Listing1

 

1
2
3
4
6
7
8
9
@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 注释:

Listing 2

1
2
3
4
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
三十
31
32
33
34
@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容器,此触发器是对其中一个方法的调用

 

1
T BeanManager#getReference(Bean<T> type, Qualifier... qualifiers);

 

返回给定类型T的实例。通过这样做,它不仅会 以递归方式创建返回的实例,还会创建所有@Inject-注释的子节点

通常, getReference(Bean <T>) 方法不是手动调用,而是由Expression Language Resolver在内部调用。写点东西

 

1
<h:inputText value="#{mailForm.text}"/>

 

ELResolver将查找名为“mailForm” Bean <T>并解析该实例。

范围,上下文,单例:每个CDI容器的管理对象都是Ward Cunningham指定的原始意义上的“单例”,他不仅发明了Wiki,而且还与Kent Beck一起发明了1987年的计算机科学设计模式。在这种意义上,“单身人士”意味着在指定良好的环境中只有一个实例。在Joshua Kerievsky对“重构模式”的评论中,Ward指出:

每个计算都有适当的上下文。面向对象编程的大部分内容都是关于建立上下文,关于平衡变量生命周期,使它们生活在合适的时间长度然后优雅地死亡。

CDI就是在一个明确的环境中创建单身人士。在我们的例子中,实例的生命周期由它们的范围定义。一个@ SessionScoped-注解豆每会话存在一次。我们也可以将它命名为“会话单身人士”。如果用户 第一次访问我们的@SessionScoped bean,它将被创建并存储在会话中。每个后续访问都将返回完全相同的实例。会话结束时,存储在其中的所有CDI管理的实例也将被正确销毁。

如果我们有一个 @RequestScoped bean,我们可以称之为‘request singleton‘,@ ConversationScoped bean是‘conversation singleton‘等。

Terminus‘Managed Bean‘:CDI规范中使用了一些术语,需要简短说明。Java中的术语“Bean”已经非常成熟,意味着带有getter和setter的POJO(Plain Old Java Object)。终端技术‘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的所有ClassPath 条目, 并且将解析所有类并将其作为“Managed Bean”(接口Bean <T> )元信息存储在CDI容器内。在启动时扫描此信息的原因是:首先。及早发现错误,在运行时大大提高性能。

为了能够正确处理所有CDI作用域,CDI容器只使用标准的Servlet回调,如 ServletRequestListener HttpSessionListener

标准范围

JSR-299定义了构建经典Web应用程序的最重要的范围:

  • @ApplicationScoped
  • @SessionScoped
  • @ConversationScoped
  • @RequestScoped

这些范围被元注释为 @NormalScope ,这意味着它们具有明确定义的生命周期。

除此之外,还有另一个非正常的范围: @Dependent 如果类没有任何显式CDI范围注释或使用@Dependent 显式注释,则将为每个 InjectionPoint 创建一个实例,并将共享它们注入的上下文实例的生命周期。

例如:如果 @RequestScoped MyBackingBean中注入@Dependent MySecurityHelper ,那么MySecurityHelper实例将在请求结束时MyBackingBean 实例一起被销毁如果您@Inject MySecurityHelper @SessionScoped UserSettings 对象,也将被视为@SessionScoped  

Qualifiers

如果一个应用程序需要同一个接口的多个实现,那么以前通过给它们不同的名称解决了这个问题。这种方法的问题是这个基于字符串的解决方案不是类型安全的,很容易导致 ClassCastExceptions CDI规范引入了一种类型安全的方法来实现与 @Qualifier 元注释相同的结果

一个小样本:应用程序需要使用JPA访问两个不同的数据库。因此,我们需要两个不同的 EntityManagers 为了区分这些,我们只创建两个 @Qualifier 注释 @CustomerDb 和 @AdminDb (模拟):

 

1
2
3
4
@Target( { TYPE, METHOD, PARAMETER, FIELD })
@Retention(RUNTIME)
@Documented
@Qualifier
public @interface CustomerDb {}

现在可以轻松地使用这些限定符来注入适当的 EntityManager 

 

1
2
3
4
public @ApplicationScoped class MyService {
 private @Inject @CustomerDb EntityManager customerEm;
 private @Inject @AdminDb EntityManager adminEm;
 ...

如果未使用 限定符,则将假定预定义的@Default 限定符。

 

Producer Methods

在前面的示例中,我们故意省略了如何 创建这些EntityManager 一种可能性是使用生产者方法:

Listing3

1
2
3
4
6
7
8
9
10
11
12
13
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 的每个定义不可序列化 - 因此我们无法将其存储在会话中。我们还需要实现一种 在每个请求结束时正确清理EntityManagers 的方法。使用@Disposes注释可以使用处理方法完成此操作:

 

1
2
3
4
6
7
8
// class MyEntityManagerProducers continued
 public void disposeUdEm(@Disposes @UserData EntityManager em) {
   em.close();
 }
 public void disposeBoEm(@Disposes @BackOffice EntityManager em) {
   em.close();
 }
}

Events

CDI规范定义了一种基于Observer / Observable模式的灵活但非常容易使用的事件机制。在许多EE应用程序中,在会话中“缓存”某些信息是有意义的。此类信息的一个示例是用户角色和权限以及基于这些权限的菜单树。通常没有必要为每个请求执行这种昂贵的计算。相反,它可以简单地存储在会话中。

这种方法的一个问题是在运行时更改用户设置 - 例如,当用户以管理员身份临时登录或更改其查看语言时 - 并不容易。通过使用CDI事件系统,我们可以非常优雅地实现这一点。我们只是发送一个UserSettingsChanged 事件,而不是手动清理所有依赖信息,所有感兴趣的人都可以做出相应的反应。事件本身是由类安全地表示的,它也可能包含有效负载数据:

 

1
2
3
4
6
7
public class UserSettingsChanged {
 public UserSettingsChanged(String userName) {
   this.userName = userName;
 }
 private String userName; // + getter und setter
 ...
}

现在让我们关注事件源。为了触发 UserSettingsChangedEvent,我们首先需要注入一个事件源:

 

1
2
3
4
6
7
8
9
public class MyLoginBean {
 private @Inject Event<UserSettingsChanged> userChangedEvent;
 ...
 public boolean login(String username, String password) {
   .. do the login stuff
   userChangedEvent.fire(new UserSettingsChanged(username));
   ...
 }
}

任何需要对此事件做出反应的类现在都可以通过观察者方法轻松地观察它:

1
2
3
4
6
7
8
public @SessionScoped class MyBackingBean {
 Locale userLanguage;
 ...
 public void refreshLanguage(@Observes UserSettingsChanged usc) {
   userLanguage = getDefaultLanguageOfUser(usc.getUserName());
 }
 ...
}

如果 触发UserSettingsChange 事件,则将调用当前活动作用域中所有bean的观察者方法。

Inceptors

CDI提供了一种简单的方法来创建自己的自定义拦截器,我们将通过创建自己的 @Transactional 拦截器来显示。我们的小拦截器不会手动管理事务,而是为我们执行此操作,如以下用法示例所示:

 

1
2
3
4
6
7
8
9
@ApplicationScoped
public class MyUserService {
 private @Inject EntityManager em;
 
 @Transactional
 public storeUser(User u) {
   em.persist(u);
 }
}

为了实现此功能,我们需要提供两个部分。第一个显然是注释本身。它被元注释为 @InterceptorBinding,它将其标记为旨在用于拦截器的注释:

 

1
2
3
4
@Retention(RetentionPolicy.RUNTIME)
@Target( { ElementType.TYPE, ElementType.METHOD })
@InterceptorBinding
public @interface Transactional {}

第二部分是拦截器实现本身。此类必须注释为 @Interceptor ,另外还有其预期的拦截器绑定。拦截器功能本身是在一个注释为 @AroundInvoke的方法中实现的:

Listing4

1
2
3
4
6
7
8
9
10
11
12
13
14
15
16
17
18
@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 Features的主要内容,如果未能解决你的问题,请参考以下文章

CDI Features

CDI Features

CDI FEATURES

CDI Features(EL(SPEL),Decorator,Interceptor,Producer)

具有相同功能的活动和片段

Java EE 6 和 CDI