EJB 3.1 Timer Services: cosa bolle di nuovo in pentola?

Se EJB 3 è stata una rivoluzione, si può dire che EJB 3.1 è una ben accetta evoluzione. Anche se ancora non abbiamo mai affrontato l’argomento in modo analitico, in altre occasioni avevamo già sottolineato che la nuova specifica EJB 3.1 tende ad una ulteriore semplificazione della piattaforma enterprise, introducendo miglioramenti che vengono incontro alla già difficile vita di noi sviluppatori…

Tra le varie cose, anche i Timer Services hanno subito qualche miglioramento atteso rispetto alla versione 3.0, di cui avevamo già avuto modo di imparare in un post precedente. Vediamo di che si tratta.

I limiti del Super Saiyan 3

Nel post precedente non ci siamo fermati a puntualizzare alcuni difetti che emergono dall’uso dei servizi di timer, ma visto che la nuova specifica li ha risolti vale la pena capire quali siano questi miglioramenti. A prima vista, i due limiti della specifica 3 erano:

  • configurazione del timer solo tramite date o offset in millisecondi: se vogliamo per esempio che un certo evento parta ogni Lunedì, è necessario effettuare una serie di calcoli espressi in millisecondi o in date per capire quando farlo scadere la prima volta e le volte successive;
  • inizializzazione programmatica: l’unico modo per inizializzare un timer è chiamare esplicitamente uno dei metodi dell’interfaccia TimerService. Come fare quindi ad avviare un timer all’avvio dell’applicazione senza richiedere l’interazione dell’utente? Una soluzione, se non l’unica, potrebbe essere quella di usare un ServletContextListener, anche se non è molto elegante tirare su un modulo web solo per avviare dei servizi del modulo EJB…

Vediamo come queste limitazioni sono state risolte con la specifica 3.1.

Pianificazione di un timer basata su calendario

Oltre ai metodi di creazione programmatica di un timer basata su date o millisecondi della specifica 3.0, la nuova specifica 3.1 introduce la possibilità di pianificare un timer basandosi sul calendario, con una sintassi ben nota agli utilizzatori dello scheduler cron di UNIX. L’API dei Timer Services si arricchisce così di 2 oggetti:

ScheduleExpression
è la classe principale, è più complessa, responsabile della configurazione del timer basata sul calendario. Gli attributi e i valori che essi possono assumere sono riassunti nella seguente tabella:
Attributo Descrizione Valore di Default Valori Ammessi

second

Uno o più secondi all’interno di un minuto

0

0 fino a 59. Per esempio: second="30".

minute

Uno o più minuti all’interno dell’ora

0

0 fino a 59. Per esempio: minute="15".

hour

Uno o più ore all’interno del giorno

0

0 fino a 23. Per esempio: hour="13".

dayOfWeek

Uno o più giorni della settimana

*

0 fino a 7 (sia 0 che 7 possono riferirsi a Domenica). Per esempio: dayOfWeek="3".

Sun, Mon, Tue, Wed, Thu, Fri, Sat. Per esempio: dayOfWeek="Mon".

dayOfMonth

Uno o più giorni del mese

*

1 fino a 31.
Per esempio: dayOfMonth="15".

–7 fino a –1 (un numero negativo significa l’i-esimo giorno o giorni prima la fine del mese).
Per esempio: dayOfMonth="–3".

Last. Per esempio: dayOfMonth="Last".

[1st, 2nd, 3rd, 4th, 5th, Last] [Sun,
Mon, Tue, Wed, Thu, Fri, Sat].
Per esempio: dayOfMonth="2nd Fri".

month

Uno o più mesi dell’anno

*

1 fino a 12.
Per esempio: month="7".

Jan, Feb, Mar, Apr, May,
Jun, Jul, Aug, Sep, Oct, Nov, Dec. Per esempio: month="July".

year

Un anno particolare

*

L’anno è formato da 4 caratteri. Per esempio: year="2011".

timezone

Localizzazione del calendario

Valore di default della JVM

start

E’ possible indicate la data di inizio

Popolato al momento della creazione del timer

end

Data cancellazione timer

Gli attributi possono essere popolati con:

  • Valori singoli:

    second = "10", month = "Sep";

  • Wildcard: sono tutti i valori possibili per un attributo.

    second = "*", month = "*";

  • Range: intervallo inclusivo dei valori ammessi per un certo attributo, separati da un trattino (-).

    second = "1-10", dayOfWeek = "Fri-Mon", dayOfMonth = "27-3" (equivalente a "27-Last , 1-3");

  • Liste: serie di valori singoli o range separati da virgola.

    second = "10, 20, 30", month = "Jan-Jul, Sep-Dec";

  • Incrementi: con una barra (/) si separa un momento iniziale da un intervallo. Nel caso di x/y, si indica il valore y-esimo ammesso per un certo attributo a partire dal momento x.

    minute = "*/5", hour="*" (ogni 5 minuti per ogni ora), second = "30/10" (ogni 10 secondi a partire dal 30°, equivalente a 30,40,50).

TimerConfig
Estende la configurazione dei timer a singola azione e a intervalli durante la loro creazione. In esso è possibile specificare:
  • l’oggetto info (serializzabile) da passare al metodo di callback al timeout;
  • indicare se il timer deve essere persistito o meno. Ciò significa che nel caso in cui il server non sia attivo al momento del timeout, il task schedulato verrà eseguito non appena il server sarà nuovamente attivo.

Questi oggetti vengono passati come parametri ai nuovi metodi dell’interfaccia TimerService:

Timer createCalendarTimer(ScheduleExpression schedule);
Timer createCalendarTimer(ScheduleExpression schedule, TimerConfig timerConfig);
Timer createIntervalTimer(Date initialExpiration, long intervalDuration, TimerConfig timerConfig);
Timer createIntervalTimer(long initialDuration, long intervalDuration, TimerConfig timerConfig);
Timer createSingleActionTimer(Date expiration, TimerConfig timerConfig);
Timer createSingleActionTimer(long duration, TimerConfig timerConfig); 

I nomi dei metodi sono abbastanza eloquenti e non richiedono grosse spiegazioni…

Per non perdere il vizio, giusto due righe di codice…

Vediamo adesso con un esempio banale come schedulare due timer: uno basato su calendario e l’altro tramite TimeConfig:

@Stateless
public class EJB31TaskScheduler implements EJB31TaskSchedulerLocal {

   private static final Logger logger = Logger
         .getLogger(EJB31TaskScheduler.class);

   @Resource
   private TimerService timerService;

   @Override
   public void scheduleCalendarTimer() {
      ScheduleExpression scheduleExpression = new ScheduleExpression();
      scheduleExpression.second("*/5").minute("1-59").hour("*");

      this.timerService.createCalendarTimer(scheduleExpression);

      logger.info("Scheduling task every 5 seconds in any minutes and hours");
   }

   @Override
   public void scheduleSingleActionTimerWithConfig() {
      TimerConfig config = new TimerConfig();
      config.setInfo("Executing single action timer");
      config.setPersistent(false);
      
      this.timerService.createSingleActionTimer(2000, config);
      
      logger.info("Scheduling single action timer");
   }

   @Timeout
   public void doTask(Timer timer) {
      if (timer.getInfo() != null)
         logger.info(timer.getInfo());
      else
         logger.info("Calendar-based task executed");
   }

   @Override
   public void cancelAllTimers() {
      Collection timers = this.timerService.getTimers();
      logger.info("Cancelling " + timers.size() + " timer/s");

      for (Timer timer : timers) {
         timer.cancel();
      }
   }
}
  • Il metodo scheduleCalendarTimer() istanzia un timer usando in modo misto le notazioni disponibili: crea un timer ogni 5 secondi per ogni minuto (ovvero "1-59") di ogni ora (indicata con "*"). La vera differenza con i timer visti fino adesso è che il timer non scadrà ogni 5 secondi da quando viene lanciato, ma esattamente il , 10°, 15°, 20°, ecc… secondo di ogni minuto per ogni ora. Fatto questo, vi comunico che abbiamo appena creato un timer infinito!! Come mai? Perché gli attributi oltre le ore hanno default "*"!! Per interromperlo basta quindi chiamare il metodo cancelAllTimers(), altrimenti vi troverete il timer attivo anche dopo il riavvio del server (ricordate che sono persistenti di default?).
  • Come si capisce dal nome, il metodo scheduleSingleActionTimerWithConfig() istanzia un timer one-shot tramite l’oggetto TimerConfig, passando un parametro alla funzione di callback doTask(Timer timer) e specificando che non dovrà essere persistito. Da notare che quest’ultimo metodo non ha l’annotazione @Override: a differenza della specifica 3, il metodo di timeout può non essere dichiarato nell’interfaccia di business (può anche essere non pubblico). Questo metodo infatti non dovrebbe essere mai chiamato direttamente!

Configurazione automatica

La grande novità della nuova specifica 3.1 riguarda la possibilità di configurare in modo dichiarativo un timer basato su calendario tramite semplici annotazioni:

  1. al momento del deploy, se un metodo di un EJB viene annotato con @Schedule o @Schedules, viene configurato in modo automatico. Non c’è più bisogno quindi di inizializzazioni tramite ServletContextListener per esempio.
  2. a differenza dei timer inizializzati in modo programmatico, ogni metodo annotato è automaticamente un metodo di callback chiamato allo scadere del timer: posso quindi avere più callback all’interno dello stesso EJB.

@Schedule si configura e si comporta come un timer istanziato con ScheduleExpression: oltre ai suoi attributi ha anche persistent e info che già conosciamo da TimerConfig.

E’ possibile quindi pianificare per esempio un batch notturno che parta alle 23 prima del week-end in questo modo:

@Schedule(dayOfWeek="Fri", hour="23")
public void nightlyVampire() { 
     logger.info("We suck young blood");
}

Conclusioni

Il sistema di schedulazine offerto da Java EE 6 è notevolmente migliorato rispetto ai suoi predecessori grazie alle due caratteristiche descritte. La pianificazione basata su calendario evita di calcolare a mano le date e gli intervalli come era necessario fare fino ad oggi e la possibilità di configurare un timer anche in modo automatico rende la loro inizializzazione sempre più immediata.

Andrea Como

Sono un software engineer focalizzato nella progettazione e sviluppo di applicazioni web in Java. Presso OmniaGroup ricopro il ruolo di Tech Leader sulle tecnologie legate alla piattaforma Java EE 5 (come WebSphere 7.0, EJB3, JPA 1 (EclipseLink), JSF 1.2 (Mojarra) e RichFaces 3) e Java EE 6 con JBoss AS 7, in particolare di CDI, JAX-RS, nonché di EJB 3.1, JPA2, JSF2 e RichFaces 4. Al momento mi occupo di ECM, in particolar modo sulla customizzazione di Alfresco 4 e sulla sua installazione con tecnologie da devops come Vagrant e Chef. In passato ho lavorato con la piattaforma alternativa alla enterprise per lo sviluppo web: Java SE 6, Tomcat 6, Hibernate 3 e Spring 2.5. Nei ritagli di tempo sviluppo siti web in PHP e ASP. Per maggiori informazioni consulta il mio curriculum pubblico. Follow me on Twitter - LinkedIn profile - Google+

  • nike

    utilizzo questo statement
    @Schedule(hour=”*/1″, minute=”01″, second=”0″, persistent=false)
    per schedulare una procedura, mentre in ambiente di sviluppo funziona, quando cerco di fare il deploy sul server di produzione mi da degli errori di tabelle non trovate… come mai?
    Grazie
    Nike

    • Ciao,
      i timer sono basati su alcune tabelle (4 se non ricordo male) sulle quali vengono salvati gli ejb timer schedulati e su cui l’application server fa polling per determinare quando lanciarli. L’implementazione dipende dall’application server. Con WebSphere per esempio si può impostare un particolare datasource per gestire gli ejb timer, con JBoss 7 non sono mai sceso nel dettaglio, ma sembra che vengano create delle tabelle su file sysyem nella cartella “data” del profilo del server. Che application server usi? Possibile che sia stato configurato in modo particolare?