美文网首页读书
浅入浅出代理模式与Spring事务管理

浅入浅出代理模式与Spring事务管理

作者: Java编程日记 | 来源:发表于2022-05-25 14:31 被阅读0次

    本文回顾了代理模式和Spring事务管理的原理。

    前言

    最近在开发业务代码的时候,犯了一个事务注解的错误:在同一个类的非事务方法中调用了另一个事务方法,导致事务没有生效,如下所示:

    public ConfirmOrderResultVO batchConfirmPurchaseOrders(Long taobaoUserId, List<String> bizOrderIds) throws TCException {
    

    其中confirmPurchaseOrder()是一个事务方法:

    @Transactional
    

    这样在直接调用batchConfirmPurchaseOrders()方法时,如果confirmPurchaseOrder()方法发生了异常,是不会回滚的。原理在于Spring的事务管理依靠的动态代理模式,当在同一个类中调用一个非事务方法,是不会生成代理对象的,自然也不会触发事务。借此机会回顾一下代理模式和Spring事务管理的原理。

    代理模式

    网上讲解代理模式的文章千奇百怪,很多时候看完了也不明白讲的重点是什么。

    事实上在生活中我们常常会遇到各种各样的代理模式,例如火车票代售点代理出售火车票,他在“帮忙”出售火车票的同时,收取了额外的手续费,记录了自己店里的流水等。

    [图片上传失败...(image-88fa31-1652967961415)]

    又比如班长代理老师来上交班费,他在“上交班费”的动作之前,还进行了检查班级同学是否到齐、向每一位同学收班费、核对班费金额,最后再上交班费。

    [图片上传失败...(image-75ae76-1652967961415)]

    总而言之,代理模式就是 代理其他对象,在完成原动作的基础上(前后)做一些额外的自定义的工作 。

    聪明的朋友看到这里一定想到了:那我在一个方法中调用另一个方法不就是代理模式了吗?啊对对对,从功能上来讲是一样的,但是“模式”之所以为“模式”,以我拙见,其本质目的在于形成规范,以减少重复代码的编写。因此如果不能达到减少代码重复这一本质目的,便不能称之为“模式”。

    按照代理模式的实现方式,分为两种:静态代理和动态代理。那我们就拿刚刚说过的“写作业”这件事来做例子,讲解一下两种实现方式的区别。

    ▐ **** 静态代理

    首先定义一个“作业”接口,里面有一个方法“做作业”

    public interface Homework {
    

    小红实现了这个接口。但是小红是一个不爱学习的小学生,又因为师从马掌门成为了学校的扛把子,所以为了不让老师发现,他决定自己随便做做,把剩下的部分交给小弟们来做。

    public class XiaoHong implements Homework{
    

    其中小王、小张两个小弟成绩优异,一个负责数学作业,一个负责英语作业,于是他们两个自告奋勇实现了Homework接口,在代理完成作业的基础上,还不断学习提高自己的能力,并且对作业答案进行了校验。

    小王:

    public class XiaoWang implements Homework{
    

    小张:

    public class XiaoZhang implements Homework{
    

    于是,小红可以放心的把作业交给小王和小张了:

     public static void main(String[] args) {
    

    输出:

    XiaoZhang is studying---------------
    

    问题来了,如果老师又布置了一个Book接口和readBook方法,但是readBook前后的动作是一样的(都需要study和check),如何通过代理来实现呢?

    一个方案是让小王和小张都实现Book接口和readBook方法,并持有新的Book对象;另一个方案是再找一个小赵实现Book接口和readBook方法。

    这样两种方案实际上都能达到效果,但是如果接口和方法增多起来呢?如果让一个代理类实现一堆方法,并持有一堆不同的对象,这个类势必会变得臃肿不堪;如果每一个新的方法都创建一个新的代理类,引用不同的对象,又会造成代码的大量重复。因此,动态代理就出现了。

    ▐ **** 动态代理

    前文所讲的静态代理之所以为“静态”,是因为每个接口的每个方法,我们都要显式的去实现、创建、调用,所有的操作都是写死的、是静态的。而动态代理的巧妙之处在于,可以在程序的运行期间,动态的生成不同的代理类,来完成相同的工作。也就是说,如果你想实现一个方法,在所有其他方法执行前后做一些额外的相同的动作,例如打印日志、记录方法执行时间等,你就需要动态代理了(是不是想到了什么?)。

    事实上所有的动态代理思想都是一致的,而目前最常用的动态代理实现有两种:JDK原生实现和Cglib开源实现。

    Spring在5.X之前默认的动态代理实现一直是JDK动态代理。但是从5.X开始,Spring就开始默认使用Cglib来作为动态代理实现。并且SpringBoot从2.X开始也转向了Cglib动态代理实现。

    • JDK实现动态代理

    现在有一个牛X的机器人代理,可以帮所有人在完成老师布置的任何任务前后,进行学习和答案的核对,在JDK的动态代理实现中,他是这样编写的:

    // 实现InvocationHandler
    

    然后这样调用机器人代理:

    public static void main(String[] args) {
    

    输出:

    Robot is studying------------
    

    可以看到,JDK实现动态代理必须要依赖接口,只有实现了接口的目标类才可以被代理,而Cglib动态代理就可以消除接口的限制。

    • Cglib实现动态代理

    创建一个升级版的机器人RovotV2代理类:

    // 实现MethodInterceptor方法(要引入cglib包)
    

    调用方式:

    public static void main(String[] args) {
    

    输出:

    RobotV2 is studying------------
    

    Cglib动态代理相比于JDK代理实现有几个优势:

    1. 基于字节码,生成真实对象的子类。

    2. 运行效率高于JDK代理

    3. 不需要实现接口

    可以看到,动态代理只需要一个代理类,就可以代理所有需要同一种处理(如上文所述的study()、check())的类和方法,这就是动态代理与静态代理最大的区别。

    Spring事务管理

    OK,了解到了代理模式的好处之后,就可以自然而然的想到Spring事务管理的基本原理了。无非是借助动态代理,做一些额外的操作:在真正的方法执行之前开启事务,在方法顺利执行完成之后提交事务,如果方法执行过程中发生异常,要执行回滚操作。

    一般有两种方法使用Spring的事务管理,一种是利用注解,一种是手动编程。本文因为使用的是注解型的事务管理,因此只对注解型事务的使用和原理进行探究,以找到事务没有生效的原因。

    ▐ **** 注解型事务

    • 注解型事务使用

    注解型事务的使用非常简单,只需要在需要事务管理的类或方法上加上@Transactional(rollbackFor = Exception.class),其中rollbackFor指定的是遇到什么情况下执行回滚。

    当然这个注解还有传播级别、隔离级别等我们背过的八股文属性,就不一一展开了。

    • 事务执行原理

    PlatformTransactionManager

    我们知道,事务是针对数据库而言的。事实上,Spring并不直接管理事务,他只是提供了一套统一的框架和接口,具体由不同的、真正与数据库打交道的持久层框架来实现,如Hibernate、Ibatis、Mybatis等,这个统一的接口是PlatformTransactionManager,它提供了三个核心方法:获取事务(getTransaction)、提交事务(commit)、回滚事务(rollback),同时Spring提供了一个统一的抽象实现类AbstractPlatformTransactionManager,这是其他持久层框架事务实现类的共同父类。

    public interface PlatformTransactionManager extends TransactionManager {
    

    先来看getTransaction方法:

    @Override
    

    小结:getTransaction方法就是根据具体的传播行为返回一个当前存在的事务或者一个新的事务对象

    再来看提交事务方法commit:

      public final void commit(TransactionStatus status) throws TransactionException {
    

    小结:commit方法就是根据事务状态提交事务,如果事务被标记为“回滚”态,则进行回滚操作

    最后来看rollback方法:

    public final void rollback(TransactionStatus status) throws TransactionException {
    

    processRollback方法:

    private void processRollback(DefaultTransactionStatus status, boolean unexpected) {
    

    这里我们看到了一句熟悉的话"Transaction rolled back because it has been marked as rollback-only",相信一定有朋友在使用事务中看到过这个异常(不要犟),一般存在在这种场景下:A类的事务方法中调用了B类的事务方法,B方法发生了异常,但是在A类的try-catch模块中捕获了这个异常并正常执行。

    这个异常发生的原因是:Spring默认的事务传播级别是propagation.REQUIRED,即如果有没有事务则新启一个事务,如果已经存在事务则加入这个事务。B方法发生了异常,给当前事务打上了rollbackOnly标记,但是被A捕获到了并正常执行,由于只有一个事务,等到A方法要提交这一个事务的时候,没有发现异常但是发现事务被打上了rollbackOnly,只能回滚并抛出一个unexpectedRollback异常。

    • DynamicAdvisedInterceptor

    现在,我们已经明确了事务的执行过程。那既然我们是使用注解来进行事务管理的,有了注解,就一定有注解的处理器,这时候就要搬出Spring的核心法宝——AOP。Spring就是通过切面来获取所有的Spring注解、解析注解并处理注解下的类、方法等。而AOP的核心是动态代理,我们找到AOP包下的Cglib动态代理类CglibAopProxy,其中有内部静态类DynamicAdvisedInterceptor

    private static class DynamicAdvisedInterceptor implements MethodInterceptor, Serializable {
    

    其中有两个核心方法:

    <pre class="prettyprint hljs dart" style="padding: 0.5em; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; color: rgb(68, 68, 68); border-radius: 4px; display: block; margin: 0px 0px 1.5em; font-size: 14px; line-height: 1.5em; word-break: break-all; overflow-wrap: break-word; white-space: pre; background-color: rgb(246, 246, 246); border: none; overflow-x: auto; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass)</pre>

    <pre class="prettyprint hljs ini" style="padding: 0.5em; font-family: Menlo, Monaco, Consolas, "Courier New", monospace; color: rgb(68, 68, 68); border-radius: 4px; display: block; margin: 0px 0px 1.5em; font-size: 14px; line-height: 1.5em; word-break: break-all; overflow-wrap: break-word; white-space: pre; background-color: rgb(246, 246, 246); border: none; overflow-x: auto; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">retVal = new CglibMethodInvocation(proxy, target, method, args, targetClass, chain, methodProxy).proceed();</pre>

    第一个方法是获取拦截链,第二个方法是创建拦截链的Cglib代理对象来执行。对于SpringAOP来讲,凡是需要AOP增强的地方都会进入到这个方法,加载一系列的拦截器,按照责任链模式执行。所有拦截器都会实现MethodInterceptor接口,和invoke方法。

    • TransactionInterceptor

    我们顺藤摸瓜,找到了与事务相关的拦截器TransactionInterceptor,它实现了AOP包下的MethodInterceptor接口,和一个核心的调用方法invoke:

    public class TransactionInterceptor extends TransactionAspectSupport implements MethodInterceptor, Serializable {
    

    invokeWithinTransaction

    进入invokeWithinTransaction方法:

    @Nullable
    

    getTransactionAttribute

    再看getTransactionAttribute方法:

    public TransactionAttribute getTransactionAttribute(Method method, @Nullable Class<?> targetClass) {
    

    computeTransactionAttribute

    再进入到computeTransactionAttribute方法中:

      protected TransactionAttribute computeTransactionAttribute(Method method, @Nullable Class<?> targetClass) {
    

    invokeWithinTransaction

    拿到事务管理器后,再回到invokeWithinTransaction方法,我们看到一些核心代码:

    PlatformTransactionManager ptm = asPlatformTransactionManager(tm);
    

    总结

    Spring通过AOP,在执行具有@Transactional注解的方法时,会根据目标类生成一个代理对象,在目标方法执行之前获取事务并开启事务,在目标方法执行之后提交事务或者回滚事务。当执行一个类中的普通方法时,Spring没有生成代理对象,即使这个方法内部调用了同类其他的事务方法,也就无法参与事务管理了。

    相关文章

      网友评论

        本文标题:浅入浅出代理模式与Spring事务管理

        本文链接:https://www.haomeiwen.com/subject/dskxprtx.html