Maven Dependencies
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>com.demo</groupId>
<artifactId>tx-xml-demo</artifactId>
<version>1.0.0</version>
<properties>
<spring.version>5.3.21</spring.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.9.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.0.33</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.26</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>${spring.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>11</source>
<target>11</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
Spring XML Configuration
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:aop="http://framework.org/schema/aop"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/tx
https://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">
<bean id="bankService" class="com.demo.service.BankServiceImpl">
<property name="bankRepository" ref="bankRepository"/>
</bean>
<bean id="bankRepository" class="com.demo.repo.BankRepositoryImpl">
<property name="dataSource" ref="dataSource"/>
</bean>
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
<property name="url"
value="jdbc:mysql://localhost:3306/bankdb?useSSL=false&serverTimezone=UTC"/>
<property name="username" value="bankapp"/>
<property name="password" value="bankapp"/>
</bean>
<!-- Transaction Manager -->
<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- Transaction Advice -->
<tx:advice id="txAdvice" transaction-manager="txManager">
<tx:attributes>
<tx:method name="transfer*" propagation="REQUIRED"/>
<tx:method name="get*" propagation="SUPPORTS" read-only="true"/>
<tx:method name="query*" propagation="SUPPORTS" read-only="true"/>
</tx:attributes>
</tx:advice>
<aop:config>
<aop:pointcut id="serviceLayer"
expression="execution(* com.demo.service..*(..))"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="serviceLayer"/>
</aop:config>
</beans>
Domain Model
package com.demo.model;
import lombok.Data;
import java.io.Serializable;
@Data
public class Account implements Serializable {
private Long id;
private String owner;
private Double balance;
}
Repository Layer
package com.demo.repo;
import com.demo.model.Account;
public interface BankRepository {
Account fetchById(Long id);
Account fetchByOwner(String owner);
void persist(Account account);
}
package com.demo.repo;
import com.demo.model.Account;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.support.JdbcDaoSupport;
import java.util.List;
public class BankRepositoryImpl extends JdbcDaoSupport implements BankRepository {
@Override
public Account fetchById(Long id) {
List<Account> list = getJdbcTemplate().query(
"SELECT * FROM account WHERE id = ?",
new BeanPropertyRowMapper<>(Account.class), id);
return list.isEmpty() ? null : list.get(0);
}
@Override
public Account fetchByOwner(String owner) {
List<Account> list = getJdbcTemplate().query(
"SELECT * FROM account WHERE owner = ?",
new BeanPropertyRowMapper<>(Account.class), owner);
if (list.size() != 1) {
throw new IllegalStateException("Owner must be unique");
}
return list.get(0);
}
@Override
public void persist(Account account) {
getJdbcTemplate().update(
"UPDATE account SET owner = ?, balance = ? WHERE id = ?",
account.getOwner(), account.getBalance(), account.getId());
}
}
Service Layer
package com.demo.service;
import com.demo.model.Account;
public interface BankService {
Account getAccount(Long id);
void transfer(String fromOwner, String toOwner, Double amount);
}
package com.demo.service;
import com.demo.model.Account;
import com.demo.repo.BankRepository;
public class BankServiceImpl implements BankService {
private BankRepository bankRepository;
public void setBankRepository(BankRepository bankRepository) {
this.bankRepository = bankRepository;
}
@Override
public Account getAccount(Long id) {
return bankRepository.fetchById(id);
}
@Override
public void transfer(String fromOwner, String toOwner, Double amount) {
Account source = bankRepository.fetchByOwner(fromOwner);
Account target = bankRepository.fetchByOwner(toOwner);
source.setBalance(source.getBalance() - amount);
target.setBalance(target.getBalance() + amount);
bankRepository.persist(source);
if (true) throw new RuntimeException("Simulated failure");
bankRepository.persist(target);
}
}
JUnit Test
package com.demo;
import com.demo.service.BankService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:application.xml")
public class BankServiceTest {
@Autowired
private BankService bankService;
@Test
public void testRollbackOnFailure() {
bankService.transfer("Alice", "Bob", 200.0);
}
}