Przewodnik po Dżakarcie EE JTA

1. Przegląd

Java Transaction API, bardziej znany jako JTA, to API do zarządzania transakcjami w Javie. Umożliwia nam uruchamianie, zatwierdzanie i wycofywanie transakcji w sposób niezależny od zasobów.

Prawdziwa siła JTA polega na jego zdolności do zarządzania wieloma zasobami (np. Bazami danych, usługami przesyłania wiadomości) w ramach jednej transakcji.

W tym samouczku poznamy JTA na poziomie koncepcyjnym i zobaczymy, jak kod biznesowy często współdziała z JTA.

2. Uniwersalny interfejs API i transakcje rozproszone

JTA zapewnia abstrakcję kontroli transakcji (rozpoczęcie, zatwierdzenie i wycofanie) do kodu biznesowego.

W przypadku braku tej abstrakcji musielibyśmy zajmować się poszczególnymi interfejsami API każdego typu zasobu.

Na przykład musimy radzić sobie z zasobami JDBC w ten sposób. Podobnie zasób JMS może mieć podobny, ale niezgodny model.

Dzięki JTA możemy zarządzać wieloma zasobami różnych typów w spójny i skoordynowany sposób .

Jako API, JTA definiuje interfejsy i semantykę do zaimplementowania przez menedżerów transakcji . Implementacje są dostarczane przez biblioteki takie jak Narayana i Bitronix.

3. Przykładowa konfiguracja projektu

Przykładowa aplikacja jest bardzo prostą usługą zaplecza aplikacji bankowej. Mamy dwa usług, BankAccountService i AuditService przy użyciu dwóch różnych baz danych . Te niezależne bazy danych muszą być koordynowane przy rozpoczęciu transakcji, zatwierdzeniu lub wycofaniu .

Na początek nasz przykładowy projekt wykorzystuje Spring Boot, aby uprościć konfigurację:

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

Na koniec przed każdą metodą testową inicjalizujemy AUDIT_LOG z pustymi danymi i bazą ACCOUNT z 2 wierszami:

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

4. Deklaratywne rozgraniczenie transakcji

Pierwszy sposób pracy z transakcjami w JTA polega na wykorzystaniu adnotacji @Transactional . Bardziej szczegółowe wyjaśnienie i konfigurację można znaleźć w tym artykule.

Dodajmy adnotację do metody usługi fasady executeTranser () za pomocą @Transactional. To instruuje menedżera transakcji, aby rozpocząć transakcję :

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

Oto metoda executeTranser () zwraca 2 różnych usług, AccountService i AuditService. Usługi te korzystają z 2 różnych baz danych.

Kiedy executeTransfer () powraca, menedżer transakcji uznaje, że jest to koniec transakcji i zobowiązać się do obu baz danych :

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. Wycofywanie się w deklaratywnej demarkacji

Na końcu metody executeTransfer () sprawdza stan konta i generuje wyjątek RuntimeException, jeśli fundusz źródłowy jest niewystarczający:

@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."); } }

Nieobsługiwany RuntimeException obok pierwszego @Transactional będzie wycofać transakcję do obu baz danych . W efekcie wykonanie przelewu na kwotę większą niż saldo spowoduje wycofanie :

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. Automatyczne rozgraniczenie transakcji

Innym sposobem kontrolowania transakcji JTA jest programowe użycie UserTransaction .

Teraz zmodyfikujmy metodę executeTransfer (), aby obsługiwać transakcje ręcznie:

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(); }

W naszym przykładzie metoda begin () rozpoczyna nową transakcję. Jeśli walidacja salda nie powiedzie się, wywołujemy funkcję rollback (), która wycofa zmiany w obu bazach danych. W przeciwnym razie wywołanie commit () zatwierdza zmiany w obu bazach danych .

Należy zauważyć, że zarówno commit (), jak i rollback () kończą bieżącą transakcję.

Ostatecznie użycie programowego rozgraniczenia daje nam elastyczność precyzyjnej kontroli transakcji.

6. Wniosek

W tym artykule omówiliśmy problem, który JTA próbuje rozwiązać. Przykłady kodu ilustrują kontrolowanie transakcji z adnotacjami i programowo , z udziałem 2 zasobów transakcyjnych, które należy koordynować w ramach jednej transakcji.

Jak zwykle przykładowy kod można znaleźć na GitHub.