Spring提供了数据库事务管理,只需要在含有数据库层操作的方法或类上使用注解@Transactional,Spring会自动帮我们管理数据库事务。比如当数据库操作逻辑执行发生异常后,Spring会将当前的事务回滚。

当我们在使用@Transactiona完成Spring 事务管理的时候,有时会出现由于某些细节没有掌握、使用不当,从而导致Spring没有实现事务管理功能。导致Spring事务失效的场景有很多,今天我们只是从动态代理的角度去探究导致Spring事务失效的原因。

Spring使用注解@Transactional变能够完成事务管理,其功能是基于Spring AOP实现的。而Spring AOP又是基于动态代理实现的。所以可以站在动态代理的角度聊一聊Spring事务失效的原因。

在Java中实现动态代理的方式,有JDK动态代理和Cglib动态代理。两者之间的实现方式是不同的,但是思想是一致的,即通过构建一个对象的子类,从而将子类作为该对象的代理,在子类中实现增强的逻辑。JDK动态代理是面向接口的,通过实现接口(implements)来创建子类。Cglib是面向类的,通过继承(extends)来创建子类。(关于详细的动态代理介绍,请戳《 JDK动态代理 》、《 CGLIB动态代理 》)

下面通过简单的代码来模拟Spring事务管理的代理类。当我们在一个方法上标注@Transactional,Spring会使用动态代理,为该类生成一个代理对象,然后为标注了@Transactional注解的方法实现增强。怎么实现增强呢,即在该方法调用前后进行处理。代码如下:

在类UserOperation中进行数据库的增删改查操作。其中insertUser方法使用@Transactional注解,otherMethod没有使用注解。

public class UserOperation {
    @Transactional
    public void insertUser() {
        // 在这里进行用户新增的逻辑处理
    public void otherMethod() {
        // 其他方法逻辑处理

Spring为UserOperation会生成一个代理对象,可以理解成如下形式:

代理对象通过继承UserOperation类,这样代理对象将也会持有UserOperation中的方法。在代理对象中重写insertUser方法,在UserOperation对象调用实际的insertUser方法前后实现开启事务,正确执行完提交事务,有异常时回滚事务的逻辑。

public class UserOperationProxy extends UserOperation {
    @Override
    public void insertUser() {
        // 开启事务
        try {
            super.insertUser();
            // 提交事务
        } catch (Exception e) {
            // 回滚事务

上面的代理对象的内容只是为了方便理解简单的写了下,实际的内容要比这多很多。而且在代理对象中并不是直接调用被代理对象的方法,而是通过一个Handler进行转发,在Handler中持有被代理对象的引用,通过反射去调用被代理对象的方法以及调用前后逻辑的增强。

聊了那么多动态代理,现在开始说说Spring事务失效的场景。

(1)通过手动new对象去调用方法。

当你获取的对象不是从Spring容器中获取(比如使用@Autowired自动注入),而是通过new关键字创建的对象。此时当使用该对象调用注解方法的时候,Spring事务是失效的。Spring事务管理是通过代理对象去实现的,所以只有通过代理对象去调用方法才能生效。

(2)私有方法

在上面说过,Spring事务管理本质上是通过动态代理实现的,而动态代理无非就两种方式,实现接口或继承类。在Java中会存在方法可见性的问题,当方法使用private修饰的时候,该方法仅当前类的内部可见。子类是不可见的。所以当继承该类的时候,子类是不可见该类的私有方法的,也就是代理对象是无法持有这个私有方法的。自然也就无法通过代理对象调用该方法了。

(3)rollbackFor没有指定的异常

通过rollbackFor指定了起作用的异常,当方法抛出此异常的时候,Spring会回滚事务。当抛出了之外的其他异常,Spring事务失效。通过代码的方式更好理解:

比如通过rollbackFor指定管理的异常有NullPointerException、ArithmeticException,可以理解成Spring为我们生成的代理对象如下,在try...catch中,只会捕获这两种异常,对于其他的异常会被抛出去,交给调用程序处理。

@Override
public void insertUser() {
    // 开启事务
    try {
        super.insertUser();
        // 提交事务
    } catch (NullPointerException e) {
        // 回滚事务
    } catch (ArithmeticException e) {
        // 回滚事务

(4)方法没有抛出异常

当在注解方法中使用了try...catch捕获所有的异常,方法并没有将异常抛出,这样该方法的调用程序是无法感知到异常发生的,因为异常已经在方法内被捕获了。该方法的调用程序感知不到异常,也就是代理对象感知不到异常,代理对象就会认为方法正确执行了,就会将事务提交,然而实际并非无此。

事务是一组由于逻辑上紧密关联而合并成一个整体的多个数据操作,这些操作要么都执行,要么都不执行。事务有以下4个特性(ACID): 原子性(Atomicity):操作这些指令时,要么全部执行成功,要么全部不执行。只要其中一个指令执行失败,所有的指令都执行失败,数据进行回滚,回到执行指令前的数据状态。 一致性(Consistency):事务的执行使数据从一个状态转换为另一个状态,但是对于整个数据的完整性保持稳定。 隔离性(Isolation):隔离性是当多个用户并发访问数据库时,比如操作同一张表 默认java动态代理, 相同的接口,内部有个属性是原object,故A内部调用B方法,B方法是代理前的逻辑. cglib是一个子类, 故A内部调用B方法,B方法也是代理后的逻辑. 附录两篇文章: Spring AOP注解失效的坑及JDK动态代理 Spring经过cglib proxy之后的class取注解 spring 有五个事务隔离级别:ISOLATION_DEFAULT、ISOLATION_READ_UNCOMMITTED、ISOLATION_READ_COMMITTED、ISOLATION_REPEATABLE_READ、ISOLATION_SERIALIZABLE 第一种是 Spring 默认使用 DB设置的事务隔离级别,后面四种事务隔离级别跟 Mysql 的事务隔离级别一致,下面就类比着 Mysql 的事务隔离级别,进行分析!!! 事务并发可能产生的问题释义 脏读 ( Dirty Read
Spring有两种动态代理实现方式 java动态代理:利用反射机制生成一个实现代理接口的匿名类,在调用具体方法前调用InvokeHandler来处理。 cglib动态代理:利用asm开源包,对代理对象类的class文件加载进来,通过修改其字节码生成子类来处理 JDK动态代理CGLIB字节码生成的区别: (1)JDK动态代理只能对实现了接口的类生成代理,而不能针对类 (2)CGLIB是针对类实现代理,主要是对指定的类生成一个子类,覆盖其中的方法【】因为是继承,所以该类或方法最好不要声明成final 1、数据库引擎不支持事务 这里以 MySQL 为例,其 MyISAM 引擎是不支持事务操作的,InnoDB 才是支持事务的引擎,一般要支持事务都会使用 InnoDB。 根据 MySQL 的官方文档: https://dev.mysql.com/doc/refman/5.5/en/storage-en...
Spring在程序运行期,就能帮助我们把切面中的代码织入Bean的方法内,让开发者能无感知地在容器对象方法前后随心添加相应处理逻辑,所以AOP其实就是个代理模式。 但凡是代理,由于代码不可直接阅读,也是初级程序员们 bug 的重灾区。 使用this方法,调用本类的某个方法,统计该方法的调用时间 @Service public class DoSomething(){ public void work(){ this.do(); public v
Spring框架中提供了两种动态代理方式:CGLIBJDK动态代理CGLIB是一个强大的高性能的代码生成库,它可以在运行时动态生成字节码,从而实现对类的动态代理CGLIB可以代理没有实现接口的类,因此它比JDK动态代理更加灵活,但是它的代理速度相对较慢。 JDK动态代理是Java自带的动态代理机制,它只能代理实现了接口的类,但是它的代理速度相对较快。JDK动态代理使用反射机制实现代理,因此它的代理对象必须实现一个或多个接口。 在使用Spring框架时,我们可以根据需要选择使用CGLIBJDK动态代理来实现对类的动态代理。如果被代理的类没有实现接口,我们可以选择使用CGLIB动态代理;如果被代理的类实现了接口,我们可以选择使用JDK动态代理