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.