18 April 2011

Java EE ist durch das Convention over Configuration Paradigma einfach zu handhaben, solange man den Konventionen vertraut. Sobald man davon abweicht, muss man vorsichtig sein, was man tut.

Speziell das Transaction-Handling sorgt immer wieder für Probleme. Per Konvention läuft eine Methode einer EJB in einer Transaktion. Gibt es bereits eine Transaktion, wird diese wiederverwendet.

Wenn aber über die Annotation @TransactionAttribute(TransactionAttributeType.REQUIRES_NEW) eine neue Transaktion gestartet werden soll, dann wird es schwieriger. Wenn dann dem Entwickler aus fachlicher Sicht nichtmal klar ist, wie das Transaktionsverhalten sein sollte, werden Konstrukte gebaut, die nicht mehr wartbar sind.

Meine bisherige Erfahrung ist, dass die Kombination der TransactionAttributeTypes REQUIRES_NEW/REQUIRED und NEVER hilfreich ist, wenn es darum geht, Timeouts für Transaktionen zu vermeiden. Denn sobald man mit REQUIRES_NEW eine neue Transaktion starten lässt, wird die Transaktion, die zuvor gestartet wurde, pausiert. Wenn im Application-Server dann ein Timeout konfiguriert ist, kommt es z.B. beim WebSphere AS nach 10 Minuten zu einem Transaction-Timeout und durch eine TransactionRolledbackException wird die Transaktion zurückgerollt. Deshalb versuche ich bei langläufigen Transaktionen (speziell Batch-Verarbeitung) alles, was nicht innerhalb einer Transaktion ablaufen muss, mit einer EJB ohne Transaktion (TransactionAttributeType.NEVER) verarbeitet wird. Mit einer zweiten EJB werden dann mit einer neuen Transaktion alle Datensätze aktualisiert. Dadurch verhindert man in den meisten Fällen, dass man den Konfigurationswert für ein Transaction-Timeout immer weiter nach oben schrauben muss.

Wenn allerdings dem Entwickler nicht klar ist, wie die einzelnen EJBs überhaupt zusammenarbeiten, wird es schwierig. Dann werden Konstrukte gebaut, die weit weg vom Standard sind. Da wird dann mit Hilfe des Decorator-Patterns mit Technik versucht etwas zu korrigieren, was man bei genauerer Betrachtung mit einfachsten Mitteln hätte gelöst werden können.

Ein weiteres Problem sind Unit-Tests. Viele solcher Tests sind meist keine richtigen Unit-Tests, sondern halbe Integrationstests. Diese werden meist so aufgebaut, dass am Anfang eine Transaktion gestartet und am Ende beendet wird. Dies spiegelt natürlich nicht das tatsächliche Verhalten im Container wieder.
Jetzt lässt sich natürlich darüber streiten, ob es solche Tests überhaupt geben sollte. Wenn man aber solche Tests schreibt, sollte man das Transaktionsverhalten auch möglichst nachbauen und nicht über Code, der auch im produktiven Betrieb genutzt wird, ein Konstrukt zu bauen, das scheinbar funktioniert. Zum einen ist solch ein Code in vielen Fällen schwer wartbar und zum anderen wird diese Art von Code in den meisten Fällen nicht wieder entfernt, wenn er mal im Produktion war.

Am Besten ist es, sich auch mal mit der Fachlichkeit zu beschäftigen. Dann muss man keine Probleme mit überkomplizierten Code lösen.