Java Spring -- 声明式事务控制

一. 基于xml声明式事务控制

1.1 快速入门

在早期的 Spring 项目中,XML 是配置事务的主流方式。它的核心思想是通过 AOP 切面,将事务逻辑"织入"到业务方法中。

  1. 配置事务管理器:事务管理器是 Spring 事务管理的核心,负责事务的开启、提交和回滚。对于 JDBC 或 MyBatis,通常使用 DataSourceTransactionManager:
xml 复制代码
<!-- 配置事务管理器 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource"/>
</bean>
  1. 配置事务通知(Advice):事务通知定义了"哪些方法需要什么样的事务属性"。我们可以针对不同的方法名(如 save*, update*, get*)配置不同的事务规则:
xml 复制代码
<!-- 配置事务通知 -->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
    <tx:attributes>
        <!-- 增删改方法:开启REQUIRED事务,遇到Exception即回滚 -->
        <tx:method name="save*" propagation="REQUIRED" rollback-for="java.lang.Exception"/>
        <tx:method name="update*" propagation="REQUIRED" rollback-for="java.lang.Exception"/>
        <tx:method name="delete*" propagation="REQUIRED" rollback-for="java.lang.Exception"/>
        <!-- 查询方法:只读事务,提升性能 -->
        <tx:method name="get*" read-only="true" propagation="SUPPORTS"/>
    </tx:attributes>
</tx:advice>
  1. 配置 AOP 切面织入:通过 AOP 将事务通知与切点表达式结合,应用到具体的 Service 层:
xml 复制代码
<!-- 配置 AOP 织入 -->
<aop:config>
    <!-- 切点:匹配 service 包下所有类的所有方法 -->
    <aop:pointcut id="servicePointcut" expression="execution(* com.example.service.*.*(..))"/>
    <!-- 将事务通知与切点结合 -->
    <aop:advisor advice-ref="txAdvice" pointcut-ref="servicePointcut"/>
</aop:config>

核心本质: XML 方式其实是将事务管理作为了一个普通的 AOP 切面。< tx:advice> 本质上就是一个增强逻辑,而 < aop:advisor> 则负责将其织入到目标方法中。

1.2 配置详解

以下是 < tx:method> 标签的核心属性详解:

属性 说明 默认值
name 指定事务生效的方法名(支持通配符,如 save*, update* 无(必填)
propagation 事务的传播行为(如 REQUIRED, REQUIRES_NEW REQUIRED
isolation 事务的隔离级别(如 DEFAULT, READ_COMMITTED DEFAULT
timeout 事务超时时间(单位:秒),-1 表示永不超时 -1
read-only 是否为只读事务(查询方法建议设为 true,可优化数据库性能) false
rollback-for 触发事务回滚的异常类(全限定类名,如 java.lang.Exception RuntimeException
no-rollback-for 不触发事务回滚的异常类
  1. 事务传播行为(Propagation)

这是事务配置中最核心的属性,它定义了当一个事务方法被另一个事务方法调用时,事务应该如何传播。最常用的有两种:

  • REQUIRED(默认):如果当前存在事务,则加入该事务;如果不存在事务,则创建一个新事务。这是最常见的选择。
  • REQUIRES_NEW:无论当前是否存在事务,都会挂起当前事务,并创建一个全新的独立事务。常用于日志记录等需要独立提交的场景。
  1. 事务隔离级别(Isolation)

它定义了事务之间的可见性,用于解决并发事务带来的脏读、不可重复读和幻读问题。通常使用数据库默认的隔离级别(DEFAULT)即可。

隔离级别 说明 并发问题解决情况
DEFAULT 默认隔离级别,取决于当前数据库的隔离级别(例如 MySQL 默认是 REPEATABLE_READ) -
READ_UNCOMMITTED A 事务可以读取到 B 事务尚未提交的事务记录(脏读) 不能解决任何并发问题,安全性最低,性能最高
READ_COMMITTED A 事务只能读取到其他事务已经提交的记录,不能读取到未提交的记录 可以解决脏读问题,但不能解决不可重复读和幻读
REPEATABLE_READ A 事务多次从数据库读取某条记录结果一致 可以解决脏读和不可重复读问题,不可以解决幻读
SERIALIZABLE 串行化,强制事务串行执行 可以解决所有并发问题(脏读、不可重复读、幻读),安全性最高,但性能最低
  1. 回滚规则(Rollback Rules)

Spring 默认只在遇到 RuntimeException 和 Error 时回滚。如果业务中抛出了受检异常(Checked Exception),务必通过 rollback-for 显式指定,否则事务不会回滚,可能导致数据不一致。

1.3 配置原理

Spring 声明式事务的本质是 AOP。在 XML 配置方式下,Spring 是如何将 < tx:advice> 标签转化为底层的事务增强逻辑的呢?这主要依赖于 Spring 的自定义命名空间解析机制。

  1. 命名空间处理器:TxNamespaceHandler

当 Spring 容器解析到 < tx:advice> 等事务标签时,会首先找到对应的命名空间处理器 TxNamespaceHandler。在其 init() 方法中,注册了专门用于解析 < tx:advice> 标签的解析器:

java 复制代码
public class TxNamespaceHandler extends NamespaceHandlerSupport {
    @Override
    public void init() {
        // 注册 advice 标签的解析器
        this.registerBeanDefinitionParser("advice", new TxAdviceBeanDefinitionParser());
        // ... 其他标签解析器
    }
}
  1. 核心解析器:TxAdviceBeanDefinitionParser

TxAdviceBeanDefinitionParser 负责将 XML 中的 < tx:advice> 标签解析为 Spring 容器内部的 BeanDefinition。它最核心的作用是指定了要注册的 Bean 类型:

java 复制代码
public class TxAdviceBeanDefinitionParser extends AbstractBeanDefinitionParser {
    @Override
    protected Class<?> getBeanClass(Element element) {
        // 核心:将 <tx:advice> 标签解析为 TransactionInterceptor(事务拦截器)
        return TransactionInterceptor.class;
    }
}
  1. 注册到 Spring 容器

TxAdviceBeanDefinitionParser 继承了 AbstractBeanDefinitionParser,在其 parse() 方法中,会将解析生成的 TransactionInterceptor 以配置的名称(如 txAdvice)注册到 Spring 容器中:

java 复制代码
// AbstractBeanDefinitionParser 中的核心逻辑
parserContext.registerComponent(componentDefinition);

通过上述源码流程可以看出,我们在 XML 中配置的 < tx:advice>,在 Spring 容器启动时,本质上被转换成了一个TransactionInterceptor(事务拦截器) 并注册到了容器中。这个拦截器会被织入到目标 Service方法的代理对象中,从而在方法执行前后自动完成事务的开启、提交与回滚。

二. 基于注解声明式事务控制

2.1 快速入门

  1. 开启注解驱动:在配置类上添加 @EnableTransactionManagement 注解(如果是 Spring Boot 项目,通常会自动开启,无需手动添加):
java 复制代码
@Configuration
@EnableTransactionManagement // 开启声明式事务支持
public class SpringConfig {
    // 配置数据源和事务管理器...
}
  1. 在业务方法上添加 @Transactional,直接在需要事务管理的 Service 方法或类上添加 @Transactional 注解:
java 复制代码
@Service
public class UserServiceImpl implements UserService {

    @Autowired
    private UserDao userDao;

    @Override
    @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
    public void transfer(String from, String to, BigDecimal money) {
        // 1. 扣款
        userDao.deduct(from, money);
        // 2. 模拟异常,测试事务回滚
        int i = 1 / 0; 
        // 3. 加款
        userDao.add(to, money);
    }
}

2.2 配置原理

与 XML 方式类似,注解方式的底层依然是 AOP 动态代理。但它的触发机制和核心组件有所不同。

  1. 核心驱动注解:@EnableTransactionManagement

这个注解是注解方式声明式事务的入口。它通过 @Import(TransactionManagementConfigurationSelector.class) 导入了一个配置选择器,该选择器会向容器中注册两个核心 Bean:

  • AutoProxyRegistrar:负责注册自动代理创建器。
  • ProxyTransactionManagementConfiguration:负责注册事务拦截器。
  1. 核心组件:BeanFactoryTransactionAttributeSourceAdvisor

与 XML 方式直接注册 TransactionInterceptor 不同,注解方式会向容器中注册一个名为 BeanFactoryTransactionAttributeSourceAdvisor 的切面(Advisor)。

这个切面包含了两个关键部分:

  • Pointcut(切点):匹配所有被 @Transactional 注解修饰的方法。
  • Advice(通知):即 TransactionInterceptor(事务拦截器),负责执行具体的事务逻辑。
  1. 代理对象的生成与执行流程

结合我们之前学过的 AOP 原理,注解方式的底层执行流程如下:

  • 生成代理对象:Spring 容器在初始化 Service Bean 时,AnnotationAwareAspectJAutoProxyCreator 发现它匹配了BeanFactoryTransactionAttributeSourceAdvisor 的切点,于是生成代理对象。
  • 拦截方法调用:当外部调用 Service 的方法时,实际调用的是代理对象,调用会被转发给 TransactionInterceptor。
  • 获取事务属性:拦截器会解析当前方法上的 @Transactional 注解,将其转换为事务属性(TransactionAttribute)。
  • 开启事务:拦截器通过 PlatformTransactionManager 获取数据库连接,并开启事务。
  • 执行业务逻辑:拦截器通过反射调用真实目标对象的业务方法。
  • 提交或回滚
    • 如果业务方法正常返回,拦截器会提交事务。
    • 如果业务方法抛出异常,拦截器会根据 rollbackFor 等规则判断是回滚事务还是提交事务。

2.3 事务失效场景

在开发中,很多时候我们明明加了 @Transactional 注解,但报错时数据却没有回滚。这通常是因为触发了 Spring 事务失效的"隐形规则"。以下是三种最典型的失效场景:

场景一:异常被"吞"掉了

这是新手最容易犯的错误。如果在 Service 层手动 try-catch 了异常,且没有再次抛出,Spring 事务管理器就"感知"不到异常的发生,自然也就不会执行回滚操作。

** 错误代码示例 **

java 复制代码
@Transactional
public void saveUser(User user) {
    try {
        // 数据库插入操作
        userMapper.insert(user);
        // 模拟抛出异常
        int i = 1 / 0; 
    } catch (Exception e) {
        // 错误:仅仅打印日志,没有抛出异常
        // Spring 认为方法正常执行结束,会提交事务!
        log.error("保存用户失败", e); 
    }
}

** 修复方案 **

catch 块中处理完日志后,必须手动抛出运行时异常,或者抛出原异常:

java 复制代码
} catch (Exception e) {
    log.error("保存用户失败", e);
    throw new RuntimeException("保存用户失败"); // 必须抛出异常,触发回滚
}
场景二:同类内部方法调用(this 调用)

Spring 的事务是基于 AOP 动态代理 实现的。只有当外部调用经过代理对象(Proxy)时,事务拦截器才会生效。如果在同一个类中,用 this 调用本类的方法,是直接调用目标对象(Target)的方法,绕过了代理对象,事务就会失效。

** 错误代码示例 **

java 复制代码
@Service
public class UserService {

    public void methodA() {
        // 错误:使用 this 调用,相当于直接调用原始对象的方法,AOP 拦截不到
        this.methodB(); 
    }

    @Transactional
    public void methodB() {
        // 数据库操作
        userMapper.insert(new User());
        throw new RuntimeException("出错了");
    }
}

** 修复方案 **

方案 1(推荐): 注入自己(自注入),通过代理对象调用。

java 复制代码
@Service
public class UserService {
    
    @Autowired
    private UserService self; // 注入自己

    public void methodA() {
        self.methodB(); // 通过代理对象调用,事务生效
    }

    @Transactional
    public void methodB() {
        // ...
    }
}

方案 2: 使用 AopContext.currentProxy() 获取当前代理对象(需要在启动类开启 exposeProxy = true)。

场景三:方法不是 public 修饰的

@Transactional 注解只能应用在 public 方法上。如果应用在 privateprotected 或默认(default)修饰的方法上,Spring 在解析时会直接忽略该注解,导致事务失效,而且通常不会报错,非常隐蔽。

** 错误代码示例 **

java 复制代码
@Service
public class UserService {

    @Transactional
    private void saveUser() { // 错误:private 修饰,事务无效
        userMapper.insert(new User());
        throw new RuntimeException("出错了");
    }
}

** 原因分析 **

Spring AOP 底层在计算事务属性时(computeTransactionAttribute),会检查方法的修饰符。源码逻辑大致如下:

java 复制代码
if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
    return null; // 非 public 方法直接返回 null,事务配置失效
}
相关推荐
爱编程的小新☆36 分钟前
【LeetCode】从递归到 Flood Fill:5 道题吃透 DFS 的选择、回溯与标记
java·算法·leetcode·深度优先·回溯·flood fill
weixin_3831964743 分钟前
java基础面试题@Autowired和@Resource区别
java·开发语言
evans在进步1 小时前
LeetCode 33:搜索旋转排序数组——Java 两阶段二分查找详解
java·python·leetcode
Web Security Loop1 小时前
MAC卸载JDK环境
java·macos·jdk
Rain的Java大神之路2 小时前
线上接口负载满了如何解决
java·数据库·redis·后端·缓存·面试·架构
counting money3 小时前
Tomcat 专题:从基础架构到性能调优与 WebSocket 实战
java·websocket·tomcat
xiaohaiAIgeo3 小时前
【2026年】HG/T 20656-2024化工暖通空调设计规范:新版标准的变化与影响
java·前端·javascript·科普知识
小强库计算机毕业设计4 小时前
基于 Spring Boot + Vue3 的校园管理系统
java·spring boot·后端·项目实战·管理系统·技术分享·校园管理系统实战
霸道流氓气质4 小时前
Java中信号量(Semaphore):从本地到分布式
java·开发语言·分布式