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,资源绑定,如何管理范围的介绍性文章,并使用预选赛和替代的从服务实现中分离的组件。 本文详细研究了范围,并探究了CDI引入的一些有助于构建应用程序的功能,例如拦截器,装饰器,构造型和CDI事件。
仔细观察范围
当开发使用CDI的大型应用程序时,在作用域和JEE之间的交互中的某些细微差别就变得可见。 由于JEE容器如何初始化应用程序组件,因此实例化bean的精确点会导致意外行为。 例如, 清单1显示了使用内置CDI bean的代码。
清单1.使用内置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();
}
}
清单2显示了如何注入和使用这样的bean。
清单2.注入一个使用内置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());
}
为了使清单2中的代码正常工作,必须在容器中启用应用程序安全性,并且必须在servlet上定义安全性约束。 即使完成了这些任务,也会看到一些令人惊讶的信息:从getUserName
方法调用返回的值是null。 为什么会这样?
对于依赖范围的bean(由于@Dependent是默认作用域,因此User
类是依赖范围的),因此在初始化注入目标时将进行bean的注入。 此行为意味着初始化servlet时将注入User
实例。 初始化Servlet时,没有Principal
因为Principal
与HttpRequest
关联。 尚未将任何请求与servlet关联。 因此,当将User
类注入到Servlet中时,容器将分配可用Principal
的当前值(为null)。
那么内置的bean是否在Web容器中没有用? 一点也不! 如果将User
类定义为@RequestScoped,则它是普通作用域的bean,这意味着它是通过客户端代理注入的。 Servlet是使用客户端代理初始化的,并且在清单2的doGet
方法中使用容器时,容器将解析对User
实际实例的引用。 因此,当容器实例化User
类时, HttpRequest
处于活动状态,并且Principal
正确解析为与登录用户相对应的实例。
清单3展示了生产者方法如何工作以及如何根据您设置的上下文动态返回特定bean类型的示例,在这种情况下,本例为paymentStrategyType字段的值。
清单3.返回以编程方式确定的类型的生产者
@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();
}
}
}
清单4.注入正确的付款策略
@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
类型,则生产者方法getPaymentStrategy
必须在代码中为@RequestScoped( 清单3 )。
清单3中的代码与CDI规范中的示例相似,但有一个关键的区别。 规范中的生产者方法没有作用域限定符,即@Dependent作用域。 但是,如果该方法是依赖范围的,则会看到一个奇怪的效果:当您尝试调用StrategyBean
的setPaymentStrategyType
方法时,清单3中的switch(paymentStrategyType)
行会引发NullPointerException。
同样,发生这种情况的原因与在上一个示例中使用@Dependent范围产生令人惊讶的结果相同。 当容器实例的实例StrategyBean
,它不仅注入的实例PaymentStrategyProducer
,也是一个实例PaymentStrategy
。 但是,当需要注入PaymentStrategy
时,只能从生产者方法中获取。 生产者方法的执行发生在调用setPaymentStrategyType
方法之前,因此,此时的paymentStrategyType
为null。 因此,NullPointerException。
以下示例显示了可用于@Dependent范围的生产者方法的工作策略。 清单5显示了如何修改清单4中的代码以使用PaymentStrategyWrapper
类。
清单5.使用包装器注入正确的付款策略
@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();
}
}
清单6显示了包装器本身。 关键思想是包装器是@RequestScoped,因此定义了具有正常作用域的bean。
清单6.一个普通的作用域包装器
@RequestScoped
public class PaymentStrategyWrapper
{
private @Inject PaymentStrategy ps;
public PaymentStrategy getPaymentStrategy()
{
return ps;
}
}
现在,由于PaymentStrategyWrapper
是正常作用域的,因此仅在使用它时才实例化它的实例。 这恰好在StrategyBean
bean的getPaymentStrategy
方法中,仅在调用setPaymentStrategy
方法之后才调用该方法。
以编程方式选择bean类型
如果您不想创建用于使用javax.enterprise.inject.Instance
范围的生产者方法的包装,则可以使用javax.enterprise.inject.Instance
接口采用不同的策略。 该接口是通过使用通用参数类型定义的,并且具有select方法,这些方法可用于在运行时过滤出多种bean实现类型。 清单7显示了清单3的另一种实现。请注意Producer方法的单个参数,该参数可以代表先前版本中的任何bean类型。
清单7.具有编程查找的生产者方法
@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();
}
}
}
清单8的getPaymentStrategy
方法显示了如何使用此生产者方法。
清单8.注入@Dependent作用域生产者方法的返回值
@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
方法的定义中使用了PaymentStrategyProducer.getPaymentStrategy
。 使用此注释,容器可以将多种候选类型视为可以在此时注入的合格依赖项。 @Any是一个内置的限定符,任何bean都具有此限定符,无论是否声明。 在此示例中,指定@Any批注意味着可以将类型与PaymentStrategy
匹配的Bean实例作为该方法的参数注入。 PaymentStrategy
对容器不明确。 仅指定Instance<PaymentStrategy>
也不起作用,因为没有bean可以进行注入。 即使您有实现接口PaymentStrategy
接口的bean类型,也不能在注入点分配这些bean。 在注入点仅隐式应用了@Default限定符,并且没有一个实现Bean都应用了@Default限定符。
您可以在实现Bean中显式指定@Default限定词。 但这也将失败: StrategyBean
的注入点现在将能够直接注入PaymentStrategy
Bean,而无需通过生产方方法,并且容器将抛出AmbiguousResolutionException异常,因为它不再知道是使用生产方方法还是直接注入实例化bean
拦截器
拦截器是一种来自面向方面的编程世界的编程和设计模式。 它们是JEE5中引入的,它们是EJB 3.0 API的一部分。 通过使用拦截器,您可以在调用感兴趣的方法(被拦截的方法)之前和之后执行代码(拦截器方法)。 Servlet过滤器以类似的方式工作。 但是,这里的范围是可以在任何CDI管理的bean上定义拦截器,也可以是另一个拦截器。
面向方面的编程的典型用例是解决诸如日志记录或安全性之类的跨领域问题。 拦截器也可以用于此目的。 但是,它们的灵活性允许更高级的用途,例如在代码中定义抽象层以实现代码重用。 考虑一个现有应用程序的示例,该应用程序一次处理一个文件的目录树,其中源文件位于本地文件系统上。
假设有一个新要求:应用程序需要处理文件系统上以.zip文件形式存在的一些树。 一种简单的处理方法是克隆现有代码,以创建处理.zip文件的新版本。 更好的方法是添加拦截器,以处理.zip文件的额外复杂性。 现在,如果有时需要从远程位置获取文件,则可以添加一个附加的抽象层来处理树的位置。 这些抽象层如图1所示。
图1.增强代码以处理其他需求
![增强代码以处理其他要求](https://i-blog.csdnimg.cn/blog_migrate/e875325ecc7ad651ca1e78f57ffc4511.png)
清单9. @UnzipArchive的拦截器绑定
@Inherited
@InterceptorBinding
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
@LocalCopy
public @interface UnzipArchive {
}
清单10显示了处理提取的归档输入的抽象层。
清单10. @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();
清单11. @LocalCopy的拦截器绑定
@Inherited
@InterceptorBinding
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface LocalCopy {
}
清单12显示了处理远程文件的抽象层(在应用程序的其余部分将其转换为本地文件)。
清单12. @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();
清单13.被拦截的代码
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;
}
});
清单14显示了最后一部分–如何启用拦截器,以便在应用程序调用FileProcess.process
方法时,容器自动调用@LocalCopy和@UnzipArchive拦截器。
清单14.在beans.xml中启用拦截器
<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>
关于拦截器的澄清和规则
- 在清单9中 ,@ UnzipArchive的拦截器绑定用@LocalCopy注释。 此注释意味着,每当对截获的代码使用@UnzipArchive时,@LocalCopy也会生效。 如果不希望出现这种情况,则需要采取其他策略:从@UnzipArchive的拦截器绑定中删除@LocalCopy,然后在要使用它们拦截代码的任何地方分别指定@Unzip和@LocalCopy。 例如, 清单13可能使用@UnzipArchive @LocalCopy。
- 拦截器的调用顺序(在这种情况下,@ UnzipArchive之前的@LocalCopy)完全由bean.xml文件中定义的顺序控制。
- 拦截器注释可以在类级别或方法级别使用,因为拦截器绑定允许使用@Target注释进行绑定。 如果在类级别定义拦截器(@Target为ElementType.TYPE),则指定的拦截器可以拦截类中的任何公共方法。
- 拦截器规范定义了一种为方法定义拦截器的替代方法,该方法不需要您定义拦截器绑定。 此替代方法使用@Interceptors批注,可用于直接指定实现拦截器的类。 此方法不是首选方法,因为对此类名称的任何重构都需要更改@Interceptors批注中引用的所有地方的代码。
- 拦截器可以捕获在调用链中进一步调用的另一个拦截器中或从目标Bean中引发的异常。 捕获到异常后,拦截器可以再次合法地调用
InvocationContext.proceed
。 如果调用此方法,则随后的所有拦截器都将被目标bean调用。
@Priority:启用和订购的另一种方式
因为在bean.xml文件中指定了拦截器启用和排序,并且由于beans.xml文件对于JEE模块有效,所以拦截器启用的范围在模块级别。 此范围是在包含被拦截bean的模块中定义的,而不是在包含拦截器注释和绑定的模块中定义的。 WebSphere Liberty V8.5.5.6和更高版本中存在的Weld实现在Bean归档级别支持支持,但是标准不要求这种粒度级别。
在图2中,一个EAR文件由三个JEE模块构建:一个Web模块,一个EJB模块和一个包含三个拦截器绑定的实用程序模块。 实用程序模块位于EJB和Web模块的类路径中。 实用程序模块中的拦截器绑定适用于EJB模块和Web模块中包含的bean。
图2. JEE应用程序的实用程序模块中的拦截器
如下面的代码示例所示,即使可以使用所有三个拦截器对这些模块中的bean进行注释,但在EJB模块中,仅启用了拦截器1和2( 清单15 )。 相反,在Web模块中,所有三个拦截器都已启用( 清单16 )。
清单15. EJB模块的拦截器启用
<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>
清单16. Web模块的拦截器启用
<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。 清单17显示了如何使用@Priority启用拦截器并对其进行优先级排序。
清单17.使用@Priority启用拦截器并确定优先级
@Interceptor1 @Interceptor @Priority(Interceptor.Priority.APPLICATION + 5)
public class Interceptor1Impl {
private boolean status;
public Interceptor1Impl() {
status = true;
}
使用较低的@Priority值定义的拦截器将在使用较高的值的拦截器之前调用。 如果使用相同的优先级值定义了两个拦截器,则它们的调用顺序是不确定的。 如果某些拦截器是通过使用@Priority启用的,而其他拦截器是通过使用bean.xml机制启用的,则启用@Priority的拦截器会在使用beans.xml文件启用的拦截器之前被调用。
注意,拦截器是在JEE 5的EJB 3.0规范中引入的,因此,EJB规范传统上定义了自己的启用拦截器的机制。 JEE 6和7使EJB拦截器的行为与拦截器规范的行为保持一致,但是仍保留了某些特定于EJB的行为(例如通过使用部署描述符启用拦截器)。 EJB特定行为的另一个示例是Java事务API(JTA)事务拦截器不允许与EJB一起使用。 最重要的是,即使未为模块启用CDI,也可以在EJB上使用拦截器。
刻板印象
要构建复杂的应用程序,您需要将抽象应用于设计。 抽象通过模块化功能和概念来帮助您管理复杂性。 构造型是一个抽象层,您可以将其应用于使用Interceptor作为实现必要功能的下层机制的实现。 构造型通过提供拦截器元数据的封装机制来实现抽象。
如清单18所示,构造型实质上是对一个或多个拦截器的绑定(在示例中,@ Cloak是拦截器),并且是对默认bean作用域的绑定。
清单18.定义构造型
@RequestScoped
@Stereotype
@Target({ElementType.TYPE})
@Cloak // Interceptor
@Retention(RetentionPolicy.RUNTIME)
public @interface CloakedList
{
}
如果在bean中使用了构造型,则绑定到构造型的所有拦截器都将应用于该bean。 指定的范围不是构造型(仅是注释)或基础拦截器绑定的范围。 相反,范围是构造型应用于的bean的范围。 如果Bean显式定义了范围,则不使用构造型中定义的范围。 清单19显示了使用清单18中定义的构造型的REST端点。
清单19.使用构造型
@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。 此外,此类应用程序可能会提供搜索功能,其中甚至可能会限制查看搜索结果的能力(隐藏列表)。
因此,在示例应用程序中,您可能支持完全打开的列表,部分打开的列表( 清单19的隐蔽列表),当然还支持对文档本身的访问( 清单20 )。 对文档的访问可能支持所有授权用户的视图访问,并且能够为较小的用户子集修改文档。 为了支持所有这些功能,三种不同的内容服务机制被标记为两种不同的构造型。 向所有人开放的列表没有构造型,隐藏的列表使用@CloakedList构造型,文档服务使用@PrivilegedAccessOnly构造型。 另外,如果列表是由于搜索搜索词而生成的,请实施列表的缓存。 应用缓存时,搜索功能的性能更好。 可以使用清单19中所示的@Cached构造型实现这种功能。
清单20显示了如何使用特权访问的构造型。
清单20.构造型标识提供服务的文档
@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
清单20在@PrivilegedAccessOnly构造型下隐藏了很多复杂性。 大量拦截器可能是该简单标签的基础。 例如,假设用户被拒绝访问文档,并且需要一种应用程序机制来请求访问。 授予此访问权限的个人或团体可能取决于文档的分类级别或文档本身。 可以通过在InvocationContext.proceed
方法之前进行检查的拦截器来实现这种服务。 如果拒绝访问,则拦截器将负责响应的内容。 对于这样的应用程序的实现者和维护者来说,通过使用构造型支持的抽象将关注点非常清晰地分开。
如果在构造型定义中使用@Alternative批注,则使用构造型的每个bean都是替代方案。 请参阅相关主题的替代品的细节。 这种方法使您可以将注释用于抽象机制,以实现应用程序或产品中基于配置的功能差异。
标记文档分类有两种样式:
- 您对文档进行分类,然后将文档分类映射到用户分类:“此文档被标记为SECRET”。 映射将显示:“具有PROJECT TOPAZ访问权限的人可以查看SECRET文档。 具有CLASSIFIED访问权限的人无法看到搜索结果中的SECRET文档。”
- 该文档本身记录了允许哪些分类级别的访问类型:“具有PROJECT TOPAZ访问权限的人员可以查看此文档”。
两种不同的样式可能是产品的配置选项,并且可以通过替代方法来实现。
一个bean上可以使用多个构造型,这些构造型可能在其定义中指定不同的默认范围。 不同的范围会导致部署错误。 避免该错误的唯一方法是在使用它们的bean上覆盖默认构造型作用域。
装饰工
当组件具有不同的可变点时,您可以选择继承作为对各种类型的组件进行建模的方法。 例如,如果您要处理书籍,则可能有一本教科书,一本笔记本或一本日记。 可以有很多变体,但是很容易将它们建模为来自“ Book”的公共父级的派生类。
当这些变体组合在一起时,这种方法就会出现问题。 例如,对于建筑物,您可以拥有个人住宅,办公楼或餐厅。 但是您也可以为在家工作的个人提供个人住所。 个人住所可以是他们的工作地点(办公室),餐厅顶层的办公室或个人住所,也可以是包括工作地点,个人住所和餐馆的高层建筑。 这些组合的数量有时会使基于继承的解决方案Swift复杂化。
用于解决此问题的常见设计模式是装饰器模式。 图3显示了装饰器的工作方式。
图3.装饰器模式的参与者
这里的关键思想是装饰器拥有对Component的引用。 当然,此引用可能会解析为ConcreteComponent的实例。 但是它也可能解析为另一个Decorator的实例。 因此,在个人住宅,办公室和餐厅的示例中,您可能只有一个混凝土组件BuildingImpl。 装饰者可能是HomeDecorator,OfficeDecorator和RestaurantDecorator。 因此,如果您需要将房屋和办公室的各个方面结合在一起的建筑物,则可以实例化一个引用BuildingImpl的HomeDecorator,然后将HomeDecorator作为对OfficeDecorator的组件引用提供。
要实现此示例,您必须完全控制对象实例化过程,并且能够使用注入到其构造函数中的正确引用来实例化正确的装饰器。 但是除非容器由bean实例控制,否则CDI不会起作用。 因此,CDI支持装饰器模式的有限形式。 使用CDI时,您可以控制可用的装饰器(即组件的功能或变体的特定组合)。 容器在运行时实例化所有装饰器。 您无法在运行时动态定义需要哪些特定的装饰器组合。
清单21显示了如何为实现CheckoutCounter接口的任何非装饰器托管Bean定义两个装饰器(请注意,装饰器无法装饰)。
清单21.定义装饰器
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>
清单22显示了CheckoutCounter的一种非修饰实现。 注入CheckoutCounter并对其调用方法后,容器将在AlcoholTobaccoHandler
类上调用相同的方法。 如果它对注入到其中的委托调用方法,则将调用ControlledSubstanceHandler
类的该方法。 反过来,如果ControlledSubstanceHandler
类委托,则调用FullServiceCounter
的相应方法。 装饰器的委派顺序与bean.xml文件中定义它们的顺序相同。
清单22.使用装饰器
@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需要基于事件的编程的三种构造:事件本身,观察事件的事物和触发事件的事物。 您必须指定事件对象和事件的限定符。 限定符是必需的,以便同一事件对象可以支持不同的事件类型。 事件对象是一个常规的托管bean。 清单23显示了引发(触发)事件的代码示例。 这是一个REST端点,该端点处理Path参数以选择要触发的正确事件。 ClearanceEvent对象本身是一个POJO,其中没有特殊的CDI相关处理。
清单23.事件生产者
@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!!";
}
}
清单24显示了事件的使用者(“观察者”)。
清单24.事件使用者
@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!");
}
清单23和清单24显示了如何使用CDI事件来解耦UI前端的开发。 管理员工访问权限的应用程序功能与支持在发生此类操作时需要运行的所有审核和其他后端任务的功能分离。 当员工发生grant
操作时,容器将调用Auditor checkValidity
方法,因为该方法正在观察该事件。
事务性观察者方法是接收与事务的各个阶段有关的事件通知的观察者方法。 这些方法使用注释@Observes(during = <value>),其中<value>是javax.enterprise.event.TransactionPhase
的枚举数javax.enterprise.event.TransactionPhase
。 如果存在事务上下文,则这些方法在正确的事务阶段接收事件。
CDI事件可能看起来等效于基于消息传递的(发布和订阅)设计,以使应用程序组件分离。 但是,与消息传递不同,它们在行为上是同步的。
结论
本文介绍了用于开发可维护且强大的应用程序的高级CDI技术。 您探索了示例代码和有效使用构造型,拦截器,修饰符和事件的指南。
致谢
作者感谢IBM CDI开发负责人Emily Jiang审阅本文并提供了宝贵的反馈。
翻译自: https://www.ibm.com/developerworks/websphere/library/techarticles/1605_stephen-trs/1605_stephen.html