Java™EE 6 引入了一个新的特性和服务集合,称为上下文依赖注入 (CDI)。Java EE 7 在这一领域添加了更多特性。CDI 依赖于 Java EE 6 中引入的其他技术并为这些技术提供服务。IBM®WebSphere®Application Server V8 和 V8.5(完整配置文件是完全符合要求的 Java EE 6 容器。在 V8.5.5 中,WebSphere Application Server Liberty 配置文件引入了对 Java EE 6 Web 配置文件(其中包含对 CDI 1.0 的支持)的支持。从 V8.5.5.6 开始,Java EE 7 支持完全合规模式并包含对 CDI 1.2 的支持。本文中引用的一些 CDI 特性使用 CDI V1.2。
本文假设您熟悉 CDI 的基本概念。请参阅查阅一篇介绍性文章,其中介绍了 CDI 中的托管 bean、资源绑定、如何管理范围,以及使用限定符和备选项 (Alternative) 来将组件与服务实现隔离。本文将详细介绍范围的概念,深入介绍 CDI 为帮助构建应用程序而引入的一些工具,比如拦截器、修饰器、注释模板和 CDI 事件。
开发使用 CDI 的大型应用程序时,范围与 JEE 之间的交互的一些细节就会显现出来。由于 JEE 容器初始化应用程序组件的方式,实例化 bean 的精确时刻会导致意外的行为。例如,给出了使用内置 CDI bean 的代码。
import java.security.Principal; import javax.inject.Inject; public class User { @Inject private Principal securityPrincipal; public String getUserName() { return securityPrincipal.getName(); } public void setUserName(String userName) { throw new UnsupportedOperationException(); } }
展示了如何注入和使用这个 bean。
public class UserTest extends HttpServlet { private static final long serialVersionUID = 1L; @Inject User u; // ..... protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { System.out.println("The user name is " + u.getUserName()); }
要让中的代码正常运行,必须在容器中启用应用程序安全性,还必须在 servlet 上定义安全限制。在完成这些任务后,您甚至还会看到一些奇怪的结果:从 getUserName
方法调用返回的值是空的。为什么出现这种情况?
对于具有依赖范围的 bean( User
类也具有依赖范围,因为 @Dependent 是默认范围),bean 的注入会在初始化注入目标时发生。此行为意味着,在初始化 servlet 时注入了一个 User
实例。初始化 servlet 时,没有 Principal
,因为 Principal
与一个 HttpRequest
有关联。而且也没有与 servlet 有关联的请求。因此,当 User
类注入 servlet 中时,容器会分配可用的 Principal
的当前值(为 null)。
那么内置的 bean 是否在 Web 容器中无用?完全不是!如果将 User
类定义为 @RequestScoped,那么它是具有正常范围的 bean 的事实意味着它会通过客户端代理注入。servlet 使用客户端代理来完成初始化,而且在的 doGet
方法使用 User
时,容器会解析该类的实际实例的引用。所以当容器实例化 User
类时, HttpRequest
就会激活, Principal
会正确解析为与登录的用户对应的实例。
通过一个示例展示了 producer 方法的工作原理,以及它们如何根据您设置的上下文(在本例中为 paymentStrategyType 字段的值)来动态返回特定的 bean 类型。
@SessionScoped public class PaymentStrategyProducer implements Serializable { private PaymentStrategyType paymentStrategyType; public void setPaymentStrategyType(PaymentStrategyType type) { paymentStrategyType = type; } @Produces @SessionScoped /* * This must be a scope that allows one to get to the same * contextual reference of the bean (PaymentStrategyProducer) * that the setPaymentStrategyType method was invoked on. For * example, if the bean was @SessionScoped, then the method * could be @RequestScoped */ PaymentStrategy getPaymentStrategy( @CreditCard PaymentStrategy creditCard, @Cheque PaymentStrategy cheque, @Online PaymentStrategy online) { switch (paymentStrategyType) { case CREDIT_CARD: return creditCard; case CHEQUE: return cheque; case ONLINE: return online; default: throw new IllegalStateException(); } } }
中所示的 producer 方法的使用非常简单。给出了一个示例。
@Stateless @LocalBean public class StrategyBean { @Inject PaymentStrategyProducer psp; @Inject PaymentStrategy ps; public StrategyBean() { } @RolesAllowed("everyone") public void setPaymentStrategyType(PaymentStrategyType psType) { psp.setPaymentStrategyType(psType); } @RolesAllowed("everyone") public PaymentStrategy getPaymentStrategy() { return ps; } }
这个 EJB 的使用非常简单。基于 Web 请求中的查询参数,在用户的 Web 会话的某个时刻,会调用 StrategyBean
setPaymentStrategy
方法。在这之后,同一个会话中对 getPaymentStrategy
方法的任何后续请求都会看到正确的 PaymentStrategy
对象。如果应用程序需要在会话中途更改 PaymentStrategy
类型,producer 方法 getPaymentStrategy
需要在代码中使用 @RequestScoped()。
清单 3 中的代码类似于 CDI 规范中的一个例子,但有一个重要区别。来自规范的 producer 方法没有范围限定符,也即范围为 @Dependent。但是,如果该方法具有依赖范围,则会看到奇怪的效果:当您尝试调用 StrategyBean
的 setPaymentStrategyType
方法时,会在清单 3 中的 switch(paymentStrategyType)
行抛出 NullPointerException。
同样地,出现这种情况的原因,与前一个例子中获得奇怪的 @Dependent 范围结果的原因相同。当容器实例化一个 StrategyBean
实例时,它不仅会注入一个 PaymentStrategyProducer
实例,还会注入一个 PaymentStrategy
实例。但是,当需要注入 PaymentStrategy
时,只能使用 producer 方法获得它。producer 方法是在调用 setPaymentStrategyType
方法之前执行的,因此 paymentStrategyType
在当时为 null。因此会抛出 NullPointerException。
以下示例展示了一种可用于 @Dependent 范围 producer 方法的有效策略。展示了如何修改中的代码来使用 PaymentStrategyWrapper
类。
@Stateless @LocalBean public class StrategyBean { @Inject PaymentStrategyProducer psp; @Inject PaymentStrategyWrapper psw; public StrategyBean() { } @RolesAllowed("everyone") public void setPaymentStrategyType(PaymentStrategyType psType) { psp.setPaymentStrategyType(psType); } @RolesAllowed("everyone") public PaymentStrategy getPaymentStrategy() { return psw.getPaymentStrateg(); } }
给出了包装器本身。这里的核心概念是包装器为 @RequestScoped,所以定义了一个具有正常范围的 bean。
@RequestScoped public class PaymentStrategyWrapper { private @Inject PaymentStrategy ps; public PaymentStrategy getPaymentStrategy() { return ps; } }
现在,因为 PaymentStrategyWrapper
具有正常范围,所以仅在使用它时才实例化它的实例。此过程在 StrategyBean
bean 的 getPaymentStrategy
方法中发生,仅在调用 setPaymentStrategy
方法后才会调用该方法。
如果您不想创建一个适用于 @Dependent 范围 producer 方法的包装器,可以使用 javax.enterprise.inject.Instance
接口来采用不同的策略。此接口是使用一个通用参数类型来定义的,而且拥有可在运行时用来过滤多种 bean 实现类型的 select 方法。显示了清单 3 的一种备选项实现。请注意 producer 方法的单一参数,它可以代替前一个版本中的任何 bean 类型。
@SessionScoped public class PaymentStrategyProducer implements Serializable { private PaymentStrategyType paymentStrategyType; public void setPaymentStrategyType(PaymentStrategyType type) { paymentStrategyType = type; } @Produces PaymentStrategy getPaymentStrategy(@Any Instance<PaymentStrategy> ps) { switch(paymentStrategyType){ case CREDIT_CARD: return ps.select(new AnnotationLiteral<CreditCard>(){}).get(); case CHEQUE: return ps.select(new AnnotationLiteral<Cheque>(){}).get(); case ONLINE: return ps.select(new AnnotationLiteral<Online>(){}).get(); default: throw new IllegalStateException(); } } }
的 getPaymentStrategy
方法展示了如何使用这个 producer 方法。
@Stateless @LocalBean public class StrategyBean { @Inject PaymentStrategyProducer psp; @Inject Instance<PaymentStrategy> ps; public StrategyBean() { } @RolesAllowed("everyone") public void setPaymentStrategyType(PaymentStrategyType psType) { psp.setPaymentStrategyType(psType); } @RolesAllowed("everyone") public PaymentStrategy getPaymentStrategy() { return ps.get(); } }
请注意, PaymentStrategyProducer.getPaymentStrategy
方法的定义中使用了 @Any 注释。使用此注释,容器在限定可在当时注入的依赖项时可考虑多种候选类型。@Any 是一个内置的限定符,任何 bean 都具有此限定符,无论是否声明了它。在这个示例中,指定 @Any 注释意味着可将具有与 PaymentStrategy
匹配的 bean 类型的 bean 实例作为此方法的参数来注入。容器无法理解 @Any PaymentStrategy
。简单地指定 Instance<PaymentStrategy>
不起作用,因为没有 bean 符合注入条件。即使您拥有实现接口 PaymentStrategy
的 bean 类型,也不能在注入时刻分配这些 bean。在注入时刻,只会隐式地应用 @Default 限定符,所有实现 bean 都没有应用 @Default 限定符。
您可以在实现 bean 中显式指定 @Default 限定符。但这也会失败: StrategyBean
中的注入点现在能够直接注入 PaymentStrategy
bean,无需使用 producer 方法,而且容器会抛出一个 AmbiguousResolutionException 异常,因为它不再知道是应该使用 producer 方法还是直接注入来实例化该 bean。
拦截器是一种借鉴自面向方面的编程领域的编程和设计模式。它们是在 JEE5 中引入的,是 EJB 3.0 API 的一部分。通过使用拦截器,您可以在调用感兴趣的方法(被拦截的方法)之前和之后执行代码(拦截器方法)。Servlet 过滤器具有类似的工作原理。但是,这里的范围是拦截器可在 CDI 托管的任何 bean(也可能是另一个拦截器)上定义的。
面向方面的编程的典型用例是横切关注点,比如日志记录或安全性。拦截器也可用于此用途。但是,它们的灵活性支持更高级的用途,比如在代码中定义抽象层来实现代码重用。考虑采用一个现有应用程序的示例,该应用程序一次一个文件地处理文件目录树,其中的源文件位于本地文件系统上。
假设传入了一个新需求:应用程序需要处理文件系统上一些以 .zip 文件形式存在的树。处理这种情况的一种直观方式是克隆现有代码来创建处理 .zip 文件的新版本。一种更好的方法是添加拦截器来解决处理 .zip 文件的额外的复杂性。现在,如果额外需要在某些时候从远程位置获取文件,可添加一个额外的抽象层来处理树的位置。这些抽象层如图 1 所示。
到展示了如何使用拦截器实现此示例。
@Inherited @InterceptorBinding @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD, ElementType.TYPE}) @LocalCopy public @interface UnzipArchive { }
展示了处理已存档输入的提取的抽象层。
@UnzipArchive @Interceptor public class UnzipArchiveImpl { @AroundInvoke public Object unzip(InvocationContext ic) throws Exception { Object [] parms = ic.getParameters(); String zipPath = (String)parms[0]; if(zipPath.endsWith(".zip")){ // The input is a .zip file. // Unzip.... parms[0] = zipPath.replaceAll("[.]zip$", ""); // Set unzipped to location as argument to intercepted method ic.setParameters(parms); } return ic.proceed();
@Inherited @InterceptorBinding @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD, ElementType.TYPE}) public @interface LocalCopy { }
展示了如何处理远程文件的抽象层(将他们转换为本地文件,供应用程序的剩余部分使用)。
@LocalCopy @Interceptor public class LocalCopyImpl { @AroundInvoke public Object copy(InvocationContext ic) throws Exception { Object [] parms = ic.getParameters(); String remotePath = (String)parms[0]; System.out.println("LocalCopyImpl: " + remotePath); // Retrieve file from remote location and place it locally // In practice this path would not be hard-coded and built using logic parms[0] = "/home/zaphod/IBM/alpha-workspaces/rsa-models/cdiEJB.zip"; ic.setParameters(parms); return ic.proceed();
public class FileProcessor { @UnzipArchive public void process(String treeTop) { Path topDir = Paths.get(treeTop); try { Files.walkFileTree(topDir, new SimpleFileVisitor<Path>() { public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { System.out.println("File = " + file.toString()); return FileVisitResult.CONTINUE; } });
展示了最终部分 - 如何启用拦截器,以便在应用程序调用 FileProcess.process
方法时,容器会自动调用 @LocalCopy 和 @UnzipArchive 拦截器。
<beans xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/beans_1_0.xsd"> <interceptors> <class>com.ibm.issw.bean.LocalCopyImpl</class> <class>com.ibm.issw.bean.UnzipArchiveImpl</class> </interceptors> </beans>
InvocationContext.proceed
。如果调用此方法,在以后调用所有拦截器后都会调用目标 bean。 因为拦截器启用和排序是在 beans.xml 文件中指定的,而且因为 beans.xml 文件对 JEE 模块有效,所以拦截器启用范围为模块级别。此范围是在包含被拦截的 bean 的模块中定义的,而不是在包含拦截器注释和绑定的模块中定义。WebSphere Liberty V8.5.5.6 和更高版本中包含的 Weld 实现支持 bean 存档级别的启用,但此粒度级别并不是标准所要求的。
在图 2 中,EAR 文件是使用 3 个 JEE 模块构建的:Web 模块、EJB 模块和包含 3 个拦截器绑定的实用程序模块。实用程序模块位于 EJB 和 Web 模块的类路径中。实用程序模块中的拦截器绑定应用于 EJB 模块和 Web 模块中包含的 bean。
如以下代码示例所示,即使这些模块中的 bean 可使用所有 3 个拦截器来标注,在 EJB 模块中也仅启用了拦截器 1 和 2()。相较而言,在 Web 模块中,所有 3 个拦截器都已启用()。
<beans xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/beans_1_0.xsd"> <interceptors> <class>com.ibm.issw.bean.Interceptor1Impl</class> <class>com.ibm.issw.bean.Interceptor2Impl</class> </interceptors> </beans>
<beans xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/beans_1_0.xsd"> <interceptors> <class>com.ibm.issw.bean.Interceptor1Impl</class> <class>com.ibm.issw.bean.Interceptor2Impl</class> <class>com.ibm.issw.bean.Interceptor3Impl</class> </interceptors> </beans>
您可以为整个应用程序启用拦截器,无论在何处使用它。同样的机制也可用于定义拦截器的顺序。您使用了 @Priority 注释,该注释接受一个整数参数。这些整数参数的有效值的定义很复杂。一种简单方法是使用介于 2000 到 2999 的值。也可以将这些值写为 Interceptor.Priority.APPLICATION - Interceptor.Priority.APPLICATION + 999。展示了如何使用 @Priority 启用和优先化拦截器。
@Interceptor1 @Interceptor @Priority(Interceptor.Priority.APPLICATION + 5) public class Interceptor1Impl { private boolean status; public Interceptor1Impl() { status = true; }
定义了较低 @Priority 值的拦截器会在具有较高值的拦截器之前调用。如果为两个拦截器定义了相同的优先级值,那么它们的调用顺序并不明确。如果使用 @Priority 启用了一些拦截器,使用 beans.xml 机制启用了其他拦截器,那么使用 @Priority 启用的拦截器会在使用 beans.xml 文件启用的拦截器之前调用。
请注意,拦截器是在 JEE5 中引入 EJB 3.0 规范的,因此,EJB 规范在传统上定义了自己的拦截器启用机制。JEE 6 和 7 已针对拦截器规范的行为调整了 EJB 拦截器的行为,但一些特定于 EJB 的行为仍然存在(比如使用部署描述符来启用拦截器)。特定于 EJB 的行为的另一个示例是,不允许与 EJB 结合使用 Java Transaction API (JTA) 事务性拦截器。最重要的是,即使没有为 EJB 模块启用 CDI,也允许在 EJB 上使用拦截器。
要构建复杂的应用程序,您需要向设计中应用抽象。抽象通过模块化功能和概念来帮助管理复杂性。注释模板是一个抽象层,可应用于使用拦截器作为较低级机制来实现必要函数的实现。注释模板通过为拦截器元数据提供封装机制来实现抽象。
如所示,注释模板实质上是与一个或多个拦截器的绑定(在本例中,@Cloak 是一个拦截器)和与默认 bean 范围的绑定。
@RequestScoped @Stereotype @Target({ElementType.TYPE}) @Cloak // Interceptor @Retention(RetentionPolicy.RUNTIME) public @interface CloakedList { }
如果 bean 中使用了注释模板,所有绑定到该注释模板的拦截器都会应用于该 bean。指定的范围不是注释模板或底层拦截器绑定的范围(它只是一个注释)。该范围是应用了注释模板的 bean 的范围。如果 bean 显式定义了范围,则不会使用注释模板中定义的范围。给出了一个使用中定义的注释模板的 REST 端点。
@Path("/slist1") @CloakedList @CachedList public class SpecialListOne { @Path("/{type}") @GET @Produces(MediaType.APPLICATION_JSON) public Object view(@PathParam("type") String entityType) { // More code to implement the REST service
要更好地了解注释模板的优势,可以考虑这个示例背后的用例。在许多应用程序中,通用框架提供了内容查看和管理服务,而且依赖于用户的限制通常会限制可提供给用户的内容。依赖于用户的显示在军事情报工作或者甚至警察工作中很常见。人员 X 不能查看或编辑某些文档,而人员 Y 可以。另外,这些应用程序可提供搜索功能,其中甚至可能具有限制查看搜索结果的能力(遮蔽列表)。
所以,在示例应用程序中,您可能支持完全开放的列表、部分开放的列表(中的遮蔽列表),当然还支持访问文档本身()。访问文档的能力可能支持所有授权用户执行查看访问,而一小部分用户能够修改文档。要支持所有这些功能,使用两种不同的注释模板标记了 3 种不同的内容提供机制。向所有人开放的列表没有注释模板,遮蔽列表使用 @CloakedList 注释模板,文档服务使用了 @PrivilegedAccessOnly 注释模板。此外,如果列表是搜索词汇后生成的,则需要实现列表的缓存。在应用缓存后,搜索功能的性能更高。这样一种功能可使用中的 @Cached 注释模板来实现。
展示了如何对特权访问使用注释模板。
@PrivilegedAccessOnly @Path("/doc") public class SpecificDocument { @GET @Path("{id}") @Produces(MediaType.APPLICATION_JSON) public JSONObject getDocument(@PathParam("id") String docId) { // More code to implement the REST service
将大量复杂性隐藏在 @PrivilegedAccessOnly 注释模板下。一个简单的标记背后可能有许多拦截器。例如,假设用户被拒绝访问文档,而且需要采用一种应用程序机制来请求访问。授予此访问的人员或小组可能取决于文档的分类级别或文档本身。这样一个服务可通过在 InvocationContext.proceed
方法之前检查的拦截器来实现。如果访问被拒绝,拦截器会负责提供响应内容。对于这样一个应用程序的实现者和维护者,使用注释模板所支持的抽象非常明确地分离了关注点。
如果您在一个注释模板定义中使用了 @Alternative 注释,那么使用该注释模板的每个 bean 都是备选项。请参阅,了解备选项的详细信息。此方法允许您使用注释作为应用程序或产品内基于配置的功能差异的抽象机制。
有两种标记文档分类的样式:
两种不同的样式可能是产品的配置选项,并由备选项来实现。
可在一个 bean 上使用多个注释模板,而且这些注释模板可以在其定义中指定不同的默认范围。不同的范围会导致部署错误。避免该错误的唯一方式是在使用默认注释模板范围的 bean 上覆盖这些范围。
当组件具有不同的可变点时,您可以选择使用继承作为建模各种类型的组件的一种方法。例如,如果您在处理图书,您可能拥有教科书、笔记本或日记本等各种图书。可能还有许多变体,但很容易从一个通用的父类 “Book” 将这些变体建模为派生类。
组合这些变体时,此方法就会出现问题。例如,对于建筑,您可能拥有个人住房、办公大楼或饭店。但也可能为在家里工作的个人提供个人住房。个人住房可以是工作地点(办公室)、饭店顶层的办公室或个人住房,或者包含工作地点、个人住房或饭店的高层建筑。这些组合的数量有时很快让基于继承的解决方案变得非常复杂。
用于解决此问题的一种常见设计模式是修饰器模式。图 3 展示了修饰器的工作原理。
这里的核心概念是修饰器保存组件的引用。当然,可以将此引用解析到 ConcreteComponent 的实例。但也可以将它解析到另一个修饰器的实例。所以,在个人住房、办公室和饭店的示例中,您可能有一个具体的组件 BuildingImpl。修饰器可能是 HomeDecorator、OfficeDecorator 和 RestaurantDecorator。所以,如果您需要一个组合了住房和办公室的一些方面的建筑,可以实例化一个引用 BuildingImpl 的 HomeDecorator,然后提供 HomeDecorator 作为 OfficeDecorator 的组件引用。
要实现此示例,必须能够全面控制对象实例化流程,能够实例化正确的修饰器,并将正确的引用注入到其构造函数中。但除非容器能够控制 bean 实例化,否则 CDI 不起作用。所以,CDI 支持一种有限形式的修饰器模式。当使用 CDI 时,您可以控制哪些修饰器可用(也就是说,组件的特性或变体的特定组合)。容器在运行时实例化所有修饰器。在运行时,您不能动态地定义需要哪种特定的修饰器组合。
展示了如何为任何实现 CheckoutCounter 接口的非修饰器托管 bean 定义两个修饰器(请注意不能对修饰器进行修饰)。
public interface CheckoutCounter { public int sellProduct(String productId, int quantity); public int lookupPrice(String productId); } @Decorator public class AlcoholTobaccoHandler implements CheckoutCounter { @Inject @Delegate CheckoutCounter delegate; //.... @Override public int sellProduct(String productId, int quantity) { if(isAlcoholicProduct(productId) || isTobaccoProduct(productId)){ return 1; }else return delegate.sellProduct(productId, quantity); } } @Decorator public class ControlledSubstanceHandler implements CheckoutCounter { @Inject @Delegate CheckoutCounter delegate; //.... @Override public int sellProduct(String productId, int quantity) { if(isControlledSubstance(productId)) if(quantity <= lookupQuantityLimit(productId)) return delegate.sellProduct(productId, quantity); else return 2; else return delegate.sellProduct(productId, quantity); } } <beans bean-discovery-mode="all" xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/beans_1_1.xsd"> <decorators> <class>com.ibm.issw.cdi.decorators.AlcoholTobaccoHandler</class> <class>com.ibm.issw.cdi.decorators.ControlledSubstanceHandler</class> </decorators> </beans>
给出了 CheckoutCounter 的未修饰实现。当注入 CheckoutCounter 并在它之上调用一个方法时,容器会在 AlcoholTobaccoHandler
类上调用相同的方法。如果它在注入到其中的委托类上调用一个方法,也会在 ControlledSubstanceHandler
类上调用该方法。反过来,如果 ControlledSubstanceHandler
类发出委托,则会调用 FullServiceCounter
中的相应方法。修饰器的委托顺序与 beans.xml 文件中定义它们的顺序相同。
@RequestScoped public class FullServiceCounter implements CheckoutCounter { @Override public int sellProduct(String productId, int quantity) { // Business logic.... } @Override public int lookupPrice(String productId) { // Business logic.... } }
如上面的清单所示,即使修饰器让容器拦截已修饰的 bean,拦截器与修饰器之间也存在根本性的区别。拦截器会在被拦截的方法周围添加函数。相较而言,修饰器会更改被修饰对象的方法实现的行为,也就是说,业务逻辑本身会被修改或增强。
拦截的 bean 也可以被修饰。容器会在调用修饰器之前调用所有拦截器。
所有大型应用程序都需要一定程度地解耦其各种组件,以便允许进行独立开发和维护。函数 X 可能依赖于函数 Y 来完成其工作,但这种依赖性不能是构建时或编译时的依赖性。您可以在架构中使用消息来解耦组件。CDI 提供了更加轻量的机制来解耦交互的组件:支持基于事件的编程。
CDI 需要 3 种结构来实现基于事件的编程:事件本身、观察事件的实体和触发事件的实体。您必须为事件指定事件对象和一个限定符。限定符必不可少,这样同一个事件对象才能支持不同的事件类型。事件对象是一个定期管理的 bean。给出了一个引起(“触发”)该事件的代码示例。这是一个 REST 端点,用于处理 Path 参数来选择要触发的正确事件。ClearanceEvent 对象本身是一个 POJO,其中没有与 CDI 相关的特殊处理。
@Path("/admin") @RequestScoped public class SecurityAdmin { @Inject @Grant private Event<ClearanceEvent> ge; @Inject @Revoke private Event<ClearanceEvent> re; @Inject EmployeeList eDir; @GET @Path("{action}") public String processAdminAction(@PathParam("action") String clearanceAction, @QueryParam("employeeName") String name) { if(clearanceAction.equals("grant")) ge.fire(new ClearanceEvent(clearanceAction, eDir.getEmployee(name))); else if(clearanceAction.equals("revoke")) re.fire(new ClearanceEvent(clearanceAction, eDir.getEmployee(name))); return "Administrative action!!"; } }
给出了一个事件使用者(“观察者”)。
@SessionScoped public class Auditor implements Serializable{ private static final long serialVersionUID = 5434691128553517536L; public void checkValidity(@Observes @Grant ClearanceEvent ce) { System.out.println("auditor " + id + " observed event!"); }
和展示了如何使用 CDI 事件来解耦 UI 前端的开发。管理员工访问权的应用程序功能与为所有审计任务和其他需要在发生这些操作时运行的后端任务提供支持的函数分离。当发生针对员工的 grant
操作时,容器会调用 Auditor checkValidity
方法,因为该方法在观察该事件。
事务观察者方法是收到与事务的不同阶段相关的事件通知的观察者方法。这些方法使用了注释 @Observes(during = <value>),其中 <value> 是 javax.enterprise.event.TransactionPhase
的枚举器之一。如果存在事务上下文,这些方法会在正确的事务阶段上收到事件。
CDI 事件可能等效于解耦应用程序组件的基于消息的设计(发布和订阅)。但是,不同于消息,它们在行为上是同步的。
本文介绍了如何开发可维护的、稳健的应用程序的高级 CDI 技术。您探索了有效使用注释模板、拦截器、修饰器和事件的示例代码和指南。
感谢 IBM 的 CDI 开发主管 Emily Jiang 评审本文并提供宝贵的反馈。