Transaktionsgrenzen mit Tapestry 5 und dem Spring Framework

09.03.2012 Permalink

Die Definition von Transaktionsgrenzen in einem System gehört zu den weniger alltäglichen Tätigkeiten in der JEE Softwareentwicklung. Ein EJB Container nimmt einem die Entscheidung praktisch ab, denn allgemein gilt hier: wenn ein Request in das System eintritt, wird eine Transaktion gestartet. Wird der Request bearbeitet, ohne dass der Container eine Exception fängt, wird die Transaktion committet, sonst abgebrochen und zurückgerollt.

Im Servlet Container (z.B. Tomcat) besteht deutlich mehr Spielraum. Das Spring Framework bietet drei weitverbreitete Wege an: Einen vierten Weg habe ich in der vorstehenden Aufzählung mit Absicht nicht erwähnt, weil ich ihn für unnötig riskant und in den meisten Fällen für verzichtbar halte: direkte Aufrufe des Transaction Managers, so dass man die volle Kontrolle und Verantwortung für die korrekte Verwaltung und Ausnahmebehandlung hat. Das Transaction Template ist eine Art Call Wrapper, Freunde funktionaler Programmierung erinnert es an eine Funktion höherer Ordnung. Das Template erhält einen als Klasseninstanz verpackten Codeblock, und kümmert sich vor und nach Ausführung des Codeblocks um das Starten und Beenden / Abbrechen der Transaktion. Es ist ein universales Mittel, um auf sichere Art die transaktionale Ausführung des Codeblocks zu gewährleisten. Leider ist es auch eine (in Java!) syntaktisch hässliche Variante. Und sie besitzt den Nachteil, dass die eher umständliche Codekonstruktion überall bewusst verwendet werden muss. Wenn man das "Einpacken" vergisst, fehlt die Transaktion. Und das muss nicht mal gleich auffallen.

Annotationen sind in den letzten Jahren in Mode gekommen, da sie sperrige XML Konfigurationen ablösen und damit die Wartbarkeit verbessern. Im Bereich des Transaktionsmanagements halte ich sie jedoch für eine schlechte Wahl. Man braucht zwar nur eine Zeile XML und muss dann "nur" jeden Service oder Service Methode annotieren, schon ist die Sache erledigt. Aber das darf eben auch nirgends vergessen werden. Und man vermengt die querschnittliche Antwort auf die Frage "Wo gehört die Transaktionsgrenze hin?" mit dem Business Code, den das eigentlich nichts angeht.

AOP bietet demgegenüber den Vorzug, dass ich von außen und ohne Berührung des zu ummantelnden Codes festlegen kann, was transaktional auszuführen ist. Der geringe Preis ist, dass wir uns einmal mit Pointcut Ausdrücken beschäftigen müssen. Wird jedoch im Servlet Container neben Spring auch ein in der Request-Verarbeitung "weiter oben" angesiedeltes Framework wie Tapestry (Web UI) oder CXF (Web Services) verwendet, entsteht sofort die Frage, ob ich mit den Mitteln von Spring AOP noch "dazwischen" komme.

Aber warum reicht es nicht, erst auf der Ebene der Spring Services, quasi knapp unterhalb von CXF oder Tapestry, die Transaktionsgrenze anzusetzen? Die Antwort ist einfach: aus Sicht eines Benutzers oder Service Consumers ist der einzelne Request eine atomare Arbeitseinheit. Es ist nicht ausreichend, die einzelnen Aufrufe, die aus einer Page Action Methode oder einer Service Operation Methode herausgehen, jeweils in eine Transaktion zu packen. Sie müssen alle zusammen ganz oder gar nicht ausgeführt werden.

Glücklicherweise ist CXF sehr gut mit Spring integriert, es reichen die normalen Spring AOP Mittel, um jede Service Operation Methode zu versorgen. Tapestry hingegen bringt einen eigenen IoC Container mit. Und ich kenne keinen Weg, um die Ausführung der Action Methoden mittels Spring AOP in Transaktionen zu verpacken. Tapestry bietet dafür eine @CommitAfter Annotation an, mit der sich die Transaktion auf der richtigen Ebene definieren lässt. Das bringt uns aber direkt den @Transactional Nachteil: dass wir darauf vertrauen müssen, dass niemand @CommitAfter hinzuschreiben vergisst.

Ein erster Gedanke, die Kombination aus Transaction Template und Servlet Request Filter, führt nicht zum gewünschten Ergebnis, da Tapestry Exceptions natürlich fängt und durch Weiterleitung auf eine Fehlerseite behandelt. Damit fehlt das Rollback im Fehlerfall.

Das Problem lässt sich zum Glück mit zwei Tapestry Service Decorators lösen. Die Idee ist, den PageRenderRequestHandler und ComponentEventRequestHandler mit einer Art Around Advice zu versorgen. Dies geschieht im AppModule folgendermaßen:

public class AppModule {
	
    @Inject
    TransactionTemplate txHandler;
	
    public ComponentEventRequestHandler decorateComponentEventRequestHandler 
                                                (final ComponentEventRequestHandler delegate) {
        return new ComponentEventRequestHandler() {
            @Override
            public void handle(final ComponentEventRequestParameters params) throws IOException {
            	txHandler.execute(new TransactionCallback() {
                    @Override
                    public Void doInTransaction(TransactionStatus status) {
                        try {
                            delegate.handle(params);
                            return null;
                        } catch (Exception ex) {
                            if (ex instanceof RuntimeException) {
                                throw (RuntimeException)ex;
                            } else {
                                throw new RuntimeException(ex);
                            }
                        }
                    }
            	});
            }
        };
    }
    
    public PageRenderRequestHandler decoratePageRenderRequestHandler
                                            (final PageRenderRequestHandler delegate) {
        return new PageRenderRequestHandler() {
            @Override
            public void handle(final PageRenderRequestParameters params) throws IOException {
            	txHandler.execute(new TransactionCallback() {
                    @Override
                    public Void doInTransaction(TransactionStatus status) {
                        try {
                            delegate.handle(params);
                            return null;
                        } catch (Exception ex) {
                            if (ex instanceof RuntimeException) {
                                throw (RuntimeException)ex;
                            } else {
                                throw new RuntimeException(ex);
                            }
                        }
                    }
            	});
            }
        };
    }
}

Der Business Code bleibt sauber. @Transactional, @CommitAfter oder Spring AOP können dafür Zuhause bleiben.