本文是 Spring 框架学习系列的第三篇,全文贯穿一个银行转账案例,带你从零入门 Spring 事务管理。
📚 一、本篇主要内容概览
- Spring 整合 JDBC 环境搭建:依赖、配置文件、数据源、JdbcTemplate
- JUnit 单元测试优化:从传统方式到 Spring Test 整合
- 账户模块 CRUD 实战:增删改查 + 批量操作
- Spring 事务控制:从转账事故到声明式事务的完整演进
🛠️ 二、Spring 整合 JDBC 环境搭建
2.1 添加依赖坐标
在 pom.xml 中加入以下依赖:
<!-- Spring 核心容器 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>6.0.5</version>
</dependency>
<!-- Spring JDBC 模块 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>6.0.5</version>
</dependency>
<!-- MySQL 驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.19</version>
</dependency>
<!-- C3P0 连接池 -->
<dependency>
<groupId>com.mchange</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.5.5</version>
</dependency>
<!-- JUnit 单元测试 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
<!-- Spring Test 模块(后续单元测试优化使用) -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>6.0.5</version>
<scope>test</scope>
</dependency>
2.2 为什么需要连接池?
每次建立数据库连接都是一次耗时耗资源的操作。连接池的作用是:
预先创建好一批连接放在内存中,应用程序需要时直接申请,用完归还,避免了频繁创建和销毁连接的性能开销。
我们使用的是 C3P0 连接池(相比 DBCP,C3P0 有自动回收空闲连接的功能)。
2.3 编写数据库配置文件
在 src/main/resources 目录下创建 db.properties:
# 驱动名
jdbc.driver=com.mysql.cj.jdbc.Driver
# 数据库连接(替换 database_name 为你的库名)
jdbc.url=jdbc:mysql://127.0.0.1:3306/database_name?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
# 数据库账号密码
jdbc.user=root
jdbc.password=123456
# 连接池可选配置
initialPoolSize=20
maxPoolSize=100
minPoolSize=10
maxIdleTime=60
acquireIncrement=5
maxStatements=5
idleConnectionTestPeriod=60
2.4 Spring 配置文件 - 整合所有资源
创建 applicationContext.xml:
<?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:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/tx
https://www.springframework.org/schema/tx/spring-tx.xsd">
<!-- 1. 加载 db.properties -->
<context:property-placeholder location="db.properties"/>
<!-- 2. 配置 C3P0 数据源 -->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="driverClass" value="${jdbc.driver}"/>
<property name="jdbcUrl" value="${jdbc.url}"/>
<property name="user" value="${jdbc.user}"/>
<property name="password" value="${jdbc.password}"/>
<!-- 可选连接池参数 -->
<property name="maxPoolSize" value="${maxPoolSize}"/>
<property name="minPoolSize" value="${minPoolSize}"/>
<property name="initialPoolSize" value="${initialPoolSize}"/>
<property name="maxIdleTime" value="${maxIdleTime}"/>
<property name="acquireIncrement" value="${acquireIncrement}"/>
<property name="maxStatements" value="${maxStatements}"/>
<property name="idleConnectionTestPeriod" value="${idleConnectionTestPeriod}"/>
</bean>
<!-- 3. 配置 JdbcTemplate -->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- 4. 组件扫描(Dao + Service) -->
<context:component-scan base-package="com.msb.dao,com.msb.service"/>
</beans>
2.5 什么是 JdbcTemplate?
JdbcTemplate 是 Spring 提供的一个模板类,封装了 JDBC 操作中的重复代码(如获取连接、处理异常、关闭资源),让我们只需要关注 SQL 语句和参数本身。
🧪 三、单元测试的三种写法演进
3.1 原始写法(每测一次都加载 Spring 容器)
public class SpringJdbcTest01 {
@Test
public void testQueryCount() {
ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
JdbcTemplate jdbcTemplate = (JdbcTemplate) ctx.getBean("jdbcTemplate");
String sql = "SELECT COUNT(1) FROM tb_account";
Integer total = jdbcTemplate.queryForObject(sql, Integer.class);
System.out.println("总记录数:" + total);
}
}
问题:每个测试方法都要重复加载配置文件、获取 Bean。
3.2 使用 @Before 优化(抽取公共代码)
public class SpringJdbcTest01 {
private JdbcTemplate jdbcTemplate;
@Before
public void init() {
ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
jdbcTemplate = (JdbcTemplate) ctx.getBean("jdbcTemplate");
}
@Test
public void testQueryCount() {
String sql = "SELECT COUNT(1) FROM tb_account";
Integer total = jdbcTemplate.queryForObject(sql, Integer.class);
System.out.println("总记录数:" + total);
}
}
3.3 Spring Test 终极封装(推荐 ⭐)
Step 1: 添加依赖 spring-test
Step 2: 创建通用父类 BaseTest
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:applicationContext.xml")
public class BaseTest {
// 所有测试类继承即可,无需重复写注解
}
Step 3: 子类直接注入使用
public class SpringJdbcTest03 extends BaseTest {
@Autowired
private JdbcTemplate jdbcTemplate;
@Test
public void testQueryCount() {
String sql = "SELECT COUNT(1) FROM tb_account";
Integer total = jdbcTemplate.queryForObject(sql, Integer.class);
System.out.println("总记录数:" + total);
}
@Test
public void testQueryCountByUserId() {
String sql = "SELECT COUNT(1) FROM tb_account WHERE user_id = ?";
Integer total = jdbcTemplate.queryForObject(sql, Integer.class, 1);
System.out.println("指定用户的账户数:" + total);
}
}
✅ 优点:无需手动加载配置文件,直接
@Autowired注入容器中的 Bean,代码最简洁。
📝 四、账户模块(tb_account)完整 CRUD 实战
4.1 建表语句(参考)
CREATE TABLE tb_account (
account_id INT PRIMARY KEY AUTO_INCREMENT,
account_name VARCHAR(50),
account_type VARCHAR(20),
money DOUBLE,
remark VARCHAR(100),
user_id INT,
create_time DATETIME,
update_time DATETIME
);
4.2 实体类 Account
创建 com.msb.pojo.Account:
public class Account {
private Integer accountId;
private String accountName;
private String accountType;
private Double money;
private String remark;
private Integer userId;
private Date createTime;
private Date updateTime;
// 构造器(不需要 accountId、createTime、updateTime,由数据库自动生成)
public Account(String accountName, String accountType, Double money,
String remark, Integer userId) {
this.accountName = accountName;
this.accountType = accountType;
this.money = money;
this.remark = remark;
this.userId = userId;
}
// getter/setter/toString 省略,请自行补充
}
4.3 Dao 接口定义
com.msb.dao.AccountDao:
public interface AccountDao {
// ========== 添加 ==========
int addAccount(Account account); // 返回受影响行数
int addAccountReturnKey(Account account); // 返回生成的主键
int addAccountBatch(List<Account> accounts); // 批量添加
// ========== 查询 ==========
int queryAccountCount(Integer userId); // 查询用户账户总数
Account queryAccountById(Integer accountId); // 根据主键查询
List<Account> queryAccountByParams(Integer userId, String accountName, String createTime); // 多条件查询
// ========== 更新 ==========
int updateAccountById(Account account); // 更新单条
int updateAccountBatch(List<Account> accounts); // 批量更新
// ========== 删除 ==========
int deleteAccountById(Integer accountId); // 删除单条
int deleteAccountBatch(Integer[] accountIds); // 批量删除
}
4.5 添加功能实现
@Repository
public class AccountDaoImpl implements AccountDao {
@Autowired
private JdbcTemplate jt;
// ===== 1. 添加单条记录 =====
@Override
public int addAccount(Account account) {
String sql = "INSERT INTO tb_account(account_name, account_type, money, remark, " +
"user_id, create_time, update_time) VALUES (?, ?, ?, ?, ?, NOW(), NOW())";
Object[] params = {
account.getAccountName(), account.getAccountType(),
account.getMoney(), account.getRemark(), account.getUserId()
};
return jt.update(sql, params);
}
// ===== 2. 添加记录并返回主键 =====
@Override
public int addAccountReturnKey(Account account) {
String sql = "INSERT INTO tb_account(account_name, account_type, money, remark, " +
"create_time, update_time, user_id) VALUES (?, ?, ?, ?, NOW(), NOW(), ?)";
KeyHolder keyHolder = new GeneratedKeyHolder();
jt.update(con -> {
PreparedStatement ps = con.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
ps.setString(1, account.getAccountName());
ps.setString(2, account.getAccountType());
ps.setDouble(3, account.getMoney());
ps.setString(4, account.getRemark());
ps.setInt(5, account.getUserId());
return ps;
}, keyHolder);
return keyHolder.getKey().intValue();
}
// ===== 3. 批量添加 =====
@Override
public int addAccountBatch(List<Account> accounts) {
String sql = "INSERT INTO tb_account(account_name, account_type, money, remark, " +
"create_time, update_time, user_id) VALUES (?, ?, ?, ?, NOW(), NOW(), ?)";
int[] result = jt.batchUpdate(sql, new BatchPreparedStatementSetter() {
@Override
public void setValues(PreparedStatement ps, int i) throws SQLException {
Account acc = accounts.get(i);
ps.setString(1, acc.getAccountName());
ps.setString(2, acc.getAccountType());
ps.setDouble(3, acc.getMoney());
ps.setString(4, acc.getRemark());
ps.setInt(5, acc.getUserId());
}
@Override
public int getBatchSize() {
return accounts.size();
}
});
return result.length;
}
// ========== 查询功能 ==========
// ===== 4. 查询用户账户总数 =====
@Override
public int queryAccountCount(Integer userId) {
String sql = "SELECT COUNT(1) FROM tb_account WHERE user_id = ?";
return jt.queryForObject(sql, Integer.class, userId);
}
// ===== 5. 根据主键查询详情 =====
@Override
public Account queryAccountById(Integer accountId) {
String sql = "SELECT * FROM tb_account WHERE account_id = ?";
// 使用 Lambda 表达式手写映射(了解即可,工作中常用 BeanPropertyRowMapper)
return jt.queryForObject(sql, (rs, rowNum) -> {
Account a = new Account();
a.setAccountId(rs.getInt("account_id"));
a.setAccountName(rs.getString("account_name"));
a.setAccountType(rs.getString("account_type"));
a.setMoney(rs.getDouble("money"));
a.setRemark(rs.getString("remark"));
a.setUserId(rs.getInt("user_id"));
a.setCreateTime(rs.getDate("create_time"));
a.setUpdateTime(rs.getDate("update_time"));
return a;
}, accountId);
}
// ===== 6. 多条件查询(动态拼接 SQL) =====
@Override
public List<Account> queryAccountByParams(Integer userId, String accountName, String createTime) {
String sql = "SELECT * FROM tb_account WHERE user_id = ?";
List<Object> params = new ArrayList<>();
params.add(userId);
if (accountName != null && !"".equals(accountName)) {
sql += " AND account_name LIKE CONCAT('%', ?, '%')";
params.add(accountName);
}
if (createTime != null && !"".equals(createTime)) {
sql += " AND create_time < ?";
params.add(createTime);
}
return jt.query(sql, (rs, rowNum) -> {
Account a = new Account();
a.setAccountId(rs.getInt("account_id"));
a.setAccountName(rs.getString("account_name"));
a.setAccountType(rs.getString("account_type"));
a.setMoney(rs.getDouble("money"));
a.setRemark(rs.getString("remark"));
a.setUserId(rs.getInt("user_id"));
a.setCreateTime(rs.getDate("create_time"));
a.setUpdateTime(rs.getDate("update_time"));
return a;
}, params.toArray());
}
// ========== 更新功能 ==========
// ===== 7. 更新单条记录 =====
@Override
public int updateAccountById(Account account) {
String sql = "UPDATE tb_account SET account_name = ?, account_type = ?, money = ?, " +
"remark = ?, user_id = ?, update_time = NOW() WHERE account_id = ?";
Object[] params = {
account.getAccountName(), account.getAccountType(), account.getMoney(),
account.getRemark(), account.getUserId(), account.getAccountId()
};
return jt.update(sql, params);
}
// ===== 8. 批量更新 =====
@Override
public int updateAccountBatch(List<Account> accounts) {
String sql = "UPDATE tb_account SET account_name = ?, account_type = ?, money = ?, " +
"remark = ?, user_id = ?, update_time = NOW() WHERE account_id = ?";
int[] result = jt.batchUpdate(sql, new BatchPreparedStatementSetter() {
@Override
public void setValues(PreparedStatement ps, int i) throws SQLException {
Account acc = accounts.get(i);
ps.setString(1, acc.getAccountName());
ps.setString(2, acc.getAccountType());
ps.setDouble(3, acc.getMoney());
ps.setString(4, acc.getRemark());
ps.setInt(5, acc.getUserId());
ps.setInt(6, acc.getAccountId());
}
@Override
public int getBatchSize() {
return accounts.size();
}
});
return result.length;
}
// ========== 删除功能 ==========
// ===== 9. 删除单条 =====
@Override
public int deleteAccountById(Integer accountId) {
String sql = "DELETE FROM tb_account WHERE account_id = ?";
return jt.update(sql, accountId);
}
// ===== 10. 批量删除 =====
@Override
public int deleteAccountBatch(Integer[] accountIds) {
String sql = "DELETE FROM tb_account WHERE account_id = ?";
int[] result = jt.batchUpdate(sql, new BatchPreparedStatementSetter() {
@Override
public void setValues(PreparedStatement ps, int i) throws SQLException {
ps.setInt(1, accountIds[i]);
}
@Override
public int getBatchSize() {
return accountIds.length;
}
});
return result.length;
}
}
4.6 JdbcTemplate 核心方法速查
| 方法 | 用途 | 示例 |
|---|---|---|
jt.update(sql, params) | 执行插入、更新、删除 | jt.update("INSERT INTO ...", obj1, obj2) |
jt.queryForObject(sql, Class, params) | 查询单行单列 | jt.queryForObject("SELECT COUNT(*)", Integer.class) |
jt.queryForObject(sql, RowMapper, params) | 查询单行映射为对象 | jt.queryForObject("SELECT * FROM ...", (rs, i) -> new Object(), id) |
jt.query(sql, RowMapper, params) | 查询多行映射为集合 | jt.query("SELECT * FROM ...", (rs, i) -> new Object()) |
jt.batchUpdate(sql, BatchPreparedStatementSetter) | 批量插入/更新/删除 | 见上方代码 |
🔥 五、Spring 事务控制(核心重点)
5.1 转账案例 - 引出事务
假设有一个转账业务:账户 A 给账户 B 转 100 元
需要执行两步操作:
- A 账户余额 -100
- B 账户余额 +100
Dao 层定义
@Repository
public class AccountDaoImpl implements AccountDao {
@Autowired
private JdbcTemplate jt;
// 支出(扣钱)
@Override
public int outAccount(Integer accountId, Double money) {
String sql = "UPDATE tb_account SET money = money - ? WHERE account_id = ?";
return jt.update(sql, money, accountId);
}
// 收入(加钱)
@Override
public int inAccount(Integer accountId, Double money) {
String sql = "UPDATE tb_account SET money = money + ? WHERE account_id = ?";
return jt.update(sql, money, accountId);
}
}
Service 层 - 没有事务的转账(有 Bug)
@Service
public class AccountService {
@Autowired
private AccountDao ad;
public int transfer(Integer outId, Integer inId, Double money) {
int code = 0;
// 1. 扣钱
ad.outAccount(outId, money);
// 2. 模拟异常!!!
int a = 1 / 0; // ArithmeticException
// 3. 加钱(因为异常,这一步执行不到)
ad.inAccount(inId, money);
code = 1;
return code;
}
}
运行结果: A 账户扣了 100 元,B 账户没收到,100 元凭空消失 ❌
原因: MySQL 默认是自动提交模式,每执行一条 SQL 就立即生效。outAccount 执行完后已经提交了,后面抛异常也无法撤销。
5.2 Spring 声明式事务 - 注解方式(⭐ 工作必用)
Step 1:配置事务管理器
在 applicationContext.xml 中添加:
<!-- 配置事务管理器 -->
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- 开启注解事务驱动 -->
<tx:annotation-driven/>
Step 2:在 Service 方法上加 @Transactional
@Service
public class AccountService {
@Autowired
private AccountDao ad;
@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
public int transfer(Integer outId, Integer inId, Double money) {
int code = 0;
ad.outAccount(outId, money);
// 即使这里抛异常,outAccount 也会回滚
// int a = 1 / 0;
ad.inAccount(inId, money);
code = 1;
return code;
}
}
✅ 加上
@Transactional后,扣钱和加钱在同一个事务中,要么都成功,要么都失败。
5.3 声明式事务 - XML 方式(了解即可)
老项目中还存在,需要能看懂。
<!-- 配置事务通知 -->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<!-- 以 update 开头的方法都加事务 -->
<tx:method name="update*" propagation="REQUIRED" rollback-for="Exception.class"/>
<!-- 查询方法设置为只读,优化性能 -->
<tx:method name="select*" read-only="true"/>
<tx:method name="query*" read-only="true"/>
</tx:attributes>
</tx:advice>
<!-- 配置 AOP 切面 -->
<aop:config>
<aop:pointcut id="servicePt" expression="execution(* com.msb.service.*.*(..))"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="servicePt"/>
</aop:config>
⚠️ 注意:XML 方式需要导入 aspectjweaver 依赖,并且方法内部不能 try-catch 吞掉异常,否则 AOP 代理无法捕获异常,事务不会回滚。
5.4 事务核心属性详解
事务传播行为 - propagation
| 传播行为 | 含义 | 使用场景 |
|---|---|---|
| REQUIRED(默认) | 有事务就加入,没有就新建 | 99% 的业务方法 |
| REQUIRES_NEW | 总是新建事务,挂起旧事务 | 操作日志:主业务失败,日志也要保存 |
| SUPPORTS | 有事务就用,没有就非事务执行 | 查询方法 |
| MANDATORY | 必须已有事务,否则抛异常 | 强制要求调用方开启事务 |
| NOT_SUPPORTED | 非事务执行,有事务则挂起 | 不需要事务的特定操作 |
| NEVER | 必须非事务执行,有事务则抛异常 | 严格禁止在事务中执行 |
| NESTED | 嵌套事务 | 部分回滚场景,极少用 |
事务隔离级别 - isolation
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 常用场景 |
|---|---|---|---|---|
| READ_UNCOMMITTED | ✅ 会 | ✅ 会 | ✅ 会 | 几乎不用 |
| READ_COMMITTED | ❌ 不会 | ✅ 会 | ✅ 会 | 大多数业务系统 |
| REPEATABLE_READ | ❌ 不会 | ❌ 不会 | ✅ 会 | MySQL 默认 |
| SERIALIZABLE | ❌ 不会 | ❌ 不会 | ❌ 不会 | 极少使用 |
日常开发直接用 Isolation.DEFAULT,让数据库决定即可。
其他常用参数
| 参数 | 作用 | 推荐值 |
|---|---|---|
timeout | 超时时间(秒),超时自动回滚 | timeout = 3 |
readOnly | 是否只读事务,优化查询性能 | readOnly = true(查询方法) |
rollbackFor | 指定哪些异常触发回滚 | rollbackFor = Exception.class(强烈推荐) |
noRollbackFor | 指定哪些异常不触发回滚 | 极少配置 |
5.5 事务失效的 5 种经典场景(避坑指南 ⭐)
| 场景 | 错误写法 | 正确姿势 |
|---|---|---|
方法不是 public | @Transactional private void transfer() | 必须是 public 方法 |
异常被 try-catch 吞了 | catch(Exception e){ e.printStackTrace(); } | 要么不 catch,要么 catch 后手动 setRollbackOnly() |
| 同类方法调用 | this.methodB() | 拆到另一个 Service 注入调用 |
| 数据库引擎不支持事务 | MySQL 用了 MyISAM 引擎 | 必须用 InnoDB 引擎 |
| 事务管理器未配置 | 没配置 DataSourceTransactionManager | 检查 Spring 配置文件 |
5.6 同类方法调用事务失效详解
@Service
public class UserService {
@Transactional
public void methodA() {
// 业务操作
this.methodB(); // ❌ REQUIRES_NEW 不生效!
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void methodB() {
// 记录日志
}
}
原因: Spring 事务通过代理对象实现,this.methodB() 走的是原始对象,绕过了代理。
✅ 正确写法:拆到另一个 Service
@Service
public class UserService {
@Autowired
private LogService logService; // 注入另一个 Service
@Transactional
public void methodA() {
// 业务操作
logService.saveLog(); // ✅ 通过代理调用,事务生效
}
}
@Service
public class LogService {
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
public void saveLog() {
// 独立事务,主业务失败也不影响
}
}
🎯 六、总结
Spring JDBC 部分
| 核心点 | 说明 |
|---|---|
| JdbcTemplate | Spring 封装的 JDBC 模板类,简化了 CRUD 操作 |
| C3P0 连接池 | 提高性能,避免频繁创建/销毁连接 |
| query/update | 查询用 query 系列,增删改用 update |
| RowMapper | 结果集映射,工作中常用 BeanPropertyRowMapper 自动映射 |
| 批量操作 | 使用 batchUpdate + BatchPreparedStatementSetter |
Spring 事务部分
| 核心点 | 说明 |
|---|---|
| 事务的作用 | 保证一组操作要么全成功,要么全失败 |
| ACID | 原子性、一致性、隔离性、持久性 |
| 声明式事务 | 用 @Transactional 注解,工作中主流 |
| 默认传播行为 | REQUIRED:有事务就加入,没有就新建 |
| 默认回滚规则 | 只回滚 RuntimeException |
| 最佳实践 | @Transactional(rollbackFor = Exception.class) |
| 事务失效 | 非 public、try-catch 吞异常、同类调用 |
📌 日常开发最佳实践模板
@Service
public class OrderService {
@Autowired
private OrderDao orderDao;
@Autowired
private LogService logService;
@Transactional(rollbackFor = Exception.class)
public void createOrder(Order order) {
// 1. 保存订单
orderDao.save(order);
// 2. 扣减库存
orderDao.deductStock(order.getProductId(), order.getQuantity());
// 3. 记录日志(独立事务)
logService.saveLog("用户下单,订单号:" + order.getId());
}
}
@Service
public class LogService {
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
public void saveLog(String message) {
logDao.save(message);
}
}
:Spring JDBC 与事务控制&spm=1001.2101.3001.5002&articleId=163662938&d=1&t=3&u=8e3f3a7cfc7a4f078403efcbf0c1fa12)
357

被折叠的 条评论
为什么被折叠?



