Einer meiner Kritikpunkte des EJB-Frameworks betrifft die fehlende Unterstützung für eine leichtgewichtige Umsetzung nebenläufiger Anwendungsfälle. Als Beispiel betrachten wir eine SessionBean, die einen aufwändigen, lang dauernden Job erledigen soll. Es ist dann naheliegend, diesen Job nebenläufig auszuführen und die Ergebnisse der Job-Abarbeitung zu einem späteren Zeitpunkt dem Anwender zur Verfügung zu stellen. Wie es Java EE konform geht will ich hier aufzeigen. Aber zunächst soll die nicht Java EE konforme Variante durchgesprochen (und verworfen) werden.
Die nicht Java EE konforme Variante (falsch!)
Erwartungsgemäß hat weder der Compiler noch der EJB-Container ein Problem damit, wenn im Kontext einer EJB ein eigener Thread gestartet wird. Wenn dieser Thread etwas Sinnvolles erledigen soll, muss er das Ergebnis seiner Arbeit in einem aus Sicht des EJB-Containers synchronisierten Kontext ablegen. Dafür stehen zwei Varianten zur Verfügung, die beide nicht funktionieren:
Zusammenfassung: eine naive Umsetzung von Nebenläufigkeit, das Starten eines eigenen Threads im Kontext einer EJB, ist verlockend, aber nutzlos. Hinzufügen möchte ich noch, dass das Starten eines solchen Threads ohnehin formal gegen EJB-Restriktionen vertößt.
Asynchronizität über Message Queues
Die momentan einzig korrekte Lösung innerhalb einer Java EE Umgebung nutzt Message Queues. Die Session Bean schickt also eine Nachrichten an eine Message-Queue des Servers, an der eine Message Driven Bean als Listener angemeldet ist. Diese Nachricht wird von der MDB empfangen, als Job interpretiert und nebenläufig abgearbeitet.
Das Deployment einer Message Driven Bean ist nicht schwer und hier am GlassFish demonstriert. Damit dem EJB Container die beiden Ressourcen zur Verfügung stehen, müssen sie in der ejb-jar.xml deklariert sein:
<ejb-jar> : <enterprise-beans> <session> : <resource-ref> <description>MQConnection Support for Asynch Services</description> <res-ref-name>jms/AsynchSupportConnectionFactory</res-ref-name> <res-type>javax.jms.QueueConnectionFactory</res-type> <res-auth>Container</res-auth> <res-sharing-scope>Shareable</res-sharing-scope> </resource-ref> : <resource-env-ref> <description>Queue for Asynch Services</description> <resource-env-ref-name>jms/AsynchSupportQueue</resource-env-ref-name> <resource-env-ref-type>javax.jms.Queue</resource-env-ref-type> </resource-env-ref> </session> : </enterprise-beans> <assembly-descriptor> : </assembly-descriptor> : </ejb-jar>
... und mit den JNDI Namen der JMS Ressourcen verknüpft werden. Das erfolgt wie immer serverspezifisch und sieht für den GlassFish in der sun-ejb-jar.xml so aus:
<sun-ejb-jar> : <resource-ref> <res-ref-name>jms/AsynchSupportConnectionFactory</res-ref-name> <jndi-name>jms/OpossumQueueConnectionFactory</jndi-name> </resource-ref> : <resource-env-ref> <resource-env-ref-name>jms/AsynchSupportQueue</resource-env-ref-name> <jndi-name>jms/OpossumQueue</jndi-name> </resource-env-ref> : </sun-ejb-jar>
Ausblick: Asynchronizität mit EJB3.1
Session Beans können ab EJB3.1 mit der Annotation @Asynchronous eine Methode (oder eine Klasse) als asynchron deklarieren. Solche Methoden werden asynchron ausgeführt, das heißt nach dem Aufruf kehrt die Abarbeitung sofort zum Client zurück. Zwei Rückgabewerte für asynchrone Methoden sind möglich:
@Asynchronous public Future<Integer> performLongrunnningStuff(...) { <- Abarbeitung kehrt sofort zum Client zurück // ... do expencive stuff Integer result = ...; return new javax.ejb.AsyncResult<Integer>(result); <- ab jetzt wird Future.isDone true ergeben }Mit Future.cancel kann der Client die laufende Abarbeitung stoppen. Innerhalb der Bean ist dann solch ein Abbruch mit SessionContext.isCancelled() feststellbar. Kommt es bei der Abarbeitung zu einer Ausnahme, so wirft Future.get am Client eine java.util.concurrent.ExecutionException, deren getCause die Originalexception liefert.
Wie organisiert der Container die Abarbeitung der asynchronen Aufrufe? Das ist nicht spezifiziert, aber wahrscheinlich bedient er sich eines Workerthread-Pools, dessen Größe festgelegt werden kann. Asynchrone Aufrufe werden dann parallel abgearbeitet, solange der Workerthread-Vorrat das zulässt, ist diese Grenze überschritten wird seriell abgearbeitet und bis sie an der Reihe sind verbleiben die Aufrufparameter der Abarbeitung im Speicher des Servers. Vielleicht werden sie auch vorübergehend persistenziert. In jedem Fall sollte man die Größe der Aufrufparameter möglichst beschränken.
Weiterhin muss man exzessives Polling an der Handle-Instanz vermeiden. Es scheint zunächst verlockend, im Sekundentakt das Future.isDone zu rufen. Wenn das 200 Clients gleichzeitig tun, sind das 200 Anfragen pro Sekunde! Vernünftig ist es, erst dann ein Future.isDone zu rufen, wenn eine fachlich begründete Aussicht darauf besteht, dass das Ergebnis der Abarbeitung vorliegt. Wenn möglich sollte auf den Rückgabewert Future zu verzichtet werden.