Seam使得异步执行一个来自Web请求的工作变得非常容易。当大多数人在Java EE里考虑异步时,他们想到用JMS。 在Seam中,这确实是一种解决方案,当你有严格和明确定义的QoS服务需求时,这是正确的。Seam利用Seam组件让发送和接收JMS消息更容易进行。
但是对于多数用例来说,用JMS无异于杀鸡用牛刀。Seam将简单的异步方法和事件应用分层,置于你选择的 dispatchers 之上。
java.util.concurrent.ScheduledThreadPoolExecutor (默认)
EJB Timer Service (针对 EJB 3.0 环境)
Quartz
异步的事件和方法调用与底层的分配机制有着相同的服务期待质量。 基于 ScheduledThreadPoolExecutor 的默认dispatcher执行得很好,但不提供对持久化异步任务的支持,因此不保证一项任务真正会被执行。 如果你在一个支持EJB 3.0的环境中工作,并将下面这一行添加到 components.xml 中:
<async:timer-service-dispatcher/>
那么,你的异步任务将由容器的EJB定时服务处理。 如果你不熟悉Timer服务,也不必担心,如果你想要在Seam中使用异步方法,并不需要与它直接交互。 要了解一件重要的事情:任何好的EJB 3.0实现都将有使用持久化定时器的选择,它为任务最终得到处理提供了一些保证。
另一种选择是使用开源的Quartz库来管理异步的方法。 你要将Quartz库JAR(在 lib 路径中)绑定在你的EAR中,并在 application.xml 中将它声明成一个Java模块。 另外,你还需要将下面的行添加到 components.xml 中来安装Quartz Dispatcher。
<async:quartz-dispatcher/>
Seam的API对于默认的 ScheduledThreadPoolExecutor 的Seam API,及EJB3 Timer与Quartz Scheduler 大体相同。 它们可以只是通过在 components.xml 中添加一行来进行”即插即用(plug and play)“。
最简单的形式,一个异步的调用只是异步地处理来自访问者的方法调用(在不同的线程中)。 当我们要返回一个即时响应给客户端时,通常使用一个异步调用,并让一些费时的工作在后台处理。 此模式在使用AJAX的应用程序中运行良好,在AJAX应用中客户端能够自动地从服务器上获得工作结果。
对于EJB组件,我们在本地接口上进行注解,来指定某个方法要被异步地处理。
@Local public interface PaymentHandler { @Asynchronous public void processPayment(Payment payment); }
(对于JavaBean组件,如果喜欢的话,我们可以注解组件实现类。)
异步的使用对于Bean类来说是透明的:
@Stateless @Name("paymentHandler") public class PaymentHandlerBean implements PaymentHandler { public void processPayment(Payment payment) { //do some work! } }
并且对客户端也是透明的:
@Stateful @Name("paymentAction") public class CreatePaymentAction { @In(create=true) PaymentHandler paymentHandler; @In Bill bill; public String pay() { paymentHandler.processPayment( new Payment(bill) ); return "success"; } }
异步方法在一个全新的事件上下文中处理,而且无法访问调用者的会话或对话上下文状态。 然而,业务流程上下文 得到了 传播。
异步方法调用可以利用 @Duration、@Expiration 和 @IntervalDuration注解为后续的执行定时。
@Local public interface PaymentHandler { @Asynchronous public void processScheduledPayment(Payment payment, @Expiration Date date); @Asynchronous public void processRecurringPayment(Payment payment, @Expiration Date date, @IntervalDuration Long interval)' }
@Stateful @Name("paymentAction") public class CreatePaymentAction { @In(create=true) PaymentHandler paymentHandler; @In Bill bill; public String schedulePayment() { paymentHandler.processScheduledPayment( new Payment(bill), bill.getDueDate() ); return "success"; } public String scheduleRecurringPayment() { paymentHandler.processRecurringPayment( new Payment(bill), bill.getDueDate(), ONE_MONTH ); return "success"; } }
客户端和服务端两者都可以访问与调用相关联的 Timer 对象。当使用EJB3 Dispatcher时, The Timer 对象会显示在下面。对于默认的ScheduledThreadPoolExecutor,返回的是JDK的对象Future。对于Quartz Dispatcher,返回QuartzTriggerHandle,我们会在下部分对此进行讨论。
@Local public interface PaymentHandler { @Asynchronous public Timer processScheduledPayment(Payment payment, @Expiration Date date); }
@Stateless @Name("paymentHandler") public class PaymentHandlerBean implements PaymentHandler { @In Timer timer; public Timer processScheduledPayment(Payment payment, @Expiration Date date) { //do some work! return timer; // 注意返回值被完全忽略 } }
@Stateful @Name("paymentAction") public class CreatePaymentAction { @In(create=true) PaymentHandler paymentHandler; @In Bill bill; public String schedulePayment() { Timer timer = paymentHandler.processScheduledPayment( new Payment(bill), bill.getDueDate() ); return "success"; } }
异步方法不能返回任何其它值给调用者。
Quartz dispatcher(它的安装方法请见前文)允许你使用 @Asynchronous、@Duration、@Expiration 和 @IntervalDuration 注解。但它还有一些其他的强大功能。Quartz dispatcher还支持三种新注解。
@FinalExpiration 注解指定一个重现任务的终止日期。
// Defines the method in the "processor" component @Asynchronous public QuartzTriggerHandle schedulePayment(@Expiration Date when, @IntervalDuration Long interval, @FinalExpiration Date endDate, Payment payment) { // do the repeating or long running task until endDate } ... ... // Schedule the task in the business logic processing code // Starts now, repeats every hour, and ends on May 10th, 2010 Calendar cal = Calendar.getInstance (); cal.set (2010, Calendar.MAY, 10); processor.schedulePayment(new Date(), 60*60*1000, cal.getTime(), payment);
注意该方法返回 QuartzTriggerHandle 对象,你以后可以用它来中止、暂停和恢复定时器。 QuartzTriggerHandle 对象是可序列化的,因此,如果你需要保留更久一点,可以把它存到数据库中。
QuartzTriggerHandle handle = processor.schedulePayment(payment.getPaymentDate(), payment.getPaymentCron(), payment); payment.setQuartzTriggerHandle( handle ); // Save payment to DB // later ... // Retrieve payment from DB // Cancel the remaining scheduled tasks payment.getQuartzTriggerHandle().cancel();
@IntervalCron 注解支持Unix cron语法的任务调度。例如,下面的异步方法在三月份每周三的2:10pm和2:44pm运行。
// Define the method @Asynchronous public QuartzTriggerHandle schedulePayment(@Expiration Date when, @IntervalCron String cron, Payment payment) { // do the repeating or long running task } ... ... // Schedule the task in the business logic processing code QuartzTriggerHandle handle = processor.schedulePayment(new Date(), "0 10,44 14 ? 3 WED", payment);
@IntervalBusinessDay 注解支持在”第n个Business Day“调用。 例如,下面的异步方法在每个月的第2个business day的14:00运行。 默认时,它从business day中排除了2010年之前的所有周末和米国联邦假期。
// Define the method @Asynchronous public QuartzTriggerHandle schedulePayment(@Expiration Date when, @IntervalBusinessDay NthBusinessDay nth, Payment payment) { // do the repeating or long running task } ... ... // Schedule the task in the business logic processing code QuartzTriggerHandle handle = processor.schedulePayment(new Date(), new NthBusinessDay(2, "14:00", WEEKLY), payment);
NthBusinessDay 对象包含调用触发器的配置。 你可以通过 additionalHolidays 属性指定更多的假期(例如,公司假期、非美国的假期等等。)
public class NthBusinessDay implements Serializable { int n; String fireAtTime; List <Date> additionalHolidays; BusinessDayIntervalType interval; boolean excludeWeekends; boolean excludeUsFederalHolidays; public enum BusinessDayIntervalType { WEEKLY, MONTHLY, YEARLY } public NthBusinessDay () { n = 1; fireAtTime = "12:00"; additionalHolidays = new ArrayList <Date> (); interval = BusinessDayIntervalType.WEEKLY; excludeWeekends = true; excludeUsFederalHolidays = true; } ... ... }
@IntervalDuration、@IntervalCron 和 @IntervalNthBusinessDay 注解相互排斥。 如果把它们用在同一个方法中,就会抛出 RuntimeException。
Seam让JMS消息发送到Seam组件和从Seam组件接收变得很容易。
为了给发送JMS消息配置Seam的基础结构,你需要告诉Seam关于任何你想发送消息到的主题(Topic)和队列(Queue),并且也要告诉Seam到哪里寻找 QueueConnectionFactory 和/或 TopicConnectionFactory。
Seam默认使用 UIL2ConnectionFactory,它是使用JBossMQ时常用的连接工厂。 如果你正使用其他的JMS提供者,就需要在 seam.properties、web.xml 或 components.xml 文件中设置一个或两个 queueConnection.queueConnectionFactoryJndiName 和 topicConnection.topicConnectionFactoryJndiName。
你也需要在 components.xml 文件中列出主题(Topic)和队列(Queue),来安装Seam受控的 TopicPublisher 和 QueueSender:
<jms:managed-topic-publisher name="stockTickerPublisher" auto-create="true" topic-jndi-name="topic/stockTickerTopic"/> <jms:managed-queue-sender name="paymentQueueSender" auto-create="true" queue-jndi-name="queue/paymentQueue"/>
现在,你可以注入一个JMS TopicPublisher 和 TopicSession 到任何组件里:
@In private TopicPublisher stockTickerPublisher; @In private TopicSession topicSession; public void publish(StockPrice price) { try { stockTickerPublisher.publish( topicSession.createObjectMessage(price) ); } catch (Exception ex) { throw new RuntimeException(ex); } }
或用来同Queue一起使用
@In private QueueSender paymentQueueSender; @In private QueueSession queueSession; public void publish(Payment payment) { try { paymentQueueSender.send( queueSession.createObjectMessage(payment) ); } catch (Exception ex) { throw new RuntimeException(ex); } }
你可以利用任何EJB3消息驱动Bean来处理消息。 消息驱动Bean甚至可以是Seam组件,在这种情况下,它可能注入其他事件和应用程序作用域的Seam组件。