Java EE: @Transactional – Deklarative Transaktionssteuerung mit CDI und ihre Auswirkungen
Deklarative Transaktionssteuerung war in der Vergangenheit eine der klassischen Domänen des Enterprise JavaBeans-Standards. Doch mit Java EE 7 wurde die Deklaration von Transaktionsgrenzen per Annotation verallgemeinert. Dies ermöglicht nun auch anderen Java EE-Standards, diese Mechanismen zu nutzen. Dieser Artikel zeigt, wie auch leichtgewichtige, POJO-basierte CDI-Komponenten von der Transaktionssteuerung per Annotation profitieren. Er erläutert, was über die (aus EJB bekannte) einfache Transaktionssteuerung hinaus mit dem neuen Mechanismus möglich ist, wo die Grenzen dieses Ansatzes liegen und wie sich die Transaktionssteuerung auch auf andere Bereiche des CDI-Standards auswirkt. Das Beispiel, aus dem die Code-Auszüge dieses Artikels entnommen sind, ist auf github verfügbar [1].
Wenn es um die Auswahl eines serverseitigen Komponentenstandards geht, war die Möglichkeit zur deklarativen Transaktionssteuerung lange eines der vorrangigen Argumente für den Einsatz von Enterprise JavaBeans (EJB). Auch wenn im Rahmen der Java EE 6 mit Contexts and Dependency Injection (CDI) ein neuer, leichtgewichtigerer Komponentenstandard das Licht der Welt erblickt hat, blieb die Transaktionssteuerung meist EJB überlassen. Obwohl in CDI von Anfang an die Implementierung eigener Transaktionsinterceptoren möglich war, scheuten doch viele Projekte diesen Schritt. Vor dem Hintergrund, dass es sich dabei um eine proprietäre Erweiterung handelt, oft eine sehr wohl nachvollziehbare Entscheidung. Auch der Einsatz von CDI-Erweiterungen, wie sie beispielsweise das Projekt Deltaspike [2] bereitstellt, ist überraschend selten in CDI 1.0 Projekten zu finden. So blieb i.d.R. nur der Einsatz von EJB zur Implementierung der CDI-Komponenten oder die programmatische Transaktionssteuerung via UserTransaction.
Transaktionsdeklaration in Java EE: Unabhängig vom gewählten Komponentenstandard
Mit CDI 1.1 [3] (Teil der Java EE 7) haben sich die Kräfteverhältnisse erneut verschoben. Ab dieser Version unterstützt auch der CDI-Container deklarative Transaktionssteuerung! Der Dank dafür gebührt der ebenfalls im Java EE-Standard vorhandenen Java Transaction API (JTA) [4], die seit Version 1.2 mit der Annotation @javax.transaction.Transactional eine Variante der Transaktionssteuerung anbietet, die unabhängig von EJB definiert wurde und für alle Implementierungen des ManagedBean-Standards zur Verfügung steht. Damit unterstützt insbesondere auch der CDI-Container diese Variante der Transaktionssteuerung durch standardmäßige Implementierung und Aktivierung eines entsprechenden Transaktionsinterceptors. Ein weiterer wichtiger Schritt hin zu einem Anwendungsdesign mit leichtgewichtigen, POJO-basierten, serverseitigen Komponenten.
Wie bereits von anderen Annotationen bekannt, kann @Transactional entweder direkt an der implementierenden Klasse der CDI Bean oder an einzelne Methoden der Bean gesetzt werden. Im Fall der Deklaration am Kopf der Klasse wirkt sie auf alle öffentlichen Methoden der CDI-Komponente. Annotationen direkt an den Methoden überschreiben wiederum eine evtl. vorhandene Klassenannotation. Unabhängig davon, ob Klasse oder Methode annotiert wird, ist darauf zu achten, dass die Annotation an der implementierenden Klasse, nicht an einem möglicherweise vorhandenen Interface, gesetzt werden muss.
Listing 1: Verwendung der @Transactional-Annotation auf Klasse- und Methoden-Ebene
@ApplicationScoped @Transactional //Default: TxType.REQUIRED public class KundenServiceImpl implements KundenService { @Inject private EntityManager em; public long erzeugeKunde(String vorname, String nachname) throws KundenServiceException{ //... } @Transactional(TxType.SUPPORTS) // Die Suche braucht nicht zwingend eine TX public Kunde findeKunde(long kundennummer){ //... } //... }
Die vorhandenen Möglichkeiten zur Definition des Transaktionsverhaltens werden dem erfahrenen Entwickler aus dem EJB-Umfeld bekannt vorkommen. Hier werden diese über das Attribut value der Annotation @Transactional gesteuert. Wird dieses nicht explizit gesetzt, so wird Transactional.TxType.REQUIRED als Default vorgegeben.
Java EE: Verfügbare Werte zur Transaktionsteuerung (javax.transaction.Transactional.TxType)
REQUIRED | Ist keine Transaktion aktiv, wird bei Betreten der Methode automatisch eine Transaktion gestartet. Diese wird beendet, sobald die Methode wieder verlassen wird. Ist bereits eine Transaktion aktiv, so wird die Methode innerhalb dieser Transaktion ausgeführt. (Default-Verhalten) |
REQUIRES_NEW | Bei Betreten der Methode wird immer eine neue Transaktion gestartet. Diese Transaktion wird beim Verlassen der Methode wieder beendet. Die neu gestartete Transaktion hat keine Verbindung zu einer beim Aufruf evtl. schon vorhandenen Transaktion. Diese wird suspendiert und nach Ausführung der Methode wieder aufgenommen. |
SUPPORTS | Es erfolgt keine explizite Transaktionssteuerung. Falls die Methode mit aktiver Transaktion gerufen wird, so wird sie im Kontext dieser Transaktion ausgeführt. Wird die Methode ohne Transaktion gerufen, so läuft sie ohne Transaktionskontext ab. |
NOT_SUPPORTED | Die Methode läuft immer ohne Transaktionskontext ab. Sollte bereits eine Transaktion aktiv sein, so wird diese zu Beginn des Methodenaufrufs suspendiert und nach dem Ende des Methodenaufrufs wieder aufgenommen. |
MANDATORY | Die annotierte Methode muss immer mit einem aktiven Transaktionskontext betreten werden. Ist beim Aufruf der Methode keine Transaktion aktiv, so wird eine TransactionRequiredException geworfen. |
NEVER | Die annotierte Methode darf nie mit einem aktiven Transaktionskontext betreten werden. Ist beim Aufruf der Methode eine Transaktion aktiv, so wird eine InvalidTransactionException geworfen. |
Über die Kombination der verfügbaren Transaktionsattribute in einer Kette von CDI Bean-Aufrufen, lässt sich jedes gewünschte Transaktionsverhalten abbilden. Abb.1 verdeutlich die Propagierung bzw. Änderung der aktiven Transaktion beim Aufruf weitere CDI-Komponenten.
Transaktionen und Exceptions
Eines der berühmtesten Missverständnisse im Zusammenhang mit deklarativem Transaktionsmanagement ist die Frage, in welchen Fällen die Transaktion vom Container zurückgerollt wird. Die Standardantwort hierbei ist: "Natürlich im Fehlerfall!" Aber wie genau definiert sich so ein Fehlerfall? Ist jede Exception ein Fehler? Ist ein Aufruf der Methode ohne Exception die einzige Variante eines erfolgreichen Aufrufs? Die Antwort auf diese Fragen sind im ersten Moment überraschend, bei genauerem Hinsehen aber vollkommen logisch nachvollziehbar: Erwartetes Methodenverhalten führt zu einem Transaktions-Commit, unerwartetes Verhalten führt zu einem Transaktions-Rollback. Klingt im ersten Moment einfach, es ist aber zu bedenken, dass an der Methode deklarierte (checked) Exceptions als erwartetes Verhalten gelten, die Transaktion also nicht (!) zurückrollen. Eine Tatsache, die in der Anwendung bei so manchem Entwickler schon zu überraschtem Kopfschütteln geführt hat.
Dieses Verhalten hat der Welt der serverseitigen Java-Komponenten die Unterscheidung in Application Exceptions (alle Arten von checked Exceptions) und System Exceptions (Ableitungen von Runtime Exceptions, also unchecked Exceptions) eingebracht. Die Ersten lösen kein Rollback aus, die Zweiten führen hingegen zwingend zu einem Transaktions-Rollback.
Diese Unterscheidung anhand der Exceptiontypen hat sich in der Vergangenheit allerdings als zu grob herausgestellt. Daher können bei @Transactional – zusätzlich zu den grundlegenden Transaktionsattributen – auch noch die Exceptions angegeben werden, die die Transaktion automatisch zurückrollen (rollbackOn) bzw. diejenigen, die dies nicht tun (dontRollbackOn). Exceptions, die hier nicht explizit aufgeführt werden, unterliegen der o. g. Aufteilung in Application Exceptions und System Exceptions und dem daraus abgeleiteten Transaktionsverhalten.
Listing 2: Steuerung des Transaktionsverhaltens bei Exceptions
@ApplicationScoped @Transactional public class KundenServiceImpl implements KundenService { @Inject private EntityManager em; // Die KundenServiceException soll ein Rollback ausloesen @Transactional(rollbackOn=KundenServiceException.class) public long erzeugeKunde(String vorname, String nachname) throws KundenServiceException{ //... } @Transactional(TxType.SUPPORTS) public Kunde findeKunde(long kundennummer){ //... } //... }
Dieser Ansatz erinnert an die Annotation @ApplicationException aus dem EJB-Standard, die vom CDI-Standard nicht unterstützt wird. Der entscheidende Unterschied der "neuen Lösung" liegt in der Position an der entschieden wird, welche Transaktionen Auswirkungen auf die Transaktionssteuerung haben und welche nicht. Dies gibt nun nicht mehr die Exception selbst vor, sondern der Anbieter der Methode entscheidet über das Verhalten im Ausnahmefall.
CDI: Transaktionssteuerung und Stereotypen
Anders als im EJB-Standard sind CDI Beans nicht standardmäßig transaktional. Um nun nicht jede Bean explizit mit @Transactional annotieren zu müssen, bietet es sich an, das CDI Feature der Stereotypen zu nutzen. Dabei werden CDI-Annotationen, die an gleichartigen Bean-Typen immer wieder verwendet werden, zu einer Stereotypen-Annotation zusammengefasst. Sind beispielsweise CDI-Komponenten die zustandslose Services implementieren immer @ApplicatonScoped, @Named und @Transactional, so lassen sich diese drei Annotationen in einen Stereotypen mit dem Namen @SingletonService zusammenfassen.
Listing 3: Definition eines transaktionalen Stereotypes
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @Stereotype @ApplicationScoped @Transactional @Named public @interface SingletonService { }
Alle Komponenten, die mit diesem Stereotypen annotiert werden, tragen nun implizit die o. g. Annotationen. Somit sind SingletonServices standardmäßig transaktional. Soll für einzelne Methoden das Verhalten geändert werden, so kann dies über direkte Annotierung der jeweiligen Methoden erreicht werden.
Listing 4: Verwendung der Stereotypen-Annotation
@SingletonService public class KundenServiceImpl implements KundenService { //... }
Java Persistence API: EntityManager ist nicht gleich EntityManager
Der Weg in die Datenbank ist in der Welt der Java EE der im Rahmen der Java Persistence API (JPA) definierte EntityManager. Der einfachste Weg in einem Java EE Server an einen solchen EntityManager zu kommen, ist die Annotation @PersistenceContext. Der so erhaltene vom Container verwaltete EntityManager arbeitet auf einem sogenannten "transaction-scoped PersistenceContext". Das bedeutet, dass der PersistenceContext, der für die eigentliche Verwaltung der Entitäten und deren Anbindung an die Datenbank zuständig ist, immer nur dann aktiv ist, wenn auch eine aktive Transaktion existiert. Wird diese beendet, so wird auch der PersistenceContext geschlossen und alle gerade noch persistenten Entitäten werden "detached", ihre Verbindung in die Datenbank unterbrochen. Sollen diese in der nächsten Transaktion wieder mit der Datenbank verbunden werden, so müssen sie entweder neu geladen oder über die Methode EntityManager.merge() explizit wiedereingegliedert werden.
Der aus dem EJB-Umfeld bekannte Ansatz zur Umgehung dieses Verhaltens, ist der Einsatz eines EntityManagers mit einem sogenannten "Extended PersistenceContext". Ein solcher PersistenceContext bleibt auch über mehrere Transaktionen hinweg bestehen, so dass das händische Wiedereingliedern der Entitäten entfällt. Das Problem dabei: Diese Art des PersistentContext wird in CDI nicht direkt unterstützt. Soll ein entsprechender EntityManager verwendet werden, muss auf den Einsatz von Stateful SessionBeans zurückgegriffen werden – hier kann über die Annotation
@PersistenceContext(type=PersistenceContextType.EXTENDED
ein EntityManager mit einem Extended PersistenceContext genutzt werden.
Neben dem vom Container verwalteten EntityManager ist aber auch die Verwendung eines durch die Anwendung verwalteten EntityManagers möglich. Diese werden entweder durch die Injizierung einer EntityManagerFactory (vgl. Listing 5) oder durch Verwendung der Bootstrapping-Klasse javax.persistence.Persistence erzeugt. Unabhängig ob vom Container oder von der Anwendung verwaltet, beide Varianten von EntityManagern können von mit @Transactional gestarteten Transaktionen gesteuert werden. Die dabei zu beachtenden Besonderheiten werden im folgenden Absatz bzw. im Abschnitt "Transaktionen und Kontexte" geschildert.
Listing 5: Container-managed und application-managed EntityManager Producer
/** * Zentrale Factory fuer container-managed und application-managed EntityManager. * Die verwendeten technischen Qualifier dienen der Verdeutlichung und waeren * normalerweise fachlich getrieben. * * Der @Default EntityManager ist container-managed. */ @ApplicationScoped public class EntityManagerProducer { // Container Managed EntityManager @Produces @Default @ContainerManaged @PersistenceContext(unitName="kunde") private EntityManager em; // Application-managed EntityManager @PersistenceUnit(unitName="kunde_app") private EntityManagerFactory emf; @Produces @ApplicationManaged @RequestScoped public EntityManager erzeugeEntityManager(){ return emf.createEntityManager(); } /** * Diese Methode wird fuer ApplicationManaged EntityManager vor Beendigung * ihres Scopes gerufen. Damit kann der EntityManager geschlossen und * damit gehaltene DB-Ressourcen freigegeben werden. * * @param em der zu schliessende EntityManager */ public void closeEntityManager(@Disposes @ApplicationManaged EntityManager em){ em.close(); } }
Auch wenn beide EntityManager-Varianten funktionieren, die Empfehlung ist hier eindeutig: Die Verwendung von vom Container verwalteten, transaction-scoped EntityManagern ist meist das beste Mittel der Wahl. U. a auch deswegen, weil die Einbindung in die aktuell laufende Transaktion automatisch im Hintergrund erfolgt. Von der Anwendung selbst verwaltete EntityManager müssen nicht nur im richtigen Moment wieder aufgeräumt (Stichwort: Disposer Method, vgl. Listing 5), sondern mittels EntityManager.join() auch explizit in die aktive Transaktion eingehängt werden. Warum also die ganze Arbeit selbst machen, wenn man sie dem Container überlassen kann?
CDI: Transaktionen und Events
Die bisher vorgestellten Details der Transaktionssteuerung mit @Transactional bergen keine wirklichen Überraschungen. Doch das transaktionale Verhalten endet bei CDI nicht mit dem Speichern von Daten in einer Datenbank. Auch der im Standard enthaltene Eventing-Mechanismus gliedert sich mit in die Transaktionssteuerung ein. Dabei erfolgt das Versenden von Events wie gewohnt über ein per Dependency Injection erhaltenes Event Object. Doch was hilft es dem Empfänger eines Events über die Erzeugung eines neuen Datensatzes, wenn die Transaktion, in deren Rahmen die Erzeugung stattfand, zurückgerollt wurde? Muss der Empfänger nun auch seinen Ursprungszustand wiederherstellen? Nicht, wenn der Empfang des Events auf das Commit der Transaktion verschoben wird! Dadurch wird der Empfänger tatsächlich erst dann benachrichtigt, wenn der entsprechende Datensatz in der Datenbank angelegt wurde (vgl. Listing 6).
Listing 6: Transaktionale Observer-Methoden
@ApplicationScoped @Transactional public class KundenServiceImpl implements KundenService { @Inject private Event<Kunde> kundeErzeugtEvent; @Transactional(rollbackOn=KundenServiceException.class) public long erzeugeKunde(String vorname, String nachname) throws KundenServiceException{ //... kundeErzeugtEvent.fire(kunde); //... } //... } @ApplicationScoped public class KundenObserver { public void neuerKundeErzeugt(@Observes(during=TransactionPhase.AFTER_SUCCESS) Kunde kunde){ System.out.println("Neuer Kunde erzeugt !"); } public void keinKundeErzeugt(@Observes(during=TransactionPhase.AFTER_FAILURE) Kunde kunde){ System.out.println("Kunde konnte nicht erzeugt werden !"); } }
Ob sich ein Event-Empfänger direkt benachrichtigen lassen möchte, oder erst dann, wenn die entsprechende Transaktion beendet wurde, kann über das Attribut during der Annotation @Observes angegeben werden. Hier kann der Empfänger entscheiden, in welcher Phase der Transaktion er das Event empfangen möchte: z. B. am Ende der Transaktion (AFTER_COMPLETION), nur bei einem erfolgreichen Commit (AFTER_SUCCESS) oder im Falle eines Rollbacks (AFTER_FAILURE). Wird kein konkretes Verhalten vorgegeben, so erfolgt die Zustellung des Events direkt ohne Rücksicht auf den weiteren Verlauf der Transaktion (IN_PROGRESS). Die Abarbeitung wird dabei Teil der beim Feuern des Events aktiven Transaktion.
Java EE: Mögliche Transaktionsphasen beim Empfang von Events (javax.enterprise.event.TransactionPhase)
AFTER_COMPLETION | Das Event wird erst nach Ende der Transaktion empfangen. |
AFTER_FAILURE | Das Event wird nach Ende der Transaktion empfangen, falls diese nicht erfolgreich commited werden konnte. |
AFTER_SUCCESS | Das Event wird nach Ende der Transaktion empfangen, falls diese erfolgreich commited werden konnte. |
BEFORE_COMPLETION | Das Event wird empfangen, bevor das Zwei-Phasen-Commit-Protokoll der aktiven Transaktion startet. |
IN_PROGRESS | Das Event wird sofort empfangen, sobald dieses gefeuert wurde, unabhängig vom Verlauf der Transaktion (Default-Verhalten). |
CDI-Komponenten: Transaktionen als Kontext
Eine weitere hilfreiche Integration in die Welt der Transaktionen ist die Möglichkeit, CDI-Komponenten für die Laufzeit einer Transaktion zu erzeugen. Dies wird durch die Verwendung der Annotation @TransactionScoped erreicht. Komponenten, die mit dieser Scope-Annotation versehen werden, existieren jeweils im Kontext der aktiven Transaktion. Sie werden aber nicht nur für jede neue Transaktion neu erzeugt, sie sind auch automatisch mit der Transaktion verknüpft. Dadurch ergibt sich u. a. die Möglichkeit, einen EntityManager, der eigentlich von der Anwendung selbst erzeugt wird, direkt mit der Transaktion zu verknüpfen. Damit ist kein explizites em.joinTransaction() mehr notwendig. Auf diese Weise wird auch aus einem von der Anwendung verwalteten Entity Manager ein transaction-scoped Entity Manager.
Listing 7: Selbstverwaltete EntityManager im TransactionScope
@ApplicationScoped public class EntityManagerProducer { // ... @PersistenceUnit(unitName="kunde_app") private EntityManagerFactory emf; @Produces @ApplicationManaged @TransactionScoped public EntityManager erzeugeEntityManager(){ return emf.createEntityManager(); } /** * Diese Methode wird fuer ApplicationManaged EntityManager vor Beendigung * ihres Scopes gerufen. Damit kann der EntityManager geschlossen und * damit gehaltene DB-Ressourcen freigegeben werden. * * @param em der zu schliessende EntityManager */ public void closeEntityManager(@Disposes @ApplicationManaged EntityManager em){ em.close(); } } @SingletonService public class KundenServiceImpl implements KundenService { @Inject @ApplicationManaged private EntityManager em; //... }
Resümee
Lange hat es gedauert, aber mit der JTA 1.2 ist das deklarative Transaktionsmanagement endlich in der Welt aller Java EE-Komponenten angekommen. @Transactional hält dabei wenig Überraschungen bereit. Aber schließlich hatte der Standard ja auch fast eineinhalb Jahrzehnte Erfahrung mit deklarativer Transaktionssteuerung in EJB, bevor es zu dieser Verallgemeinerung kam.
Positiv ist zu festzuhalten, dass die in dieser Zeit gemachten Lehren bzgl. Exception-Handling, nun endlich ihren Weg in den Standard gefunden haben. Auch die Integration in anderen Bereichen des CDI-Standards (Eventing und Scoping) machen das Bild rund.
Lediglich die Einschränkungen bei der Wahl des PersistenceContexts stößt etwas sauer auf. Da die Extended-Variante aber in der Entwicklergemeinde selten verwendet wird und sowohl transaction-scoped PersistenceContexte als auch application-managed EntityManager unterstützt werden, handelt es sich hierbei um eine akzeptable Einschränkung. Bei genauer Betrachtung ist diese Tatsache auch dem CDI-Standard an sich geschuldet und kann nicht dem Ansatz zur Transaktionssteuerung angelastet werden.
Alles in allem gilt also: Daumen hoch! Mit @Transactional werden nun hoffentlich noch mehr POJO-basierte CDI-Anwendungen entstehen, die vollständig auf einen EJB-Container verzichten können, ohne auf den Komfort der technischen Unterstützung des Containers verzichten zu müssen.
Publikationen
- Enterprise JavaBeans 3.1: Das EJB-Praxisbuch für Ein- und Umsteiger: Werner Eberling, Jan Leßner