Start - Publikationen - Wissen - TOGAF - Impressum -

Einleitung


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:

  1. Der nebenläufige Thread benutzt ein transaktionales System (zum Beispiel eine Datenbank oder eine transaktionale Ressource über einen ResourceAdapter). Auf jeden Fall muss er diese Ressource vom Container anfordern, der Container wird jedoch eine Zusammenarbeit mit einem ihm nicht bekannten Thread verweigern. Alternativ kann der Thread unter Umgehung des Containers auf ein transaktionales System zugreifen (zum Beispiel über eine eigene JDBC Connection), an dieser Stelle würde ich aber aufhören, von einer intakten Java EE Umgebung zu sprechen.
  2. Der nebenläufige Thread hinterlegt das Ergebnis seiner Arbeit in einen RAM basierten (multithread-fähigen) Cache. Egal wie clever dieser Cache implementiert ist, irgendwann muss ein Stück Code synchronisiert sein, wenn mehrere Threads kontrolliert darauf zugreifen. An dieser Stelle kann es dann zu unvorhersagbaren Laufzeitproblemen kommen, wenn die Threadverwaltung des Containers mit dem eigenen Thread kollidiert. Sehen Sie sich dazu auch meine Anmerkungen zur Implementierung von Singletons in EJB-Containern an.

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:

  1. void: Keine Rückgabe, das Ergebnis der Methodenausführung wird implizit bereitgestellt.
  2. java.util.concurrent.Future: Der Client erhält als Rückgabe ein Handle auf die Abarbeitung und kann beschränkt Einfluss daruf nehmen oder sich über den Zustand der Abarbeitung informieren. Das Ergebnis der Methodenausführung wird dann explizit bereitgestellt, indem der Client Future.get ruft. Mit javax.ejb.AsyncResult wird eine Standard-Implementierung für Future bereitgestellt - dem Konstruktor ist die Instanz des Resultats zu übergeben.
    @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.

Referenzen


JSR-000318 - Enterprise JavaBeans 3.1

copyright © 2003-2021 | Dr. Christian Dürr | prozesse-und-systeme.de | all rights reserved