Spring源码的图文详解:Spring事务

一:概述及目录

下⾯我会简单介绍⼀下 Spring 事务的基础知识,以及使⽤⽅法,然后直接对源码进⾏拆解。

目录:

二. 项⽬准备

下⾯是 DB 数据和 DB 操作接⼝:

Id

uname

usex

1

小王

2

小李

1

小赵

@Data
public class MyUser {
    private int id;
    private String uname;
    private String usex;
}
复制代码
public interface UserDao {
    // select * from user_test where id = "#{id}"
     MyUser selectUserById(Integer uid);
    // update user_test set uname =#{uname},usex = #{usex} where id = #{id}
     int updateUser(MyUser user);
}
复制代码

基础测试代码,testSuccess() 是事务⽣效的情况:

@Service
public class Model {
    @Autowired
    private UserDao userDao;
    public void update(Integer id) {
        MyUser user = new MyUser();
        user.setId(id);
        user.setUname("张三-testing");
        user.setUsex("⼥");
        userDao.updateUser(user);
    }
    public MyUser query(Integer id) {
        MyUser user = userDao.selectUserById(id);
        return user;
    }
    // 正常情况
    @Transactional(rollbackFor = Exception.class)
    public void testSuccess() throws Exception {
        Integer id = 1;
        MyUser user = query(id);
        System.out.println("原记录:" + user);
        update(id);
        throw new Exception("事务⽣效");
    }
}
复制代码

执⾏⼊⼝:

public class SpringMyBatisTest {
    public static void main(String[] args) throws Exception {
        String xmlPath = "applicationContext.xml";
        ApplicationContext applicationContext = new
                ClassPathXmlApplicationContext(xmlPath);
        Model uc = (Model) applicationContext.getBean("model");
        uc.testSuccess();
    }
}
复制代码

输出:

三:Spring 事务⼯作流程

为了⽅便⼤家能更好看懂后⾯的源码,我先整体介绍⼀下源码的执⾏流程,让⼤家有⼀个整体的认识,否则容易被 绕进去。

整个 Spring 事务源码,其实分为 2 块,我们会结合上⾯的示例,给⼤家进⾏讲解。

第⼀块是后置处理,我们在创建 Model Bean 的后置处理器中,⾥⾯会做两件事情:

获取 Model 的切⾯⽅法:⾸先会拿到所有的切⾯信息,和 Model 的所有⽅法进⾏匹配,然后找到 Model 所有需 要进⾏事务处理的⽅法,匹配成功的⽅法,还需要将事务属性保存到缓存 attributeCache 中。

创建 AOP 代理对象:结合 Model 需要进⾏ AOP 的⽅法,选择 Cglib 或 JDK,创建 AOP 代理对象。

第⼆块是事务执⾏,整个逻辑⽐较复杂,我只选取 4 块最核⼼的逻辑,分别为从缓存拿到事务属性、创建并开启事 务、执⾏业务逻辑、提交或者回滚事务

四. 源码解读

注意:Spring 的版本是 5.2.15.RELEASE,否则和我的代码不⼀样!!!

上⾯的知识都不难,下⾯才是我们的重头戏,让我们一起⾛⼀遍代码流程。

4.1 代码⼊⼝

这⾥需要多跑⼏次,把前⾯的 beanName 跳过去,只看 model。

进⼊ doGetBean(),进⼊创建 Bean 的逻辑。

进⼊ createBean(),调⽤ doCreateBean()。

进⼊ doCreateBean(),调⽤ initializeBean()。

如果看过我前⾯⼏期系列源码的同学,对这个⼊⼝应该会⾮常熟悉,其实就是⽤来创建代理对象。

4.2 创建代理对象

这⾥是重点!敲⿊板!!!

先获取 model 类的所有切⾯列表; 创建⼀个 AOP 的代理对象。

4.2.1 获取切⾯列表

这⾥有 2 个重要的⽅法,先执⾏ findCandidateAdvisors(),待会我们还会再返回 findEligibleAdvisors()。

依次返回,重新来到 findEligibleAdvisors()。

进⼊ canApply(),开始匹配 model 的切⾯。

这⾥是重点!敲⿊板!!! 这⾥只会匹配到 Model.testSuccess() ⽅法,我们直接进⼊匹配逻辑。

如果匹配成功,还会把事务的属性配置信息放⼊ attributeCache 缓存。

我们依次返回到 getTransactionAttribute(),再看看放⼊缓存中的数据。

再回到该⼩节开头,我们拿到 mdoel 的切⾯信息,去创建 AOP 代理对象。

4.2.2 创建 AOP 代理对象

创建 AOP 代理对象的逻辑,在上⼀篇⽂章【Spring源码解析-Spring AOP】讲解过,我是通过 Cglib 创建,感兴趣的同学可以翻⼀下我的历史⽂章。

4.3 事务执⾏

回到业务逻辑,通过 model 的 AOP 代理对象,开始执⾏主⽅法。

因为代理对象是 Cglib ⽅式创建,所以通过 Cglib 来执⾏。

这⾥是重点!敲⿊板!!!

下⾯的代码是事务执⾏的核⼼逻辑 invokeWithinTransaction()。

protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,
                                         final InvocationCallback invocation) throws Throwable {
    //获取我们的事务属源对象
    TransactionAttributeSource tas = getTransactionAttributeSource();
    //通过事务属性源对象获取到我们的事务属性信息
    final TransactionAttribute txAttr = (tas != null ?
            tas.getTransactionAttribute(method, targetClass) : null);
    //获取我们配置的事务管理器对象
    final PlatformTransactionManager tm = determineTransactionManager(txAttr);
    //从tx属性对象中获取出标注了@Transactionl的⽅法描述符
    final String joinpointIdentification = methodIdentification(method,
            targetClass, txAttr);
    //处理声明式事务
    if (txAttr == null || !(tm instanceof
            CallbackPreferringPlatformTransactionManager)) {
        //有没有必要创建事务
        TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr,
                joinpointIdentification);
        Object retVal;
        try {
            //调⽤钩⼦函数进⾏回调⽬标⽅法
            retVal = invocation.proceedWithInvocation();
        }
        catch (Throwable ex) {
            //抛出异常进⾏回滚处理
            completeTransactionAfterThrowing(txInfo, ex);
            throw ex;
        }
        finally {
            //清空我们的线程变量中transactionInfo的值
            cleanupTransactionInfo(txInfo);
        }
        //提交事务
        commitTransactionAfterReturning(txInfo);
        return retVal;
    }
    //编程式事务
    else {
        // 这⾥不是我们的重点,省略...
    }
}
复制代码

4.3.1 获取事务属性

在 invokeWithinTransaction() 中,我们找到获取事务属性的⼊⼝。

从 attributeCache 获取事务的缓存数据,缓存数据是在 “3.2.1 获取切⾯列表” 中保存的。

4.3.2 创建事务

通过 doGetTransaction() 获取事务。

protected Object doGetTransaction() {
    //创建⼀个数据源事务对象
    DataSourceTransactionObject txObject = new DataSourceTransactionObject();
    //是否允许当前事务设置保持点
    txObject.setSavepointAllowed(isNestedTransactionAllowed());
    /**
     * TransactionSynchronizationManager 事务同步管理器对象(该类中都是局部线程变量)
     * ⽤来保存当前事务的信息,我们第⼀次从这⾥去线程变量中获取 事务连接持有器对象 通过数据源为key
     去获取
     * 由于第⼀次进来开始事务 我们的事务同步管理器中没有被存放.所以此时获取出来的conHolder为null
     */
    ConnectionHolder conHolder =
            (ConnectionHolder)
                    TransactionSynchronizationManager.getResource(obtainDataSource());
    txObject.setConnectionHolder(conHolder, false);
    //返回事务对象
    return txObject;
}
复制代码

通过 startTransaction() 开启事务。

下⾯是开启事务的详细逻辑,了解⼀下即可。

protected void doBegin(Object transaction, TransactionDefinition definition) {
    //强制转化事务对象
    DataSourceTransactionObject txObject = (DataSourceTransactionObject)
            transaction;
    Connection con = null;
    try {
        //判断事务对象没有数据库连接持有器
        if (!txObject.hasConnectionHolder() ||
                txObject.getConnectionHolder().isSynchronizedWithTransaction()) {
            //通过数据源获取⼀个数据库连接对象
            Connection newCon = obtainDataSource().getConnection();
            if (logger.isDebugEnabled()) {
                logger.debug("Acquired Connection [" + newCon + "] for JDBC
                        transaction");
            }
            //把我们的数据库连接包装成⼀个ConnectionHolder对象 然后设置到我们的txObject对象
            中去
            txObject.setConnectionHolder(new ConnectionHolder(newCon), true);
        }
        //标记当前的连接是⼀个同步事务
        txObject.getConnectionHolder().setSynchronizedWithTransaction(true);
        con = txObject.getConnectionHolder().getConnection();
        //为当前的事务设置隔离级别
        Integer previousIsolationLevel =
                DataSourceUtils.prepareConnectionForTransaction(con, definition);
        txObject.setPreviousIsolationLevel(previousIsolationLevel);
        最后返回到 invokeWithinTransaction(),得到 txInfo 对象。
        //关闭⾃动提交
        if (con.getAutoCommit()) {
            txObject.setMustRestoreAutoCommit(true);
            if (logger.isDebugEnabled()) {
                logger.debug("Switching JDBC Connection [" + con + "] to manual
                        commit");
            }
            con.setAutoCommit(false);
        }
        //判断事务为只读事务
        prepareTransactionalConnection(con, definition);
        //设置事务激活
        txObject.getConnectionHolder().setTransactionActive(true);
        //设置事务超时时间
        int timeout = determineTimeout(definition);
        if (timeout != TransactionDefinition.TIMEOUT_DEFAULT) {
            txObject.getConnectionHolder().setTimeoutInSeconds(timeout);
        }
        // 绑定我们的数据源和连接到我们的同步管理器上 把数据源作为key,数据库连接作为value 设
        置到线程变量中
        if (txObject.isNewConnectionHolder()) {
            TransactionSynchronizationManager.bindResource(obtainDataSource(),
                    txObject.getConnectionHolder());
        }
    }
    catch (Throwable ex) {
        if (txObject.isNewConnectionHolder()) {
            //释放数据库连接
            DataSourceUtils.releaseConnection(con, obtainDataSource());
            txObject.setConnectionHolder(null, false);
        }
        throw new CannotCreateTransactionException("Could not open JDBC Connection
       
    }
}
复制代码

最后返回到 invokeWithinTransaction(),得到 txInfo 对象。

4.3.3 执⾏逻辑

还是在 invokeWithinTransaction() 中,开始执⾏业务逻辑。

进⼊到真正的业务逻辑。

执⾏完毕后抛出异常,依次返回,⾛后续的回滚事务逻辑。

4.3.4 回滚事务

还是在 invokeWithinTransaction() 中,进⼊回滚事务的逻辑。

执⾏回滚逻辑很简单,我们只看如何判断是否回滚。

如果抛出的异常类型,和事务定义的异常类型匹配,证明该异常需要捕获。

之所以⽤递归,不仅需要判断抛出异常的本身,还需要判断它继承的⽗类异常,满⾜任意⼀个即可捕获。

到这⾥,所有的流程结束。

五. 总要有总结

我们再⼩节⼀下,⽂章先介绍了事务的使⽤示例,以及事务的执⾏流程。

之后再剖析了事务的源码,分为 2 块:

先匹配出 model 对象所有关于事务的切⾯列表,并将匹配成功的事务属性保存到缓存; 从缓存取出事务属性,然后创建、启动事务,执⾏业务逻辑,最后提交或者回滚事务。

全部评论
又是涨芝士的一天
点赞 回复 分享
发布于 2023-03-14 12:15 山东
狠狠学到了
点赞 回复 分享
发布于 2023-03-14 12:24 甘肃

相关推荐

2.6投的简历,2.7就有电话来约面试,2.8就面试,进程推进还是蛮快的,应该是缺人,所以想去蔚来base上海的,可以去冲冲!分享一下一面面经:1.&nbsp;&nbsp;自我介绍2.&nbsp;了解工作时长,一周工作几天,之后的时间规划3.&nbsp;为什么往测试开发方向发展,你对于测试的理解是什么?4.&nbsp;测试是一项什么样的工作?5.&nbsp;你发现缺陷后会继续跟踪缺陷的解决方案吗?6.关于缺陷本身是怎么解决的?缺陷解决的流程理解7.&nbsp;介绍上一份实习经历测试的对象,需要满足什么样的用户需求8.在这个实习经历中担任的角色,负责跟踪新需求还是做回归测试比较多9.&nbsp;测试用例数量很多,有疑问为什么有这么多用例和缺陷10.&nbsp;测试用例是自己写的吗,还是只负责执行11.&nbsp;介绍蔚来从一个需求的创建到上线都是需要每一个测试去跟踪的,实习生的工作和正职一样12.&nbsp;对于测试用例的设计和把控,举个例子,在实习期间,有针对哪一个需求的改动,印象比较深的13.&nbsp;讲关于实习经历中一个具体的需求,设计测试用例15.&nbsp;举例具体需求:哪些测试用例需要去覆盖,更多场景异常、边界、场景的覆盖。在淘宝app首页搜索框中搜索特殊短语,如双十一、跳转到对应页面,当命中特定短语的时候能命中活动也,对于后端就是设置多个key配对活动页面地址value,每一条key&nbsp;value还有生效时间,范围内才生效,同时配置还能配置多条,比如双十一、六一八。后端配置上去了就能生效,根据这个,作为测试,测试用例如何设计?16.&nbsp;如果你是测试,遇到一个缺陷,你会怎么去进行初步的排查,然后判断当前的问题是前端还是后端的问题,还是网络的&nbsp;问题17.&nbsp;接口有哪些部分组成18.怎么模拟100个用户操作的19.jmeter是如何进行性能测试的20.数据库mysql代码21.python&nbsp;代码22.反问总结:面试官是一个很专业,很温柔也很会引导的人,可惜就是我自己太紧张,很多应该答上来的没有抓住机会,感觉蔚来很注重基础素质的培养,进去之后做的也不是低级的手动测试工作而且没问什么八股,基本就是根据简历还有你的回答,去问问题,所以不要给自己挖坑了www##蔚来##测开面试##日常实习#
查看20道真题和解析
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客企业服务