Merge pull request #12018 from hkhan/JAVA-10626-move-jta-module

[JAVA-10626] Move jta module to spring-persistence
This commit is contained in:
kwoyke
2022-04-04 12:56:14 +02:00
committed by GitHub
19 changed files with 59 additions and 124 deletions
@@ -6,6 +6,7 @@
/data
/src/main/webapp/WEB-INF/classes
*/META-INF/*
/transaction-logs
# Packaged files #
*.jar
@@ -8,6 +8,7 @@
- [Transactional Annotations: Spring vs. JTA](https://www.baeldung.com/spring-vs-jta-transactional)
- [Test a Mock JNDI Datasource with Spring](https://www.baeldung.com/spring-mock-jndi-datasource)
- [Detecting If a Spring Transaction Is Active](https://www.baeldung.com/spring-transaction-active)
- [Guide to Jakarta EE JTA](https://www.baeldung.com/jee-jta)
### Eclipse Config
After importing the project into Eclipse, you may see the following error:
@@ -51,6 +51,21 @@
<artifactId>spring-boot-starter</artifactId>
<version>${spring-boot-starter.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jta-atomikos</artifactId>
<version>${spring-boot-starter.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
<version>${spring-boot-starter.version}</version>
</dependency>
<dependency>
<groupId>org.hsqldb</groupId>
<artifactId>hsqldb</artifactId>
<version>${hsqldb.version}</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
@@ -79,15 +94,13 @@
</dependencies>
<properties>
<!-- Spring -->
<org.springframework.version>5.2.4.RELEASE</org.springframework.version>
<spring-boot-starter.version>2.3.3.RELEASE</spring-boot-starter.version>
<!-- persistence -->
<org.springframework.version>5.3.18</org.springframework.version>
<spring-boot-starter.version>2.6.6</spring-boot-starter.version>
<persistence-api.version>2.2</persistence-api.version>
<transaction-api.version>1.3</transaction-api.version>
<spring-data-jpa.version>2.2.7.RELEASE</spring-data-jpa.version>
<!-- simple-jndi -->
<simple-jndi.version>0.23.0</simple-jndi.version>
<hsqldb.version>2.5.2</hsqldb.version>
</properties>
</project>
@@ -0,0 +1,48 @@
package com.baeldung.jtademo;
import org.hsqldb.jdbc.pool.JDBCXADataSource;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.jta.atomikos.AtomikosXADataSourceWrapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.sql.DataSource;
@EnableAutoConfiguration
@EnableTransactionManagement
@Configuration
@ComponentScan
public class JtaDemoApplication {
@Bean("dataSourceAccount")
public DataSource dataSource() throws Exception {
return createHsqlXADatasource("jdbc:hsqldb:mem:accountDb");
}
@Bean("dataSourceAudit")
public DataSource dataSourceAudit() throws Exception {
return createHsqlXADatasource("jdbc:hsqldb:mem:auditDb");
}
private DataSource createHsqlXADatasource(String connectionUrl) throws Exception {
JDBCXADataSource dataSource = new JDBCXADataSource();
dataSource.setUrl(connectionUrl);
dataSource.setUser("sa");
AtomikosXADataSourceWrapper wrapper = new AtomikosXADataSourceWrapper();
return wrapper.wrapDataSource(dataSource);
}
@Bean("jdbcTemplateAccount")
public JdbcTemplate jdbcTemplate(@Qualifier("dataSourceAccount") DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
@Bean("jdbcTemplateAudit")
public JdbcTemplate jdbcTemplateAudit(@Qualifier("dataSourceAudit") DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
}
@@ -0,0 +1,27 @@
package com.baeldung.jtademo.dto;
import java.math.BigDecimal;
public class TransferLog {
private String fromAccountId;
private String toAccountId;
private BigDecimal amount;
public TransferLog(String fromAccountId, String toAccountId, BigDecimal amount) {
this.fromAccountId = fromAccountId;
this.toAccountId = toAccountId;
this.amount = amount;
}
public String getFromAccountId() {
return fromAccountId;
}
public String getToAccountId() {
return toAccountId;
}
public BigDecimal getAmount() {
return amount;
}
}
@@ -0,0 +1,39 @@
package com.baeldung.jtademo.services;
import com.baeldung.jtademo.dto.TransferLog;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
@Service
public class AuditService {
final JdbcTemplate jdbcTemplate;
@Autowired
public AuditService(@Qualifier("jdbcTemplateAudit") JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
public void log(String fromAccount, String toAccount, BigDecimal amount) {
jdbcTemplate.update("insert into AUDIT_LOG(FROM_ACCOUNT, TO_ACCOUNT, AMOUNT) values ?,?,?", fromAccount, toAccount, amount);
}
public TransferLog lastTransferLog() {
return jdbcTemplate.query(
"select FROM_ACCOUNT,TO_ACCOUNT,AMOUNT from AUDIT_LOG order by ID desc",
rs -> {
if (!rs.next()) {
return null;
}
return new TransferLog(
rs.getString(1),
rs.getString(2),
BigDecimal.valueOf(rs.getDouble(3))
);
});
}
}
@@ -0,0 +1,34 @@
package com.baeldung.jtademo.services;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
@Service
public class BankAccountService {
final JdbcTemplate jdbcTemplate;
@Autowired
public BankAccountService(@Qualifier("jdbcTemplateAccount") JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
public void transfer(String fromAccountId, String toAccountId, BigDecimal amount) {
jdbcTemplate.update("update ACCOUNT set BALANCE=BALANCE-? where ID=?", amount, fromAccountId);
jdbcTemplate.update("update ACCOUNT set BALANCE=BALANCE+? where ID=?", amount, toAccountId);
}
public BigDecimal balanceOf(String accountId) {
return jdbcTemplate.query(
"select BALANCE from ACCOUNT where ID=?",
new Object[] { accountId },
rs -> {
rs.next();
return BigDecimal.valueOf(rs.getDouble(1));
});
}
}
@@ -0,0 +1,45 @@
package com.baeldung.jtademo.services;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.transaction.Transactional;
import javax.transaction.UserTransaction;
import java.math.BigDecimal;
@Service
public class TellerService {
private final BankAccountService bankAccountService;
private final AuditService auditService;
private final UserTransaction userTransaction;
@Autowired
public TellerService(BankAccountService bankAccountService, AuditService auditService, UserTransaction userTransaction) {
this.bankAccountService = bankAccountService;
this.auditService = auditService;
this.userTransaction = userTransaction;
}
@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.");
}
}
public void executeTransferProgrammaticTx(String fromAccontId, String toAccountId, BigDecimal amount) throws Exception {
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();
}
}
}
@@ -0,0 +1,43 @@
package com.baeldung.jtademo.services;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.Resource;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.init.ScriptUtils;
import org.springframework.stereotype.Component;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
@Component
public class TestHelper {
final JdbcTemplate jdbcTemplateAccount;
final JdbcTemplate jdbcTemplateAudit;
@Autowired
public TestHelper(@Qualifier("jdbcTemplateAccount") JdbcTemplate jdbcTemplateAccount, @Qualifier("jdbcTemplateAudit") JdbcTemplate jdbcTemplateAudit) {
this.jdbcTemplateAccount = jdbcTemplateAccount;
this.jdbcTemplateAudit = jdbcTemplateAudit;
}
public void runAccountDbInit() throws SQLException {
runScript("account.sql", jdbcTemplateAccount.getDataSource());
}
public void runAuditDbInit() throws SQLException {
runScript("audit.sql", jdbcTemplateAudit.getDataSource());
}
private void runScript(String scriptName, DataSource dataSource) throws SQLException {
DefaultResourceLoader resourceLoader = new DefaultResourceLoader();
Resource script = resourceLoader.getResource(scriptName);
try (Connection con = dataSource.getConnection()) {
ScriptUtils.executeSqlScript(con, script);
}
}
}
@@ -0,0 +1,9 @@
DROP SCHEMA PUBLIC CASCADE;
create table ACCOUNT (
ID char(8) PRIMARY KEY,
BALANCE NUMERIC(28,10)
);
insert into ACCOUNT(ID, BALANCE) values ('a0000001', 1000);
insert into ACCOUNT(ID, BALANCE) values ('a0000002', 2000);
@@ -0,0 +1 @@
spring.jta.atomikos.properties.log-base-name=atomikos-log
@@ -0,0 +1,8 @@
DROP SCHEMA PUBLIC CASCADE;
create table AUDIT_LOG (
ID INTEGER IDENTITY PRIMARY KEY,
FROM_ACCOUNT varchar(8),
TO_ACCOUNT varchar(8),
AMOUNT numeric(28,10)
);
@@ -0,0 +1,90 @@
package com.baeldung.jtademo;
import com.baeldung.jtademo.dto.TransferLog;
import com.baeldung.jtademo.services.AuditService;
import com.baeldung.jtademo.services.BankAccountService;
import com.baeldung.jtademo.services.TellerService;
import com.baeldung.jtademo.services.TestHelper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import java.math.BigDecimal;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
@ExtendWith(SpringExtension.class)
@SpringBootTest(classes = JtaDemoApplication.class)
public class JtaDemoUnitTest {
@Autowired
TestHelper testHelper;
@Autowired
TellerService tellerService;
@Autowired
BankAccountService accountService;
@Autowired
AuditService auditService;
@BeforeEach
public void beforeTest() throws Exception {
testHelper.runAuditDbInit();
testHelper.runAccountDbInit();
}
@Test
public void givenAnnotationTx_whenNoException_thenAllCommitted() {
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.getFromAccountId()).isEqualTo("a0000001");
assertThat(lastTransferLog.getToAccountId()).isEqualTo("a0000002");
assertThat(lastTransferLog.getAmount()).isEqualByComparingTo(BigDecimal.valueOf(500));
}
@Test
public void givenAnnotationTx_whenException_thenAllRolledBack() {
assertThatThrownBy(
() -> tellerService.executeTransfer("a0000002", "a0000001", BigDecimal.valueOf(100000))
).hasMessage("Insufficient fund.");
assertThat(accountService.balanceOf("a0000001")).isEqualByComparingTo(BigDecimal.valueOf(1000));
assertThat(accountService.balanceOf("a0000002")).isEqualByComparingTo(BigDecimal.valueOf(2000));
assertThat(auditService.lastTransferLog()).isNull();
}
@Test
public void givenProgrammaticTx_whenCommit_thenAllCommitted() throws Exception {
tellerService.executeTransferProgrammaticTx("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.getFromAccountId()).isEqualTo("a0000001");
assertThat(lastTransferLog.getToAccountId()).isEqualTo("a0000002");
assertThat(lastTransferLog.getAmount()).isEqualByComparingTo(BigDecimal.valueOf(500));
}
@Test
public void givenProgrammaticTx_whenRollback_thenAllRolledBack() {
assertThatThrownBy(
() -> tellerService.executeTransferProgrammaticTx("a0000002", "a0000001", BigDecimal.valueOf(100000))
).hasMessage("Insufficient fund.");
assertThat(accountService.balanceOf("a0000001")).isEqualByComparingTo(BigDecimal.valueOf(1000));
assertThat(accountService.balanceOf("a0000002")).isEqualByComparingTo(BigDecimal.valueOf(2000));
assertThat(auditService.lastTransferLog()).isNull();
}
}