Spring - @Transactional - 后台发生了什么?

443

我想知道在一个方法上添加 @Transactional 注解时实际发生了什么?

当然,我知道 Spring 会将该方法包装在一个事务中。

但是,我有以下疑问:

  1. 我听说 Spring 会创建一个代理类?有人可以更深入地解释一下吗?代理类中实际保存了什么?原始类发生了什么变化?如何查看 Spring 创建的代理类?
  2. 我也在 Spring 文档中读到过:

注意:由于此机制基于代理,只有通过代理进行的“外部”方法调用才会被拦截。这意味着“自我调用”,即目标对象中的方法调用目标对象的其他方法,即使调用的方法被标记为 @Transactional,也不会导致实际运行时的事务!

来源:http://static.springsource.org/spring/docs/2.0.x/reference/transaction.html

为什么只有外部方法调用会处于事务之下,而自我调用方法不会?


3
相关讨论在这里:https://dev59.com/63A75IYBdhLWcg3wxb_V#3120323 - dma_k
6个回答

321
这是一个大的主题。Spring的参考文档专门为此撰写了多个章节。我建议阅读有关面向切面编程事务的章节,因为Spring的声明式事务支持使用AOP作为其基础。
但是从很高的层次来看,Spring会为在类本身或成员上声明@Transactional的类创建代理。该代理在运行时大部分是不可见的。它提供了一种方式,使得Spring可以在调用对象之前、之后或周围注入行为。事务管理只是可以挂接的行为的一个例子。安全检查是另一个例子。您也可以为诸如日志记录之类的内容提供自己的行为。所以当您使用@Transactional注释方法时,Spring会动态地创建一个代理,该代理实现与您正在注释的类相同的接口。当客户端调用您的对象时,通过代理机制拦截调用并注入行为。
顺便说一下,EJB中的事务也是类似工作的。
正如您所观察到的,代理机制仅在从某些外部对象进行调用时起作用。当您在对象内部进行内部调用时,实际上是通过this引用进行调用,这会绕过代理。然而,有办法解决这个问题。我在这个论坛帖子中解释了一种方法,其中我使用BeanFactoryPostProcessor在运行时向“自引用”类注入代理实例。我将此引用保存到名为me的成员变量中。然后,如果我需要进行需要更改线程事务状态的内部调用,我将调用指向代理(例如me.someMethod())。该论坛帖子中有更详细的解释。
请注意,BeanFactoryPostProcessor代码现在可能略有不同,因为它是在Spring 1.x时期编写的。但是希望它能给您一个想法。我有一个更新的版本,可以考虑提供。

8
代理在运行时大多数时候是不可见的。 - peakit
21
没问题。如果您使用调试器逐步执行代码,就可以看到代理代码了。那可能是最简单的方法。没有什么魔法,它们只是Spring包中的类。 - Rob H
1
我发现“me”方案也可以(使用显式的连线方式,因为这符合我的思维方式),但我认为如果你用这种方式做,最好重构一下,这样就不必这么做了。但是,有时候这可能非常棘手! - Donal Fellows
@Transactional 是否也会关闭通过 sessionFactory.getCurrentSession() 打开的会话? - kapad
4
由于这个答案已经有段时间了,所以所提到的论坛帖子现在已不再可用,该帖子描述的情况是当您需要在对象内部进行 不绕过代理 的内部调用时,使用 BeanFactoryPostProcessor。然而,在我看来,这个回答中描述的方法非常相似: https://dev59.com/a2s05IYBdhLWcg3wDtpn#11277899 ...还有整个帖子中的其他解决方案。 - Oliver
显示剩余3条评论

243
当Spring加载bean定义并配置为查找 @Transactional 注释时,它将在实际的< strong> bean 周围创建这些< strong>代理对象。这些代理对象是在运行时自动生成的类的实例。当调用方法时,这些代理对象的默认行为是仅在 "目标" bean(即您的 bean)上调用相同的方法。
但是,代理也可以提供拦截器,当存在时,这些拦截器将在代理调用目标 bean 的方法之前由代理调用。对于使用@Transactional注释的目标 bean,Spring将创建一个TransactionInterceptor,并将其传递给生成的代理对象。因此,当你从客户端代码调用方法时,你是在调用代理对象上的方法,该代理对象首先调用TransactionInterceptor(开始一个事务),然后调用目标 bean 上的方法。当调用完成时,TransactionInterceptor 提交/回滚事务。客户端代码对此毫不知情。
至于 "外部方法" 问题,如果您的 bean 调用自己的方法,则它不会通过代理这样做。请记住,Spring 在代理中包装了您的 bean,而您的 bean 对此一无所知。只有来自 "外部" bean 的调用才会经过代理。
这有帮助吗?

55
记住,Spring会用代理包装你的bean,而你的bean并不知道它被包装了。 这句话说明了一切。非常好的答案。感谢您的帮助。 - peakit
2
很好的解释,关于代理和拦截器。现在我明白了Spring是如何实现一个代理对象来拦截对目标Bean的调用。谢谢! - dharag
2
我认为你正在描述Spring文档中的这张图片,看到这张图片对我很有帮助:https://docs.spring.io/spring/docs/4.2.x/spring-framework-reference/html/images/tx.png - WesternGun
1
很晚才来参加派对 - 这些代理对象是在运行时自动生成的类的实例。这究竟是在什么时候发生的?当应用程序加载到JVM中还是当应该被包装的bean第一次被调用时? - samshers
这是一个非常好的关于Spring @Transactional注解如何工作的解释!!! - Chris Lee

74

作为一个视觉化的人,我喜欢用代理模式的顺序图来阐述。如果你不知道如何阅读箭头,我会这样解读第一个箭头:Client执行Proxy.method()

  1. 从客户端的角度来看,客户端调用目标方法,但被代理静默地拦截了
  2. 如果定义了前置切面,则代理将执行它
  3. 然后,实际的方法(目标)被执行
  4. 返回后和抛出异常后都可以选择性地执行“返回后”和“抛出异常后”切面
  5. 之后,代理执行后置切面(如果定义了)
  6. 最后,代理返回给调用的客户端

代理模式顺序图 (在提到图片来源的条件下,我被允许发布该照片。作者:Noel Vaes,网站:https://www.noelvaes.eu


69
最简单的答案是:无论你在哪个方法上声明了@Transactional,事务的边界都会开始,并且当方法完成时,边界就结束了。
如果你正在使用JPA调用,则所有提交都在此事务边界内。假设你正在保存entity1,entity2和entity3。现在,在保存entity3时发生了一个异常,由于entity1和entity2处于同一事务中,因此entity1和entity2将与entity3一起回滚事务:
  1. entity1.save
  2. entity2.save
  3. entity3.save
任何异常都会导致所有JPA与数据库的事务回滚。在Spring中,JPA事务被用来实现此功能。

13
任何异常情况会导致所有与数据库相关的JPA事务回滚。 注意:只有RuntimeException会导致回滚。受检异常不会导致回滚。 - Arjun Sunil Kumar
Spring事务和异常处理:@Transactional只会回滚事务,而不是回滚异常。因此,在捕获异常之后,如果方法以编程方式传播异常,则该异常将被传播到调用者,而不会回滚任何事务。在这种情况下,可能会留下未提交的更改并锁定数据库,使系统不一致。 - Bhaskar13

19

所有现有的答案都是正确的,但我觉得无法充分说明这个复杂的话题。

为了全面、实用地解释,您可能需要查看这个Spring @Transactional In-Depth 指南,尽力用约4000个简单的词语和许多代码示例来介绍事务管理。


一个真正复杂问题的真正答案。另外,我非常喜欢你的博客。不仅仅是他的,而是所有的。 - KnockingHeads
非常好的文章。非常简单。帮了很多忙!谢谢。要更好地理解这个,一个人应该了解ACID、一致性和隔离级别的基础知识。 - undefined

9

虽然来得有些晚,但我找到了一些能够很好地解释你关于代理的疑惑(只有通过代理进行的“外部”方法调用才会被拦截)。

例如,你有一个长这样的类:

@Component("mySubordinate")
public class CoreBusinessSubordinate {
    
    public void doSomethingBig() {
        System.out.println("I did something small");
    }
    
    public void doSomethingSmall(int x){
        System.out.println("I also do something small but with an int");    
  }
}

你有一个类似于这样的方面:

@Component
@Aspect
public class CrossCuttingConcern {
    
    @Before("execution(* com.intertech.CoreBusinessSubordinate.*(..))")
    public void doCrossCutStuff(){
        System.out.println("Doing the cross cutting concern now");
    }
}

当您像这样执行它时:

 @Service
public class CoreBusinessKickOff {
    
    @Autowired
    CoreBusinessSubordinate subordinate;
 
    // getter/setters
    
    public void kickOff() {
       System.out.println("I do something big");
       subordinate.doSomethingBig();
       subordinate.doSomethingSmall(4);
   }

调用以上给定代码的kickOff函数的结果如下:

I do something big
Doing the cross cutting concern now
I did something small
Doing the cross cutting concern now
I also do something small but with an int

但是当您更改代码为

@Component("mySubordinate")
public class CoreBusinessSubordinate {
    
    public void doSomethingBig() {
        System.out.println("I did something small");
        doSomethingSmall(4);
    }
    
    public void doSomethingSmall(int x){
       System.out.println("I also do something small but with an int");    
   }
}


public void kickOff() {
  System.out.println("I do something big");
   subordinate.doSomethingBig();
   //subordinate.doSomethingSmall(4);
}

您可以看到,该方法内部调用了另一个方法,因此它不会被拦截,输出如下:

I do something big
Doing the cross cutting concern now
I did something small
I also do something small but with an int

您可以通过这种方式绕过此问题

public void doSomethingBig() {
    System.out.println("I did something small");
    //doSomethingSmall(4);
    ((CoreBusinessSubordinate) AopContext.currentProxy()).doSomethingSmall(4);
}

代码片段来源: https://www.intertech.com/Blog/secrets-of-the-spring-aop-proxy/ 该页面已不存在。


很好的例子,谢谢。URL现在无法访问。 - Prashant Zombade

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接