以前发过一个粗略篇,已经删除.这次重新修订.
Cdi中的event事件,是整个CDI的精华所在之一.其有点类似设计模式中的观察者模式.但也有不同的地方.如下3点:
即用一种维护生产者和观察者之间的分离代码的方式,来产生和订阅(即观察)在应用程序中发生的事件。使用 javax.enterprise.event.Event 类创建事件,并使用 CDI 的 @Observes 标注订阅处理事件。
事件对象只不过是一个具体的Java类的实例。
一个事件可指定限定符,观察者可以区别于其他相同类型的事件。
限定符的功能很像主题选择器, 允许限定符决定观察器将观察哪些事件。
使用@ qualifier定义的一个例子:
@Qualifier @Target({METHOD, FIELD, PARAMETER, TYPE}) @Retention(RUNTIME) public @interface Updated {}另外,事件的创建和订阅是类型安全的.
一个观察者的处理方式是在方法中,加入一个参数注解@Observes.如下所示:
public void onAnyDocumentEvent(@Observes Document document) { ... }带注解的参数称为事件参数。事件的参数类型是观察到的事件类型。事件参数还可以指定限定符。如下:
public void afterDocumentUpdate(@Observes @Updated Document document) { ... }当然也可以有其他参数
public void afterDocumentUpdate(@Observes @Updated Document document, User user) { ... }
Event producers的fire事件是使用参数化Event interface的实例.如下,通过@Inject注入该接口的一个实例.
@Inject @Any Event<Document> documentEvent;而事件生产者通过调用fire()方法,并传递"事件对象"从而激活事件处理.
documentEvent.fire(document);通过事件对象的参数值,容器调用所有观察者的方法,如果任何观察者方法抛出一个异常,容器会停止调用观察者方法,异常将会由fire()方法抛出。
@Inject @Updated Event<Document> documentUpdatedEvent;
public void afterDocumentUpdate(@Observes @Updated Document document) { ... }
注解注入的缺点是,我们不能动态地指定限定符。
CDI也考虑到了这一点.
documentEvent.select(new AnnotationLiteral<Updated>(){}).fire(document);
documentEvent注入点不用再使用限定符 @Updated. 这样可以在程序中判断后进行分支处理.
if(num==1){ documentEvent.select(new AnnotationLiteral<Updated>(){}).fire(document); }else{ documentEvent.select(new AnnotationLiteral<Other>(){}).fire(document); }
事件可以有多个事件限定符,通过select()方法可以使用任意的注解组合在事件注入点和限定符实例上.
默认情况下,在当前上下文如果没有一个观察者的实例,容器将为事件实例化观察者.
但我们希望传递给观察者的实例是已经存在于上下文中的观察者.
指定一个有条件的观察者的方式是在@Observes注释上添加receive = IF_EXISTS
public void refreshOnDocumentUpdate(@Observes(receive = IF_EXISTS) @Updated Document d) { ... }Note
@Qualifier @Target({METHOD, FIELD, PARAMETER, TYPE}) @Retention(RUNTIME) public @interface Role { RoleType value(); }可以通过注解的value值传递信息给observer.
public void adminLoggedIn(@Observes @Role(ADMIN) LoggedIn event) { ... }在事件注入点的使用
@Inject @Role(ADMIN) Event<LoggedIn> loggedInEvent;在AnnotationLiteral方式中的使用:
abstract class RoleBinding extends AnnotationLiteral<Role> implements Role {}通过select()方法的使用代码
documentEvent.select( new RoleBinding() {public void value() { return user.getRole(); }} ).fire(document);
qualifiers 是可以多重组合的.如下代码:
@Inject @Blog Event<Document> blogEvent; ... if (document.isBlog()) blogEvent.select(new AnnotationLiteral<Updated>(){}).fire(document);下面所有这些观察方法将得到通知。
public void afterBlogUpdate(@Observes @Updated @Blog Document document) { ... } public void afterDocumentUpdate(@Observes @Updated Document document) { ... } public void onAnyBlogEvent(@Observes @Blog Document document) { ... } public void onAnyDocumentEvent(@Observes Document document) { ... }}}然而,如果还有一个观察者的方法:
public void afterPersonalBlogUpdate(@Observes @Updated @Personal @Blog Document document) { ... }它不会通知,因为@Personal并未包含在事件发生处.
事务处理的observers 在事务完成之前或之后的阶段才会收到事件通知.
例如,下面的观察方法需要在应用程序上下文中刷新一个查询的结果集,但是只有在 Category 更新成功才会执行:
public void refreshCategoryTree(@Observes(during = AFTER_SUCCESS) CategoryUpdateEvent event) { ... }一共有五种transactional observers:
在一个有状态的对象模型(stateful object model)中,Transactional observers是非常重要的.因为那些状态经常是长事务的.
想象一下,我们已经在application scope范围缓存一个JPA查询,
import javax.ejb.Singleton; import javax.enterprise.inject.Produces; @ApplicationScoped @Singleton public class Catalog { @PersistenceContext EntityManager em; List<Product> products; @Produces @Catalog List<Product> getCatalog() { if (products==null) { products = em.createQuery("select p from Product p where p.deleted = false").getResultList(); } return products; } }
如果一个产品被创建或删除,我们需要重新整理产品目录,这个时候我们必须要等到这个更新的事务成功完成后.
创建和删除产品的Bean可以引发事件,例如:import javax.enterprise.event.Event; @Stateless public class ProductManager { @PersistenceContext EntityManager em; @Inject @Any Event<Product> productEvent; public void delete(Product product) { em.delete(product); productEvent.select(new AnnotationLiteral<Deleted>(){}).fire(product); } public void persist(Product product) { em.persist(product); productEvent.select(new AnnotationLiteral<Created>(){}).fire(product); } ... }
在事务完成后,对产品目录用观察者的方法进行更新/删除
import javax.ejb.Singleton; @ApplicationScoped @Singleton public class Catalog { ... void addProduct(@Observes(during = AFTER_SUCCESS) @Created Product product) { products.add(product); } void removeProduct(@Observes(during = AFTER_SUCCESS) @Deleted Product product) { products.remove(product); } }
概述流程:
我在这里也是实际阐释一下.毕竟国内CDI方面的东西基本没有,也给学习CDI的朋友一个参考.
run.java
import static java.lang.annotation.RetentionPolicy.RUNTIME; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.Target; import javax.inject.Qualifier; @Qualifier @Target({ ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD }) @Retention(RUNTIME) @Documented public @interface Run { }walk.java
import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.Target; import javax.inject.Qualifier; import static java.lang.annotation.RetentionPolicy.RUNTIME; @Qualifier @Target({ ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD }) @Retention(RUNTIME) @Documented public @interface Walk { }定义好后,我们需要定义具体的事件处理的主题.也就是运动.不管是跑还是走,都是运动的一种.所以定义运动事件主体.
其实主要是因为在这里是自己想的一个CDI EVENT的场景,
import java.util.Date; public class ExerciseEvent { private String type; //walk or run private Long howfar; private Date datetime; public String getType() { return type; } public void setType(String type) { this.type = type; } public Long getHowfar() { return howfar; } public void setHowfar(Long howfar) { this.howfar = howfar; } public Date getDatetime() { return datetime; } public void setDatetime(Date datetime) { this.datetime = datetime; } @Override public String toString() { return "在"+this.datetime+",你"+this.type+"--"+(this.howfar.toString()); } }不忙处理页面,这个时候,我们应该对走还是跑做具体的处理.
分析一下,cdi的event处理,主要是2个,一个ob一个producer.现在,我们已经定义好了event.那么接着就是先处理observer.
public class ExerciseHandler implements Serializable{ private static final long serialVersionUID = 3245934049396896828L; @Inject private Logger log; List<ExerciseEvent> exercise=new ArrayList<ExerciseEvent>(); public void run(@Observes @Run ExerciseEvent runEvent){ log.info("CDI---run方法!"); this.exercise.add(runEvent); } public void walk(@Observes @Walk ExerciseEvent walkEvent){ log.info("CDI---walk方法!"); this.exercise.add(walkEvent); } @Produces @Named public List<ExerciseEvent> getExercise() { return exercise; } }
相关语法说明,如果看了上面翻译自jboss的文档的说明外,应该也就明白这么的代码意思.
这个类的run方法将会在系统观察到有地方触发了限定符为@run,并且事件是ExerciseEvent的方法.就会去执行这个方法.\
这里就要继续写producer的相关类了.本例大家可以知道,是由页面触发的相关Exercise事件.run or walk.先是一个页面.
JSF页面,大家可以看到h:dateTable.在看看上面的 @Produces注解.就上面OB定义里的最后一段代码.
<?xml version="1.0" encoding="UTF-8"?> <ui:composition xmlns="http://www.w3.org/1999/xhtml" xmlns:ui="http://java.sun.com/jsf/facelets" xmlns:h="http://java.sun.com/jsf/html" xmlns:f="http://java.sun.com/jsf/core" template="/WEB-INF/templates/template.xhtml"> <ui:define name="content"> <h:form> <h:outputLabel value="Far:" /> <h:inputText value="#{exerciseBean.far}" /> <h:selectOneRadio value="#{exerciseBean.type}" required="true"> <f:selectItem itemLabel="Run" itemValue="run" /> <f:selectItem itemLabel="Walk" itemValue="walk" /> </h:selectOneRadio> <h:commandButton value="Go!!!" action="#{exerciseBean.process()}" /> </h:form> <h:dataTable var="exercise" value="#{exercise}" styleClass="zebra-striped"> <h:column> <f:facet name="header">Date</f:facet> <h:outputText value="#{exercise.datetime}"> <f:convertDateTime type="date" pattern="yyyy/MM/dd" /> </h:outputText> </h:column> <h:column> <f:facet name="header">type</f:facet> <h:outputText value="#{exercise.type}" /> </h:column> <h:column> <f:facet name="header">howfar</f:facet> <h:outputText value="#{exercise.howfar}" /> </h:column> </h:dataTable> </ui:define> </ui:composition>对应的backingBean代码:
@Named @SessionScoped public class ExerciseBean implements Serializable{ private static final long serialVersionUID = -2164098635097534027L; @Inject private Logger log; @Inject @Run Event<ExerciseEvent> runEventProducer; @Inject @Walk Event<ExerciseEvent> walkEventProducer; private String type="run"; private Long far; private ExerciseEvent event=new ExerciseEvent(); public void process(){ event.setType(this.type); event.setHowfar(far); event.setDatetime(new Date()); if(this.event.getType().equals("run")){ log.info("Run--Fire"); this.runEventProducer.fire(event); }else{ log.info("Walk--Fire"); this.walkEventProducer.fire(event); } } public ExerciseEvent getEvent() { return event; } public void setEvent(ExerciseEvent event) { this.event = event; } public Event<ExerciseEvent> getRunEventProducer() { return runEventProducer; } public void setRunEventProducer(Event<ExerciseEvent> runEventProducer) { this.runEventProducer = runEventProducer; } public Event<ExerciseEvent> getWalkEventProducer() { return walkEventProducer; } public void setWalkEventProducer(Event<ExerciseEvent> walkEventProducer) { this.walkEventProducer = walkEventProducer; } public String getType() { return type; } public void setType(String type) { this.type = type; } public Long getFar() { return far; } public void setFar(Long far) { this.far = far; } }启动页面后,点击按钮,选择不同的 radio方式 ,run或者walk,我就不截图了.
发一点后台的输出:
16:33:01,899 INFO (http-/0.0.0.0:8080-1) Walk--Fire
16:33:01,901 INFO (http-/0.0.0.0:8080-1) CDI---walk方法!
16:51:55,712 INFO (http-/0.0.0.0:8080-1) Walk--Fire
16:51:55,713 INFO (http-/0.0.0.0:8080-1) CDI---walk方法!
16:52:54,044 INFO (http-/0.0.0.0:8080-1) Run--Fire
16:52:54,045 INFO (http-/0.0.0.0:8080-1) CDI---run方法!