SeamFramework.orgCommunity Documentation
Seam semplifica molto l'esecuzione di lavori asincroni da una richiesta web. Quando la maggior parte delle persone pensa all'asincronicità in Java EE, pensa all'uso di JMS. Questo è certamente un modo per approcciare il problema in Seam, ed è quello giusto quando si hanno dei requisiti di qualità di servizio molto stringenti e ben definiti. Seam semplifica l'invio e la ricezione di messaggi JMS usando i componenti Seam.
Ma per molti casi d'uso, JMS è eccessivo. Seam aggiunge come nuovo layer un semplice metodo asincrono ed un meccanismo d'eventi nella scelta del dispatcher:
java.util.concurrent.ScheduledThreadPoolExecutor
(di default)
Il servizio EJB timer (per ambienti EJB 3.0)
Quartz
Eventi asincroni e chiamate a metodi hanno le stesse aspettative di qualità di servizio del meccanismo sottostante di dispatcher. Il dispatcher di default, basato su ScheduledThreadPoolExecutor
opera efficientemente ma non fornisce alcun supporto ai task asincroni di persistenza, e quindi non garantisce che un task verrà effettivamente eseguito. Se si lavora in un ambiente che supporta EJB 3.0 e si aggiunge la seguente linea a components.xml
:
<async:timer-service-dispatcher/>
allora i task asincroni verranno processati dal servizio EJB timer del container. Se non si è familiari come il servizio Timer, nessuna paura, non si deve interagire direttamente con esso per usare i metodi asincroni in Seam. La cosa importante da sapere è che qualsiasi buona implementazione di EJB 3.0 avrà l'opzione di usare i timer di persistenza, che danno garanzia che i task verranno processati.
Un'altra alternativa è quella di usare la libreria open source Quartz per gestire il metodo asincrono. Occorre incorporare il JAR della libreria Quartz (che si trova nella directory lib
) nell'EAR e dichiararla come modulo Java in application.xml
. Il dispatcher Quartz può essere configurato aggiungendo un file di proprietà Quartz al classpath. Deve essere nominato seam.quartz.properties
. Inoltre occorre aggiungere la seguente linea a components.xml
per installare il dispatcher Quartz.
<async:quartz-dispatcher/>
L'API di Seam per il ScheduledThreadPoolExecutor
di default, il Timer
EJB3, e lo Scheduler
Quartz sono più o meno la stessa cosa. Vengono azionati come "plug and play" aggiungendo una linea a components.xml
.
Nella forma più semplice, una chiamata asincrona consente che una chiamata di metodo venga processata asincronicamente (in un thread differente) dal chiamante. Solitamente si usa una chiamata asincrona quando si vuole ritornare una risposta immediata al client, e si lascia in background il lavoro dispendioso da processare. Questo pattern funziona molto bene nelle applicazioni che usano AJAX, dove il client può automaticamente interrogare il server per ottenere il risultato del lavoro.
Per i componenti EJB si annota l'interfaccia locale per specificare che un metodo viene processato asincronicamente.
@Local
public interface PaymentHandler
{
@Asynchronous
public void processPayment(Payment payment);
}
(Per i componenti JavaBean, se si vuole, si può annotare la classe d'implementazione del componente.)
L'uso dell'asincronicità è trasparente alla classe bean:
@Stateless
@Name("paymentHandler")
public class PaymentHandlerBean implements PaymentHandler
{
public void processPayment(Payment payment)
{
//fai qualche lavoro!
}
}
E è anche trasparente al client:
@Stateful
@Name("paymentAction")
public class CreatePaymentAction
{
@In(create=true) PaymentHandler paymentHandler;
@In Bill bill;
public String pay()
{
paymentHandler.processPayment( new Payment(bill) );
return "success";
}
}
Il metodo asincrono viene processato in un contesto eventi completamente nuovo e non ha accesso allo stato del contesto sessione o conversazione del chiamante. Comunque, il contesto di processo di business viene propagato.
Le chiamate del metodo asincrono possono essere schedulate per un'esecuzione successiva usando le annotazioni @Duration
, @Expiration
e @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";
}
}
Entrambi il server ed il client possono accedere all'oggetto Timer
associato all'invocazione. L'oggetto Timer
mostrato sotto è il timer EJB3 quando viene usato il dispatcher EJB3. Per il ScheduledThreadPoolExecutor
di default, l'oggetto restituito è Future
di JDK. Per il dispatcher Quartz, viene ritornato QuartzTriggerHandle
, che verrà discusso nella prossima sessione.
@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)
{
//fai qualche lavoro!
return timer; //notare che il valore di ritorno viene completamente ignorato
}
}
@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";
}
}
I metodi asincroni non possono ritornare al chiamante alcun altro valore.
Il dispatcher Quartz (vedere indietro come viene installato) consente di usare le annotazioni @Asynchronous
, @Duration
, @Expiration
, e @IntervalDuration
come sopra. Ma possiede anche altre potenti caratteristiche. Il dispatcher Quartz supporta tre nuove annotazioni.
L'annotazione @FinalExpiration
specifica una data finale per il task ricorrente. Si noti che si può iniettare il QuartzTriggerHandle
.
@In QuartzTriggerHandle timer;
// Definisce il metodo nel componente "processor"
@Asynchronous
public QuartzTriggerHandle schedulePayment(@Expiration Date when,
@IntervalDuration Long interval,
@FinalExpiration Date endDate,
Payment payment)
{
// esegui il task ripetitivo o long running fino a endDate
}
... ...
// Schedula il task nel codice che processa la business logic
// Inizia adesso, ripete ogni ora, e finisce il 10 Maggio 2010
Calendar cal = Calendar.getInstance ();
cal.set (2010, Calendar.MAY, 10);
processor.schedulePayment(new Date(), 60*60*1000, cal.getTime(), payment);
Si noti che il metodo restituisce l'oggetto QuartzTriggerHandle
, che si può usare per arrestare, mettere in pausa e ripristinare lo scheduler. L'oggetto QuartzTriggerHandle
è serializzabile, e quindi può essere salvato nel database se deve essere presente per un periodo di tempo esteso.
QuartzTriggerHandle handle =
processor.schedulePayment(payment.getPaymentDate(),
payment.getPaymentCron(),
payment);
payment.setQuartzTriggerHandle( handle );
// Salva il pagamento nel DB
// pi� tardi ...
// Recupera il pagamento dal DB
// Cancella i rimanenti task schedulati
payment.getQuartzTriggerHandle().cancel();
L'annotazione @IntervalCron
supporta la sintassi Unix del cron job per la schedulazione dei task. Per esempio, il seguente metodo asincrono gira alle 2:10pm e alle 2:44pm ogni Mercoledì del mese di Marzo.
// Define the method
@Asynchronous
public QuartzTriggerHandle schedulePayment(@Expiration Date when,
@IntervalCron String cron,
Payment payment)
{
// esegui il task ripetitivo o long running
}
... ...
// Schedula il task nel codice che processa la business logic
QuartzTriggerHandle handle =
processor.schedulePayment(new Date(), "0 10,44 14 ? 3 WED", payment);
L'annotazione @IntervalBusinessDay
supporta l'invocazione sullo scenario di "Giorno Lavorativo ennesimo". Per esempio il seguente metodo asincrono gira alle ore 14.00 del secondo giorno lavorativo di ogni mese. Di default esclude tutti i weekend e le festività federali americane fino al 2010 dai giorni lavorativi.
// Define the method
@Asynchronous
public QuartzTriggerHandle schedulePayment(@Expiration Date when,
@IntervalBusinessDay NthBusinessDay nth,
Payment payment)
{
// esegui il task ripetitivo o long running
}
... ...
// Schedula il task nel codice che processa la business logic
QuartzTriggerHandle handle =
processor.schedulePayment(new Date(),
new NthBusinessDay(2, "14:00", WEEKLY), payment);
L'oggetto NthBusinessDay
contiene la configurazione del trigger d'invocazione. Si possono specificare più vacanze (esempio, vacanze aziendali, festività non-US, ecc.) attraverso la proprietà 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;
}
... ...
}
Le annotazioni @IntervalDuration
, @IntervalCron
, e @IntervalNthBusinessDay
sono mutualmente esclusive. Se usate nello stesso metodo, verrà lanciata una RuntimeException
.
Gli eventi guidati dai componenti possono essere asincroni. Per sollevare un evento da processare in modo asincrono, occorre semplicemente chiamare il metodo raiseAsynchronousEvent()
della classe Events
. Per schedulare un evento a tempo, chiamare il metodo raiseTimedEvent()
passando un oggetto schedule (per il dispatcher di default o il dispatcher del servizio timer, usare TimerSchedule
). I componenti possono osservare eventi asincroni nel solito modo, ma si tenga presente che solo il contesto business process viene propagato nel thread asincrono.
Ogni dispatcher asincrono si comporta in modo differente quando attraverso di esso viene propagata un'eccezione. Per esempio, il dispatcher java.util.concurrent
sospenderà ogni altra esecuzione di una chiamata che si ripete, ed il servizio timer EJB3 inghiottirà quest'eccezione. Quindi Seam cattura ogni eccezione che si propaga fuori da una chiamata asincrona prima che questa raggiunga il dispatcher.
Di default ogni eccezione che si propaga fuori da un'esecuzione asincrona verrà catturata e loggata come errore. Si può personalizzare questo comportamento facendo override del componente org.jboss.seam.async.asynchronousExceptionHandler
.
@Scope(ScopeType.STATELESS)
@Name("org.jboss.seam.async.asynchronousExceptionHandler")
public class MyAsynchronousExceptionHandler extends AsynchronousExceptionHandler {
@Logger Log log;
@In Future timer;
@Override
public void handleException(Exception exception) {
log.debug(exception);
timer.cancel(false);
}
}
Per esempio, usando il dispatcher java.util.concurrent
, viene iniettato il suo oggetto di controllo e vengono cancellate tutte le future invocazioni quando si incontra un'eccezione.
Si può alterare questo comportamento per un componente individuale, implementando sul componente il metodo public void handleAsynchronousException(Exception exception);
. Per esempio:
public void handleAsynchronousException(Exception exception) {
log.fatal(exception);
}
Seam facilita l'invio e la ricezione di messaggi JMS da e verso componenti Seam.
Per configurare l'infrastruttura di Seam alla spedizione di messaggi JMS, occorre dire a Seam a quali topic e code si vuole spedire i messaggi, ed inoltre serve dire dove trovare QueueConnectionFactory
e/o TopicConnectionFactory
.
Di default Seam usa UIL2ConnectionFactory
che è la connection factory consueta per l'uso con JBossMQ. Se si impiegano altri provider JSM, occorre impostare uno o entrambi i queueConnection.queueConnectionFactoryJndiName
e topicConnection.topicConnectionFactoryJndiName
in seam.properties
, web.xml
o components.xml
.
Inoltre in components.xml
occorre elencare i topic e le code per installare i TopicPublisher
ed i QueueSender
gestiti da Seam:
<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"/>
Ora è possibile iniettare in qualsiasi componente un TopicPublisher
e un TopicSession
JMS:
@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);
}
}
O lavorando con una coda:
@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);
}
}
I messaggi possono essere processati usando un qualsiasi bean message driven EJB3. I bean message-drive possono essere anche componenti Seam, in qual caso è possibile iniettare altri componenti Seam aventi scope evento e applicazione.
Seam Remoting consente di sottoscrivere un topic JMS lato client JavaScript. Questo viene descritto in Capitolo 25, Remoting.