Zeitgesteuerte Jobs in Java / JEE (EJB-Timer und Quartz-Scheduler)

Problemstellung:

Da wir für eines unserer Projekte zeitgesteuerte Prozesse benötigen und ich gerade am Architekturkonzept schreibe, habe ich verschiedene Technologien evaluiert. Dabei bin ich auf zwei verschiedene Frameworks gestoßen:

  • EJB-Timer
  • Quartz-Scheduler

Die Anforderung ist:

Zuverlässig in einem bestimmten Intervall (z.B. täglich um 23.30 Uhr oder an jedem Jahresende) eine Reihe von Prozessen auszuführen. Die Lösung muss mit Java 7 auf dem JBoss 7.0.2 deployed werden.

 

EJB-Timer:

Vorteil

Um den EJB-Timer nutzen zu können müssen keine externen JAR-Dateien gesucht werden, da alles im javax.ejb-Packet enthalten ist. Die erforderliche JAR-Datei ist im JBoss integriert und im Eclipse-Projekt muss lediglich die JBoss 7.0 Runtime Bibliothek hinzugefügt werden.

Nachteil

Dafür musste ich leider feststellen, dass der „jboss-as-7.0.2.Final“ den <Timer-Service> nicht aktiviert hat. Um dies zu tun muss man die „Standalone-preview.xml“ laden. Dafür muss die standalone.bat (unter Windows) mit folgendem Parameter gestartet werden:

  • standalone.bat -server-config=standalone-preview.xml

Es darf dabei nicht vergessen werden, jegliche Änderung, die in der standalone.xml vorgenommen wurde, jetzt auch nochmal in der standalone-preview.xml einzutragen.

Java-Code

Um den Timer-Service zu nutzen, erstellt man eine EJB. Für die Problemstellung bietet sich ein @Singleton an, der per @Startup ausgeführt wird. Mit @Resource bekommt man den SessionContext, der wiederum über die Methode getTimerService() den gewünschten Service anspricht.

Um einen Timer zu erstellen, kennt der Service die Methode createTimer(Date startZeitpunkt, Long intervallInMillisekunden, Serializable nameDesTimers), mit der ein Timer initialisiert wird.

Nach jedem Intervall läuft der Timer in ein Timeout, für das die EJB eine Methode mit der Annotation @Timeout benötigt. Sie muss public void sein und als Übergabeparameter ein Timer-Objekt besitzen. Diese Methode wird bei jedem Timeout ausgeführt.

Problem

Beim ersten Ausprobieren war ich voller Euphorie als ich sah, dass in meinem eingestellten Intervall eine Nachricht auf der Konsole erschien. Als ich jedoch ein weiteres Mal deployed habe musste ich feststellen, dass der erste Timer immer noch lief, obwohl ich die War-Datei gelöscht, den JBoss beendet und wieder neu hochgefahren habe. Nach weiterer Recherche habe ich vom Timer-Service über die Methode getTimers() jeden aktiven Timer mit cancel() beendet. Trotzdem liefen mittlerweile sehr viele Timer weiter.

Scheinbar speichert der JBoss die seralisierten Timer-Objekte in irgendeinen Cache. Jedenfalls habe ich auch nach längerer Recherche nicht herausgefunden wie und wo die abgespeichert werden, bzw. wie ich sie wieder löschen kann. Falls jemand darüber Bescheid weiß, wäre es nett, wenn er mich kontaktieren würde.

 

Quartz-Scheduler

Vorteil

Für den Quartz-Scheduler werden keine Änderungen in der JBoss-Konfiguration benötigt. Lediglich drei JAR-Dateien müssen dem Projekt beigefügt werden.

  • Quartz-all-2.1.3.jar
  • Slf4j-api-1.6.1.jar
  • Slf4j-log4j12-1.6.1.jar

Diese findet man auf der offiziellen Seite quartz-scheduler.org.

Java-Code

Um den Quartz-Scheduler zu nutzen, benötigt man drei Objekte. Einen Job der ausgeführt werden soll, einen Trigger, der sagt wann und in welchem Intervall der Job ausgeführt werden soll und einen Scheduler, in dem der Job einem Trigger zugewiesen und gestartet wird. Für die Implementierung bietet sich eine @Singleton @Startup EJB an, die beim initialisieren (@PostConstruct) die benötigten Objekte erstellt und den Scheduler startet.

Job

Man benötigt eine JavaKlasse, die das Interface org.quartz.Job implementiert und folgende Methode überschreibt:

  • public void execute(JobExecutionContext context) throws JobExecutionException

In dieser Methode kann beliebiger Java-Code stehen, der zeitgesteuert ausgeführt werden soll.

Dieser Job muss mit einem JobDetail-Objekt verbunden werden:

JobDetail jobHolidayTaken = JobBuilder.newJob(HolidayTakenJob.class)
.withIdentity("HolidayTakenJob", "group1")
.build();

Der wird benötigt, da dem Scheduler ein Trigger zusammen mit einem JobDetail-Objekt übergeben wird.

Trigger

Der Trigger dient dazu, ein Intervall und einen Startzeitpunkt zu definieren, in dem ein Job ausgeführt werden soll. Es gibt verschiedene Arten von Triggern. Ich habe den SimpleTrigger und den CronTrigger versucht, wobei dem SimpleTrigger ein Startzeitpunkt und ein Intervall in Millisekunden übergeben werden. Der CronTrigger ist für unsere Problemstellung interessanter, da er über seine CronExpression-language mit dem Kalender klar kommt. Wenn man z.B. an jedem Monatsende einen bestimmten Prozess ausführen will, kommt man mit einem Millisekunden-intervall ohne zusätzliche Logik nicht aus. Die Syntax der CronExpression-language macht das zu einem Kinderspiel:

http://www.quartz-scheduler.org/documentation/quartz-1.x/tutorials/crontrigger

Hinweis: Der Link referenziert auf die alte Version 1.x. Ich habe diese gewählt, da dort die Syntax der CronExpression-language sehr schön dargestellt ist. Für weitere Beispiele gibts auf der Seite den Menüpunkt Quartz 2.1.x (2 Einträge weiter oben).

Beispiel:

Calendar startHolidayTaken = new GregorianCalendar();
startHolidayTaken.set(2012, 1, 2, 16, 45, 0);

CronTrigger crontriggerHolidayTaken = TriggerBuilder
.newTrigger()
.startAt(startHolidayTaken.getTime())
.withIdentity("cronHolidayTaken")
.withSchedule(CronScheduleBuilder.cronSchedule("0 30 23 * * * *"))
.build();

Dieser Trigger wird täglich um 23:30 Uhr in ein Timeout laufen und den Job ausführen, den er im Scheduler zugewiesen bekommt.

Scheduler

Der Scheduler ist so etwas wie der Container, in dem sich die Trigger und die Jobs befinden. Der Scheduler verbindet einen Job mit einem Trigger und sorgt dafür, dass beim Timeout der Job ausgeführt wird.

Beispiel:

Scheduler scheduler = new StdSchedulerFactory().getScheduler();
scheduler.start();
scheduler.scheduleJob(jobHolidayTaken, crontriggerHolidayTaken);

Fazit

Ich habe die Evaluierung mit dem EJB-Timer angefangen, da wir sowieso einen EJB-Container in unserem JBoss haben und ich kein weiteres Framework einbinden wollte.

Da ich mich allerdings einen ganzen Tag mit dem EJB-Timer beschäftigt habe und zu keinem zufriedenstellenden Ergebnis gekommen bin (wie schon erwähnt: Ich würde mich über andere Erfahrungen darüber sehr freuen), habe ich mich für den Quartz-Scheduler entschieden, den ich innerhalb kurzer Einarbeitungszeit so zum Laufen bekommen habe, wie ich es brauchte.

2 Comments

  1. Marcus

    Hallo,

    die Persistenz der EJB Timer kann z.B. durch das Attribut persistent an der @Schedule Annotation deaktiviert werden. Für die programmatische Erzeugung von nicht-persitenten Timern müssen die FactoryMethoden der TimerService benutzt werden, die eine TimerConfig als Parameter enthalten. Durch TimerConfig.setPersistent(false) wird die Persistenz deaktiviert.

    Vom Quartz Scheduler oder auch eigene gestartete Threads haben den Nachteil, dass sie nicht einfach so auf die vom ApplicationServer verwalteten Ressourcen (Datasoruces, ThreadLocals, zugreifen können).

    Siehe auch:
    http://docs.oracle.com/javaee/6/api/javax/ejb/TimerService.html#createIntervalTimer(java.util.Date, long, javax.ejb.TimerConfig)
    http://www.adam-bien.com/roller/abien/entry/simplest_possible_ejb_3_16
    und
    http://stackoverflow.com/questions/9078241/how-do-i-create-a-non-persistent-ejb-3-1-timer

    Gruß Marcus

  2. Alex

    Hallo Michael,
    erst mal Lob für den Blog!
    Ich habe letztes Jahr auch den EJB Timer in unserem JBoss integriert und bin auf das gleiche Problem gestoßen, dass die Timer öfter gestartet werden und ich nicht wusste wo sie gespeichert werden. Wir benutzen JBoss 4.2.3 und EJB 3. Die Timer werden in der lokalen Datenbank vom JBoss gespeichert. Die findest du hier: Jboss\server\default\data\hypersonic. Zusätzlich prüfe ich beim starten des Timers ob es ihn schon gibt und lösche ihn ggf.
    Jetzt zu meinem Problem. Ich versuche Quartz zu integrieren, habe aber das Problem das ich in dem Job nicht auf die aktuelle SessionContext zugreifen kann. Hast du die Jobs geclustert um an den eigentlichen EJB Container zu kommen?

    mfg
    Alex

Comments are closed.