Spring 学习笔记(三):Spring JDBC 与事务控制

本文是 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 元

需要执行两步操作:

  1. A 账户余额 -100
  2. 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 部分

核心点说明
JdbcTemplateSpring 封装的 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);
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值