mybatis源码学习之——执行流程(Executor)

mybatis源码学习之——执行流程(Executor)


声明

本博客只是作为个人学习记录,其中引用到源码阅读网相关图片,若有侵权,告知立删。
本篇博客会随着博主学习进度持续更新修订。

预备知识:

Mybatis执行过程:

Mybatis执行过程

Executor执行器涉及的设计方法:模板方法模式装饰者模式

Executor类图:

Executor接口

Executor 接口源代码:

public interface Executor {
  ResultHandler NO_RESULT_HANDLER = null;
  // 执行 insert 、 update 、 delete 三种类型的sql语句
  int update(MappedStatement ms, Object parameter) throws SQLException;
  <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql) throws SQLException;
  <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException;
  <E> Cursor<E> queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException;
  // 批处理执行器
  List<BatchResult> flushStatements() throws SQLException;
  // 事务提交
  void commit(boolean required) throws SQLException;
  // 事务回滚
  void rollback(boolean required) throws SQLException;
  // 创建缓存中用的cacheKey
  CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql);
  // 根据cachekey获取缓存
  boolean isCached(MappedStatement ms, CacheKey key);
  // 清除缓存
  void clearLocalCache();
  // 延迟加载缓存中的数据(后续介绍)
  void deferLoad(MappedStatement ms, MetaObject resultObject, String property, CacheKey key, Class<?> targetType);
  // 获取事务对象
  Transaction getTransaction();
  // 关闭Executor 对象
  void close(boolean forceRollback);
  // 检测Executor对象是否关闭
  boolean isClosed();
  void setExecutorWrapper(Executor executor);
}

mybatis-config 配置文件:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>

<properties></properties>
    <settings>
        <setting name="cacheEnabled" value="true"/>
    </settings>

    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.jdbc.Driver" />
                <property name="url" value="jdbc:mysql://localhost:3306/czcdatabase"/>
                <property name="username" value="root"/>
                <property name="password" value="root"/>
            </dataSource>
        </environment>
    </environments>
    <mappers>
        <mapper resource="bookMapper.xml"/>
    </mappers>
</configuration>

手动调用执行器实现查询

  1. 调用SimpleExecutor.doQuery()方法实现查询(跳过一级缓存)
    • 读取mybatis-config.xml配置文件
    • 创建执行器
    • 执行sql语句
    • 测试源代码
    private Configuration configuration;
    private Connection connection;
    private JdbcTransaction transaction;
    private MappedStatement ms;

    public void creatConnection() throws Exception {
        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory build = new SqlSessionFactoryBuilder().build(inputStream);
        configuration = build.getConfiguration();
        connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/czcdatabase", "root", "root");
        //事务对象(mybatis默认的事务处理器)
        transaction = new JdbcTransaction(connection);
    }

    /**
     * mybatis simpleExecutor(简单执行器) 测试实验
     */
    @Test
    public void simpleExecutor() throws Exception {
        // 初始化连接(读取配置文件)
        creatConnection();
        // 创建执行器
        SimpleExecutor executor = new SimpleExecutor(configuration, transaction);
        //执行Sql
        ms = configuration.getMappedStatement("czc.study.mybatis.mapper.BookMapper.getBookByName");
        List<Object> objects = executor.doQuery(ms, "西游记", RowBounds.DEFAULT, SimpleExecutor.NO_RESULT_HANDLER, ms.getBoundSql("西游记"));
        System.out.println(objects.get(0));
    }
  • 输出结果
   Book{id=1, bookname='西游记', date=Fri Dec 18 00:00:00 CST 2020
  1. 调用SimpleExecutor.query()方法实现查询(测试Mybatis一级缓存)
    • 一级缓存在BaseExecutor中query方法实现,以query为例分析。
    • 测试代码如下
    public void baseExecutor() throws Exception {
        // 初始化连接(读取配置文件)
        creatConnection();
        // 创建执行器
        SimpleExecutor executor = new SimpleExecutor(configuration, transaction);
        //执行Sql
        ms = configuration.getMappedStatement("czc.study.mybatis.mapper.BookMapper.getBookByName");
        List<Object> objects = executor.query(ms, "西游记", RowBounds.DEFAULT, SimpleExecutor.NO_RESULT_HANDLER);
        // 执行两次sql相同的查询,使其命中缓存(注意一级缓存是线程不安全的)
        List<Object> objects2 = executor.query(ms, "西游记", RowBounds.DEFAULT, SimpleExecutor.NO_RESULT_HANDLER);
        System.out.println(objects.get(0));
        System.out.println(objects2.get(0));
    }
  1. 调试:
  • 第一次调用查询命令,缓存里没数据;取值为null,走数据库查询。
一级缓存_调试截图_缓存没数据
  • 第二次执行相同sql查询的时候则会命中缓存,并从缓存区取数据
一级缓存_调试截图_缓存有数据
  • 一级缓存的实现,其实就是一个Map对象。
一级缓存实现代码_调用处
  • 上述代码 localCache.getObject(key)实现;
    // 一级缓存存储容器定义处
    public class PerpetualCache implements Cache {
        // 省略其余代码...
        private final Map<Object, Object> cache = new HashMap<>();
        // ...
        @Override
        public Object getObject(Object key) {
            return cache.get(key);
        }
    }

源码分析

BaseExecutor 是一个实现了 Executor 接口的抽象类,它实现了 Executor 接口的大部分方法,其中就使用了模板方法模式。 BaseExecutor 中主要提供了缓存管理和事务管理的基本功能,继承 BaseExecutor 的子类只要实现四个基本方法来完成数据库的相关操作即可,这四个方法分别是 : doUpdate()方法、 doQuery()方法、 doQueryCursor())方法、 doFlushStatement()方法,其余的功能在 BaseExecutor 中实现。

BaseExecutor 常用子类:ReuseExecutor(可重用执行器)、SimpleExecutor(简单执行器)、BatchExecutor(批处理执行器)

BatchExecutor 在处理批量增删改时,无论连接时是否开启自动提交,都需要手动执行doFlushStatements(),批处理执行器在执行过程中,增删改sql相同时只执行一次预编译。
ReuseExecutor 可重用执行器在遇到相同的sql时,只预编译一次。
CachingExecutor扮演上述三个执行器的装饰者(装饰者设计模式),为执行器提供二级缓存功能支持。

BaseExecutor中各个字段的含义如下:

    // Transaction 对象,实现事务的提交、 回滚和关闭操作
    protected Transaction transaction; 
    // 其中封装的 Executor 对象
    protected Executor wrapper ; 
    // 延迟加载队列,后面详述
    protected ConcurrentLinkedQueue<DeferredLoad> deferredLoads; 
    // 一级缓存,用于缓存该 Executor 对象查询结果集映射得到的结采对象
    protected PerpetualCache localCache ;
    // 一级缓存,用于缓存输出类型的参数
    protected PerpetualCache localOutputPararneterCache; 
    // 用来记录嵌套查询的层敛,分析 DefaultResultSetHandler 时介绍过嵌套查询,后面还会详细分析该字段
    protected int queryStack ;

一级缓存的生命周期与 SqlSession 相同,其实也就与 SqlSession 中封装的 Executor 对象的生命周期相同。当调用 Executor 对象的 close()方法时,该 Executor 对象对应的一级缓存就变得不可用。

BaseExecutor query()方法源代码分析如下:

BaseExecutor. query()方法会首先创建 CacheKey 对象,并根据该 CacheKey 对象查找一级缓存,如果缓存命中则返回缓存中记录的结果对象,如果缓存未命中则查询数据库得到结果集,之后将结果集映射成结果对象并保存到一级缓存中,同时返回结果对象 。
query()方法的具体实现如下:

    public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
        BoundSql boundSql = ms.getBoundSql(parameter);
        // 创建 CacheKey 对象,该 CacheKey 对象的组成部分在后面详细介绍
        CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
        // 调用 query ()的另一个重载,继续后续处理
        return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
    }

我们来看 CacheKey 对象由哪几部分构成, createCacheKey()方法具体实现如下:

    public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
        // 判断Executor对象是否被关闭;关闭了则抛异常
        if (closed) {
            throw new ExecutorException("Executor was closed.");
        }
        // 创建 CacheKey 对象
        CacheKey cacheKey = new CacheKey(); 
        // 将 MappedStatement 的 id 添加到 CacheKey 对象中
        cacheKey.update(ms.getId());  
        // 将 offset 添加到 CacheKey 对象中
        cacheKey.update(rowBounds.getOffset()); 
        // 将 limit 添加到 CacheKey 对象中
        cacheKey.update(rowBounds.getLimit());  
        // 将 SQL 语句添加到 CacheKey 对象中
        cacheKey.update(boundSql.getSql()); 
        List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
        TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
        // 获取用户传入的实参,并添加 CacheKey 对象中
        for (ParameterMapping parameterMapping : parameterMappings) {
        if (parameterMapping.getMode() != ParameterMode.OUT) {
            Object value;
            String propertyName = parameterMapping.getProperty();
            if (boundSql.hasAdditionalParameter(propertyName)) {
            value = boundSql.getAdditionalParameter(propertyName);
            } else if (parameterObject == null) {
            value = null;
            } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
            value = parameterObject;
            } else {
            MetaObject metaObject = configuration.newMetaObject(parameterObject);
            value = metaObject.getValue(propertyName);
            }
            cacheKey.update(value);  //将实参添加到 CacheKey 对象中
        }
        }
        // 如果 Environment 的 id 不为空,则将其添加到 CacheKey 中
        if (configuration.getEnvironment() != null) {
        // issue #176
        cacheKey.update(configuration.getEnvironment().getId());
        }
        return cacheKey;
    }

经过createCacheKey()方法便可以对每一次不同的sql请求生成一条唯一的key;由此也可以看出一级缓存命中的一些必要条件;可以清晰地看到,该 CacheKey 对象由 MappedStatement 的 id、对应的 offset 和 limit、 SQL语句(包含“?”占位符)、用户传递的实参以及 Environment 的 id这六部分构成 。

继续来看上述代码中调用的 query()方法的另一重载的具体实现,该重载会根据前面创建的CacheKey 对象查询一级缓存,如果缓存命中则将缓存中记录的结果对象返回,如果缓存未命中,则调用 doQuery()方法完成数据库的查询操作并得到结果对象,之后将结果对象记录到一级缓存中。

具体实现如下:

    public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
        ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
        // 判断Executor对象是否被关闭;关闭了则抛异常
        if (closed) {
            throw new ExecutorException("Executor was closed.");
        }
        if (queryStack == 0 && ms.isFlushCacheRequired()) {
            // 非嵌套查询,并且<select>节点配置的 flushCache 属性为 true 时,才会清空一级缓存
            // flushCache 配置项是影响一级缓存中结采对象存活时长的第一个方面
            clearLocalCache();
        }
        List<E> list;
        try {
            // 用于标记嵌套查询层数
            queryStack++;
            // 从一级缓存取数据
            list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
            if (list != null) {
                // 针对存储过程调用的处理,其功能是在一级缓存命中时,获取缓存中保存的输出类型参数。
                handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
            } else {
                // 一级缓存没有数据,则调用数据库
                // 其中会调用 doQuery ()方法完成数据库查询,并得到映射后的结采对象, doQuery ()方法是
                // 一个抽象方法,也是上述 4 个基本方法之一,由 BaseExecutor 的子类具体实现。
                list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
            }
        } finally {
            // 当前层查询结束,嵌套层减一 
            queryStack--;
        }
        if (queryStack == 0) {
            // 存在嵌套子查询时延迟加载处理逻辑(后续博文在分析此处)
            for (DeferredLoad deferredLoad : deferredLoads) {
                deferredLoad.load();
            }
            // issue #601
            deferredLoads.clear();
            if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
                // issue #482
                clearLocalCache();
            }
        }
        return list;
    }

当缓存没有命中时候,查询数据库,调用 BaseExecutor的queryFromDatabase()方法;基于模板方法模式,具体的查询实现交给了BaseExecutor的实现类去做。此案例中是SimpleExecutor的doQuery()方法实现了查询功能。

    private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
        List<E> list;
        // 缓存先放一个占位符(防止在查询结果还没出来前;调用缓存查出空数据;在 deferredLoad.load();中实现)
        localCache.putObject(key, EXECUTION_PLACEHOLDER);
        try {
            // 调用具体实现BaseExecutor的子类查询方法。
            list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
        } finally {
            // 删除占位符
            localCache.removeObject(key);
        }
        // 将查询结果存入一级缓存
        localCache.putObject(key, list);
        if (ms.getStatementType() == StatementType.CALLABLE) {
            localOutputParameterCache.putObject(key, parameter);
        }
        return list;
    }

SimpleExecutor的doQuery()方法子类

    public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
    Statement stmt = null;
        try {
            Configuration configuration = ms.getConfiguration();
            StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
            stmt = prepareStatement(handler, ms.getStatementLog());
            return handler.query(stmt, resultHandler);
        } finally {
            closeStatement(stmt);
        }
    }

未完待续~~

二级缓存

sqlSession查询流程图
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,470评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,393评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,577评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,176评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,189评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,155评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,041评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,903评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,319评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,539评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,703评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,417评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,013评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,664评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,818评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,711评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,601评论 2 353

推荐阅读更多精彩内容