Trong bài viết JDBC transaction management trong Spring, mình đã giới thiệu với các bạn về cách cấu hình transaction management sử dụng XML file. Chúng ta cũng có thể sử dụng code Java để làm điều này với annotation @EnableTransactionManagement. Cụ thể như thế nào? Chúng ta sẽ cùng nhau tìm hiểu trong bài viết này các bạn nhé!
Đầu tiên, mình sẽ tạo mới một Maven project để làm ví dụ:
Chúng ta sẽ sử dụng Java 21 cho project này:
1 2 3 4 |
<properties> <maven.compiler.target>21</maven.compiler.target> <maven.compiler.source>21</maven.compiler.source> </properties> |
Spring Context và Spring JDBC như sau:
1 2 3 4 5 6 7 8 9 10 |
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>6.1.10</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>6.1.10</version> </dependency> |
Mình sẽ sử dụng MySQL database để làm ví dụ:
1 2 3 4 5 |
<dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <version>9.0.0</version> </dependency> |
Để so sánh cách cấu hình với tập tin XML, trong bài viết này, mình cũng tạo một ứng dụng ngân hàng nhỏ cho phép chúng ta có thể chuyển tiền từ tài khoản A sang tài khoản B. Quá trình chuyển khoản này sẽ gồm 2 bước: trừ tiền trong tài khoản A và cộng tiền vào tài khoản B. Bất kỳ exception nào xảy ra trong quá trình chuyển tiền thì tất cả các hoạt động sẽ được rollback lại. Tương tự như bài viết trước.
Mình cũng định nghĩa một table với cấu trúc như sau:
1 2 3 4 5 |
CREATE TABLE account ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(45) NOT NULL, amount DECIMAL ); |
Interface AccountDAO và implementation của nó AccountDAOImpl để làm việc với table account, interface AccountService và implementation của nó AccountServiceImpl để làm việc với business của ứng dụng, có nội dung như sau:
1 2 3 4 5 6 7 8 9 10 |
package com.huongdanjava.spring; import java.math.BigDecimal; public interface AccountDAO { BigDecimal getCurrentAmount(int id); void updateAmount(int id, BigDecimal amount); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
package com.huongdanjava.spring; import java.math.BigDecimal; import java.sql.ResultSet; import java.sql.SQLException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.DataAccessException; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.ResultSetExtractor; import org.springframework.stereotype.Repository; @Repository public class AccountDAOImpl implements AccountDAO { @Autowired private JdbcTemplate jdbcTemplate; @Override public BigDecimal getCurrentAmount(int id) { String sql = "SELECT amount FROM account WHERE id=" + id; return jdbcTemplate.query(sql, new ResultSetExtractor<BigDecimal>() { @Override public BigDecimal extractData(ResultSet resultSet) throws SQLException, DataAccessException { resultSet.next(); return BigDecimal.valueOf(resultSet.getInt("amount")); } }); } @Override public void updateAmount(int id, BigDecimal amount) { String sql = String.format("UPDATE account SET amount=%f WHERE id=%d", amount, id); jdbcTemplate.execute(sql); } } |
1 2 3 4 5 6 7 8 |
package com.huongdanjava.spring; import java.math.BigDecimal; public interface AccountService { void transfer(int sourceId, int destId, BigDecimal amount) throws Exception; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
package com.huongdanjava.spring; import java.math.BigDecimal; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class AccountServiceImpl implements AccountService { @Autowired private AccountDAO accountDAO; @Override public void transfer(int sourceId, int destId, BigDecimal amount) throws Exception { BigDecimal sourceAmount = accountDAO.getCurrentAmount(sourceId); BigDecimal destAmount = accountDAO.getCurrentAmount(destId); // Update source account BigDecimal subtractAmount = sourceAmount.subtract(amount); accountDAO.updateAmount(sourceId, subtractAmount); if (subtractAmount.compareTo(BigDecimal.valueOf(80)) <= 0) { throw new Exception("Not allow send money if current amount is less than 80$"); } // Update dest account accountDAO.updateAmount(destId, destAmount.add(amount)); } } |
Như các bạn thấy, nếu sau khi trừ tiền trong tài khoản A, nếu số tiền còn lại của tài khoản A nhỏ hơn 80$, mình sẽ không cho chuyển nữa và throw Exception.
Bây giờ, mình sẽ tạo mới một class để cấu hình cho ứng dụng:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
package com.huongdanjava.spring; import javax.sql.DataSource; 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.jdbc.datasource.DriverManagerDataSource; @Configuration @ComponentScan public class ApplicationConfiguration { @Bean public DataSource dataSource() { DriverManagerDataSource dataSource = new DriverManagerDataSource(); dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver"); dataSource.setUrl("jdbc:mysql://localhost:3306/jpaexample"); dataSource.setUsername("root"); dataSource.setPassword("123456"); return dataSource; } @Bean public JdbcTemplate jdbcTemplate(DataSource dataSource) { return new JdbcTemplate(dataSource); } } |
Trong class ApplicationConfiguration này, mình sử dụng annotation @ComponentScan để Spring scan và khởi tạo bean cho tất cả các class làm việc với table account mà mình đã tạo ở trên. Mình cũng đã khởi tạo bean cho đối tượng DataSource và JdbcTemplate để những class này sử dụng.
Ví dụ bây giờ trong database của mình có 2 tài khoản A và B với các thông tin như sau:
Giờ mình sẽ viết code để thực hiện việc chuyển 20$ từ tài khoản A sang tài khoản B. Nội dung của class Application sẽ như sau:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
package com.huongdanjava.spring; import java.math.BigDecimal; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext; public class Application { public static void main(String[] args) throws Exception { ApplicationContext ctx = new AnnotationConfigApplicationContext(ApplicationConfiguration.class); AccountService as = (AccountService) ctx.getBean("accountServiceImpl"); as.transfer(1, 2, BigDecimal.valueOf(20)); } } |
Bây giờ, nếu mình chạy ứng dụng này, các bạn sẽ thấy có lỗi xảy ra:
tài khoản A bị trừ tiền nhưng tài khoản B lại không nhận được tiền:
Để enable transaction management cho ứng dụng này với annotation @EnableTransactionManagement, các bạn hãy khai báo annotation @EnableTransactionManagement và một bean của TransactionManager trong class cấu hình của ứng dụng:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
package com.huongdanjava.spring; import javax.sql.DataSource; 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.jdbc.datasource.DataSourceTransactionManager; import org.springframework.jdbc.datasource.DriverManagerDataSource; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; @Configuration @ComponentScan @EnableTransactionManagement public class ApplicationConfiguration { @Bean public DataSource dataSource() { DriverManagerDataSource dataSource = new DriverManagerDataSource(); dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver"); dataSource.setUrl("jdbc:mysql://localhost:3306/jpaexample"); dataSource.setUsername("root"); dataSource.setPassword("123456"); return dataSource; } @Bean public JdbcTemplate jdbcTemplate(DataSource dataSource) { return new JdbcTemplate(dataSource); } @Bean public PlatformTransactionManager transactionManager() { DataSourceTransactionManager transactionManager = new DataSourceTransactionManager(); transactionManager.setDataSource(dataSource()); return transactionManager; } } |
và annotate method transfer() của class AccountServiceImpl annotation @Transactional như trong bài viết trước:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
package com.huongdanjava.spring; import java.math.BigDecimal; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service public class AccountServiceImpl implements AccountService { @Autowired private AccountDAO accountDAO; @Override @Transactional(rollbackFor = Exception.class) public void transfer(int sourceId, int destId, BigDecimal amount) throws Exception { BigDecimal sourceAmount = accountDAO.getCurrentAmount(sourceId); BigDecimal destAmount = accountDAO.getCurrentAmount(destId); // Update source account BigDecimal subtractAmount = sourceAmount.subtract(amount); accountDAO.updateAmount(sourceId, subtractAmount); if (subtractAmount.compareTo(BigDecimal.valueOf(80)) <= 0) { throw new Exception("Not allow send money if current amount is less than 80$"); } // Update dest account accountDAO.updateAmount(destId, destAmount.add(amount)); } } |
Nói về annotation @EnableTransactionManagement thì là nó tương đương với khai báo <tx:annotation-driven/> mà mình đã đề cập trong bài viết trước đó các bạn! Annotation này có các attribute định nghĩa cách thức mà ứng dụng chúng ta sẽ quản lý transaction.
Attribute adviceMode định nghĩa cách thức chúng ta apply lập trình hướng khía cạnh sử dụng Java proxy, thư viện CGLIB hay thư viện AspectJ. Mặc định thì sử dụng PROXY các bạn nhé!
Attribute proxyTargetClass chỉ sử dụng cho activeMode là PROXY và nếu giá trị của nó là true thì thư viện CGLIB sẽ được sử dụng. Ngược lại, nếu giá trị của thuộc tính này là false thì Java proxy sẽ được sử dụng.
Annotation @Transactional thì mình đã đề cập với các bạn trong bài viết trước. Việc khai báo của annotation @Transactional ở đâu, class level hay method level sẽ giúp Spring biết chúng ta cần quản lý transaction ở level nào.
Interface TransactionManager có 2 sub-interface là PlatformTransactionManager và ReactiveTransactionManager.
PlatformTransactionManager dùng để thao tác với database theo kiểu imperative, còn ReactiveTransactionManager thì dành cho kiểu reactive các bạn nhé! Các bạn có thể sử dụng các implementation của từng sub-interface này phù hợp cho ứng dụng của mình. Trong ví dụ của bài viết này, mình đang sử dụng PlatformTransactionManager với implementation là DataSourceTransactionManager đó các bạn!
Cập nhập lại database để số tiền của tài khoản A về 100$ rồi chạy lại ứng dụng, các bạn sẽ thấy có lỗi xảy ra nhưng tài khoản A không bị trừ tiền nữa!