Panduan untuk Jakarta EE JTA

1. Ikhtisar

Java Transaction API, lebih dikenal sebagai JTA, adalah API untuk mengelola transaksi di Java. Ini memungkinkan kita untuk memulai, melakukan, dan mengembalikan transaksi dengan cara yang tidak bergantung sumber daya.

Kekuatan sebenarnya dari JTA terletak pada kemampuannya untuk mengelola banyak sumber daya (yaitu database, layanan pesan) dalam satu transaksi.

Dalam tutorial ini, kita akan mengenal JTA di tingkat konseptual dan melihat bagaimana kode bisnis umumnya berinteraksi dengan JTA.

2. API Universal dan Transaksi Terdistribusi

JTA menyediakan abstraksi atas kontrol transaksi (mulai, komit, dan kembalikan) ke kode bisnis.

Jika abstraksi ini tidak ada, kita harus berurusan dengan API individu dari setiap jenis sumber daya.

Misalnya, kita perlu menangani resource JDBC seperti ini. Selain itu, resource JMS mungkin memiliki model yang serupa tetapi tidak kompatibel.

Dengan JTA, kami dapat mengelola banyak sumber daya dari berbagai jenis secara konsisten dan terkoordinasi .

Sebagai API, JTA mendefinisikan antarmuka dan semantik untuk diterapkan oleh manajer transaksi . Implementasi disediakan oleh pustaka seperti Narayana dan Bitronix.

3. Contoh Setup Proyek

Aplikasi sampel adalah layanan back-end yang sangat sederhana dari aplikasi perbankan. Kami memiliki dua layanan, BankAccountService dan AuditService menggunakan dua database yang berbeda . Basis data independen ini perlu dikoordinasikan saat transaksi dimulai, komit, atau rollback .

Untuk memulainya, proyek sampel kami menggunakan Spring Boot untuk menyederhanakan konfigurasi:

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

Terakhir, sebelum setiap metode pengujian kami menginisialisasi AUDIT_LOG dengan data kosong dan ACCOUNT database dengan 2 baris:

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

4. Batasan Transaksi Deklaratif

Cara pertama menangani transaksi di JTA adalah dengan menggunakan anotasi @Transactional . Untuk penjelasan dan konfigurasi yang lebih terperinci, lihat artikel ini.

Mari kita beri anotasi metode layanan fasad executeTranser () dengan @Transactional. Ini menginstruksikan manajer transaksi untuk memulai transaksi :

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

Berikut metode executeTranser () memanggil 2 layanan yang berbeda, AccountService dan AuditService. Layanan ini menggunakan 2 database berbeda.

Ketika executeTransfer () kembali, yang manajer transaksi mengakui bahwa itu adalah akhir dari transaksi dan akan berkomitmen untuk kedua database :

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. Mengembalikan dalam Demarkasi Deklaratif

Di akhir metode, executeTransfer () memeriksa saldo akun dan menampilkan RuntimeException jika dana sumber tidak mencukupi:

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

Sebuah tertangani RuntimeException masa lalu pertama @Transactional akan rollback transaksi untuk kedua database . Akibatnya, melakukan transfer dengan jumlah yang lebih besar dari saldo akan menyebabkan 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. Batasan Transaksi Terprogram

Cara lain untuk mengontrol transaksi JTA adalah melalui program melalui UserTransaction .

Sekarang mari kita ubah executeTransfer () untuk menangani transaksi secara manual:

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

Dalam contoh kami, metode begin () memulai transaksi baru. Jika validasi saldo gagal, kita memanggil rollback () yang akan melakukan rollback pada kedua database. Jika tidak, panggilan ke commit () melakukan perubahan ke kedua database .

Penting untuk dicatat bahwa commit () dan rollback () mengakhiri transaksi saat ini.

Pada akhirnya, menggunakan demarkasi terprogram memberi kita fleksibilitas kontrol transaksi yang terperinci.

6. Kesimpulan

Pada artikel ini, kami membahas masalah yang coba diselesaikan JTA. Contoh kode menggambarkan pengendalian transaksi dengan anotasi dan secara terprogram , yang melibatkan 2 sumber daya transaksional yang perlu dikoordinasikan dalam satu transaksi.

Seperti biasa, contoh kode dapat ditemukan di GitHub.