Abnormal bank account transfer
Requirement: To transfer 10,000 from act-001 account to act-002 account, it is required that one of the balances of the two accounts is successfully reduced and the other is added successfully, that is, the two update statements executed must succeed or fail at the same time
Implementation steps
Step 1: Introduce the dependencies required for the project
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.powernode</groupId> <artifactId>spring6-013-tx-bank</artifactId> <version>1.0-SNAPSHOT</version> <packaging>jar</packaging> <!--Warehouse--> <repositories> <!--Repository of spring milestone version--> <repository> <id>repository.spring.milestone</id> <name>Spring Milestone Repository</name> <url>https://repo.spring.io/milestone</url> </repository> </repositories> <!--Dependencies--> <dependencies> <!--spring context, association introduces AOP--> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>6.0.0-M2</version> </dependency> <!--spring jdbc (JdbcTemplate of Spring framework), the association introduces transaction-related dependencies--> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>6.0.0-M2</version> </dependency> <!--mysql driver--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.30</version> </dependency> <!--Druid connection pool--> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.2.13</version> </dependency> <!--@Resource annotation--> <dependency> <groupId>jakarta.annotation</groupId> <artifactId>jakarta.annotation-api</artifactId> <version>2.1.1</version> </dependency> <!--junit--> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.13.2</version> <scope>test</scope> </dependency> </dependencies> <properties> <maven.compiler.source>17</maven.compiler.source> <maven.compiler.target>17</maven.compiler.target> </properties> </project>
Step 2: Prepare the t_act
table and insert two account records act-001 and act-002
into the table
Step 3: Write the entity class corresponding to the t_act
table
@Date public class Account {<!-- --> private String actno; private Double balance; }
Step 4: Write the AccountDao
interface in Dao (persistence layer)
and its implementation class AccountDaoImpl
which is specifically responsible for t_act
CRUD operations on tables, without any business logic code
public interface AccountDao {<!-- --> // Check the balance based on the account number Account selectByActno(String actno); //Update account information int update(Account act); }
@Repository("accountDao") public class AccountDaoImpl implements AccountDao {<!-- --> @Resource(name = "jdbcTemplate") private JdbcTemplate jdbcTemplate; @Override public Account selectByActno(String actno) {<!-- --> // Query account information based on the account number, and encapsulate the query results into the corresponding entity class String sql = "select actno, balance from t_act where actno = ?"; Account account = jdbcTemplate.queryForObject(sql, new BeanPropertyRowMapper<>(Account.class), actno); return account; } @Override public int update(Account act) {<!-- --> //Update the balance of the account based on the account number String sql = "update t_act set balance = ? where actno = ?"; int count = jdbcTemplate.update(sql, act.getBalance(), act.getActno()); return count; } }
Step 5: Write AccountService
in Service (Business Layer)
and its implementation class AccountServiceImpl
, which is responsible for the business logic processing of accounts. , such as transaction control related code
public interface AccountService {<!-- --> //Transfer method void transfer(String fromActno, String toActno, double money); }
@Service("accountService") public class AccountServiceImpl implements AccountService {<!-- --> @Resource(name = "accountDao") private AccountDao accountDao; // Because all transfer operations need to be completed in this method, the transaction needs to be controlled @Override public void transfer(String fromActno, String toActno, double money) {<!-- --> // Check whether the account balance is sufficient Account fromAct = accountDao.selectByActno(fromActno); if (fromAct.getBalance() < money) {<!-- --> throw new RuntimeException("Account balance insufficient"); } //If the balance is sufficient, start the transfer Account toAct = accountDao.selectByActno(toActno); // Modify the balance of the two objects in the memory first fromAct.setBalance(fromAct.getBalance() - money); toAct.setBalance(toAct.getBalance() + money); //Update the balance of the database account int count = accountDao.update(fromAct); count + = accountDao.update(toAct); if (count != 2) {<!-- --> throw new RuntimeException("Transfer failed, please contact the bank"); } } }
Step 6: Write spring
configuration file
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <!--Component scanning--> <context:component-scan base-package="com.powernode.bank"/> <!--Configuring data source--> <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"> <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/spring6"/> <property name="username" value="root"/> <property name="password" value="123456"/> </bean> <!--Configure jdbcTemplate--> <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate"> <property name="dataSource" ref="dataSource"/> </bean> </beans>
Step 7: Write a test program to simulate the presentation/control layer
to handle user needs, and call the corresponding business layer in the background to complete the business
public class BankTest {<!-- --> @Test public void testTransfer(){<!-- --> ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml"); AccountService accountService = applicationContext.getBean("accountService", AccountService.class); try {<!-- --> accountService.transfer("act-001", "act-002", 10000); System.out.println("Transfer successful"); } catch (Exception e) {<!-- --> e.printStackTrace(); } } }
Step 8: Simulate transfer exception. If an exception occurs during the operation of updating the balances of two accounts, the balance of the former will be reduced but the balance of the latter will not be added
@Service("accountService") public class AccountServiceImpl implements AccountService {<!-- --> @Resource(name = "accountDao") private AccountDao accountDao; @Override public void transfer(String fromActno, String toActno, double money) {<!-- --> // Check whether the account balance is sufficient Account fromAct = accountDao.selectByActno(fromActno); if (fromAct.getBalance() < money) {<!-- --> throw new RuntimeException("Account balance insufficient"); } // The balance is sufficient, start the transfer Account toAct = accountDao.selectByActno(toActno); fromAct.setBalance(fromAct.getBalance() - money); toAct.setBalance(toAct.getBalance() + money); int count = accountDao.update(fromAct); // Simulate exception String s = null; s.toString(); count + = accountDao.update(toAct); if (count != 2) {<!-- --> throw new RuntimeException("Transfer failed, please contact the bank"); } } }
Implementation of Spring transactions
Programmatic Transactions
Programmatic transactions (understanding)
, handwrite the code to control transactions in the business method
@Override public void transfer(String fromActno, String toActno, double money) {<!-- --> //The first step is to open the transaction //The second step executes the core business logic // The third step is to submit the transaction if there are no exceptions in the execution of the core business process. //The fourth step is to roll back the transaction if there is an exception in the execution of the core business process. }
Declarative transactions
Declarative transaction (commonly used)
, based on annotation method
or XML configuration method
to achieve transaction control
The bottom layer of Spring transaction management is based on AOP, so Spring has developed a set of APIs specifically for transactions PlatformTransactionManager (the core interface of the transaction manager)
and has two implementation classes
Implementation class | Description |
---|---|
DataSourceTransactionManager | Support transaction management such as JdbcTemplate, MyBatis, Hibernate |
JtaTransactionManager | Support distributed transaction management |
Step 1: Introduce the tx
namespace and its constraint file into the spring configuration file
, and associate it with the spring-jdbc
dependency Transaction-related dependencies
Step 2: Configure Transaction Manager
in the spring configuration file. If you use JdbcTemplate
in Spring 6, you must use DataSourceTransactionManager
transaction management Server to manage transactions
- Since the bottom layer of the transaction manager DataSourceTransactionManager is still
Connection connection
connection object to open and close transactions, so the transaction manager needs toconfigure the data source
Step 3: Enable the Transaction Annotation Driver
in the spring configuration file to tell the Spring framework to use annotations to control transactions
- The transaction manager is our aspect class, and the method identified by the
@Transactional
annotation is the connection point (the location where the aspect can be woven)
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:tx="http://www.springframework.org/schema/tx" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd"> <!--Component scanning--> <context:component-scan base-package="com.powernode.bank"/> <!--Configuring data source--> <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"> <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/spring6"/> <property name="username" value="root"/> <property name="password" value="root"/> </bean> <!--Configure jdbcTemplate--> <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate"> <property name="dataSource" ref="dataSource"/> </bean> <!--Configuration transaction manager--> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"/> </bean> <!--Turn on the transaction annotation driver to tell the Spring framework to use annotations to control transactions--> <tx:annotation-driven transaction-manager="transactionManager"/> </beans>
Step 4: Add the @Transactional
annotation to the business class or business method in the Service layer, indicating that the current class/method has enabled transactions, and batch DML operations in the business method can be guaranteed to be done at the same time Success or Failure
- Add annotations to the business class: indicating that all methods in the class enable transactions
- Add annotations to the business method: indicating that only the current method starts the transaction
@Service("accountService") @Transactional// Start transaction public class AccountServiceImpl implements AccountService {<!-- --> @Resource(name = "accountDao") private AccountDao accountDao; @Override public void transfer(String fromActno, String toActno, double money) {<!-- --> // Check whether the account balance is sufficient Account fromAct = accountDao.selectByActno(fromActno); if (fromAct.getBalance() < money) {<!-- --> throw new RuntimeException("Account balance insufficient"); } // The balance is sufficient, start the transfer Account toAct = accountDao.selectByActno(toActno); fromAct.setBalance(fromAct.getBalance() - money); toAct.setBalance(toAct.getBalance() + money); int count = accountDao.update(fromAct); // Simulate exception String s = null; s.toString(); count + = accountDao.update(toAct); if (count != 2) {<!-- --> throw new RuntimeException("Transfer failed, please contact the bank"); } } }