Leitfaden für Jakarta EE JTA

1. Übersicht

Die Java-Transaktions-API, besser bekannt als JTA, ist eine API zum Verwalten von Transaktionen in Java. Es ermöglicht uns, Transaktionen ressourcenunabhängig zu starten, festzuschreiben und zurückzusetzen.

Die wahre Stärke von JTA liegt in der Fähigkeit, mehrere Ressourcen (z. B. Datenbanken, Messaging-Dienste) in einer einzigen Transaktion zu verwalten.

In diesem Tutorial lernen wir JTA auf konzeptioneller Ebene kennen und sehen, wie Geschäftscode üblicherweise mit JTA interagiert.

2. Universelle API und verteilte Transaktion

JTA bietet eine Abstraktion über die Transaktionssteuerung (Start, Commit und Rollback) für Geschäftscode.

Ohne diese Abstraktion müssten wir uns mit den einzelnen APIs jedes Ressourcentyps befassen.

Zum Beispiel müssen wir uns mit solchen JDBC-Ressourcen befassen. Ebenso kann eine JMS-Ressource ein ähnliches, aber nicht kompatibles Modell haben.

Mit JTA können wir mehrere Ressourcen unterschiedlichen Typs auf konsistente und koordinierte Weise verwalten .

Als API definiert JTA Schnittstellen und Semantik, die von Transaktionsmanagern implementiert werden sollen . Implementierungen werden von Bibliotheken wie Narayana und Bitronix bereitgestellt.

3. Beispielprojekt-Setup

Die Beispielanwendung ist ein sehr einfacher Back-End-Service einer Bankanwendung. Wir haben zwei Dienste, den BankAccountService und den AuditService , die zwei verschiedene Datenbanken verwenden . Diese unabhängigen Datenbanken müssen beim Start, Festschreiben oder Zurücksetzen der Transaktion koordiniert werden .

Zunächst verwendet unser Beispielprojekt Spring Boot, um die Konfiguration zu vereinfachen:

 org.springframework.boot spring-boot-starter-parent 2.2.2.RELEASE   org.springframework.boot spring-boot-starter-jta-bitronix 

Schließlich initialisieren wir vor jeder Testmethode AUDIT_LOG mit leeren Daten und Datenbank ACCOUNT mit 2 Zeilen:

+-----------+----------------+ | ID | BALANCE | +-----------+----------------+ | a0000001 | 1000 | | a0000002 | 2000 | +-----------+----------------+

4. Deklarative Transaktionsabgrenzung

Die erste Möglichkeit, mit Transaktionen in JTA zu arbeiten, ist die Verwendung der Annotation @Transactional . Eine ausführlichere Erklärung und Konfiguration finden Sie in diesem Artikel.

Kommentieren wir die Fassadendienstmethode executeTranser () mit @Transactional. Dies weist den Transaktionsmanager an, eine Transaktion zu beginnen :

@Transactional public void executeTransfer(String fromAccontId, String toAccountId, BigDecimal amount) { bankAccountService.transfer(fromAccontId, toAccountId, amount); auditService.log(fromAccontId, toAccountId, amount); ... }

Hier ruft die Methode executeTranser () zwei verschiedene Dienste auf, AccountService und AuditService. Diese Dienste verwenden zwei verschiedene Datenbanken.

Wenn executeTransfer () zurückgegeben wird, erkennt der Transaktionsmanager , dass dies das Ende der Transaktion ist, und schreibt sich für beide Datenbanken fest :

tellerService.executeTransfer("a0000001", "a0000002", BigDecimal.valueOf(500)); assertThat(accountService.balanceOf("a0000001")) .isEqualByComparingTo(BigDecimal.valueOf(500)); assertThat(accountService.balanceOf("a0000002")) .isEqualByComparingTo(BigDecimal.valueOf(2500)); TransferLog lastTransferLog = auditService .lastTransferLog(); assertThat(lastTransferLog) .isNotNull(); assertThat(lastTransferLog.getFromAccountId()) .isEqualTo("a0000001"); assertThat(lastTransferLog.getToAccountId()) .isEqualTo("a0000002"); assertThat(lastTransferLog.getAmount()) .isEqualByComparingTo(BigDecimal.valueOf(500));

4.1. Rollback in deklarativer Abgrenzung

Am Ende der Methode überprüft executeTransfer () den Kontostand und löst RuntimeException aus, wenn der Quellfonds nicht ausreicht:

@Transactional public void executeTransfer(String fromAccontId, String toAccountId, BigDecimal amount) { bankAccountService.transfer(fromAccontId, toAccountId, amount); auditService.log(fromAccontId, toAccountId, amount); BigDecimal balance = bankAccountService.balanceOf(fromAccontId); if(balance.compareTo(BigDecimal.ZERO) < 0) { throw new RuntimeException("Insufficient fund."); } }

Eine nicht behandelte RuntimeException nach dem ersten @Transactional führt ein Rollback der Transaktion auf beide Datenbanken durch . Tatsächlich führt das Ausführen einer Überweisung mit einem Betrag, der größer als der Kontostand ist, zu einem Rollback :

assertThatThrownBy(() -> { tellerService.executeTransfer("a0000002", "a0000001", BigDecimal.valueOf(10000)); }).hasMessage("Insufficient fund."); assertThat(accountService.balanceOf("a0000001")).isEqualByComparingTo(BigDecimal.valueOf(1000)); assertThat(accountService.balanceOf("a0000002")).isEqualByComparingTo(BigDecimal.valueOf(2000)); assertThat(auditServie.lastTransferLog()).isNull();

5. Programmatische Transaktionsabgrenzung

Eine andere Möglichkeit, die JTA-Transaktion zu steuern, ist die programmgesteuerte Verwendung von UserTransaction .

Ändern wir nun executeTransfer () , um die Transaktion manuell abzuwickeln:

userTransaction.begin(); bankAccountService.transfer(fromAccontId, toAccountId, amount); auditService.log(fromAccontId, toAccountId, amount); BigDecimal balance = bankAccountService.balanceOf(fromAccontId); if(balance.compareTo(BigDecimal.ZERO) < 0) { userTransaction.rollback(); throw new RuntimeException("Insufficient fund."); } else { userTransaction.commit(); }

In unserem Beispiel startet die Methode begin () eine neue Transaktion. Wenn die Kontostandsüberprüfung fehlschlägt, rufen wir rollback () auf, wodurch beide Datenbanken zurückgesetzt werden. Andernfalls werden die Änderungen durch den Aufruf von commit () in beide Datenbanken übernommen .

Es ist wichtig zu beachten, dass sowohl commit () als auch rollback () die aktuelle Transaktion beenden.

Letztendlich bietet uns die Verwendung der programmatischen Abgrenzung die Flexibilität einer feinkörnigen Transaktionskontrolle.

6. Fazit

In diesem Artikel haben wir das Problem besprochen, das JTA zu lösen versucht. Die Codebeispiele veranschaulichen die Steuerung von Transaktionen mit Anmerkungen und programmgesteuert mit zwei Transaktionsressourcen, die in einer einzelnen Transaktion koordiniert werden müssen.

Das Codebeispiel finden Sie wie gewohnt auf GitHub.