从Mybatis源码到Spring动态数据源底层原理分析系列二、Mybatis执行器源码分析

【一线大厂Java面试题解析+后端开发学习笔记+最新架构讲解视频+实战项目源码讲义】

**开源地址:https://docs.qq.com/doc/DSmxTbFJ1cmN1R2dB **

3.1、Executor接口方法分析

public interface Executor {

int update(MappedStatement ms, Object parameter) throws SQLException;

List query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql) throws SQLException;

List query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException;

void commit(boolean required) throws SQLException;

void rollback(boolean required) throws SQLException;

CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql);

Transaction getTransaction();

}

复制代码

省略了部分接口方法, 从上面的这些执行sql的方法可以看到, Executor通过MappedStatement对象来完成sql的执行, 同时提供了sql参数, RowBounds是mybatis提供的内存分页功能, 一般我们开发不会用到(我们通常会通过sql来进行分页, 即sql利用limit关键字进行分页), BoundSql中保存了动态的sql(比如 select xx from xx where id = #{id}, 里面保存的是select xx from xx where id = ? 这是在初始化的时候进行解析动态sql得到后的结果, 如果对动态sql的解析有兴趣的同学可以深入研究下mybatis初始化过程中构建MappedStatement的过程), 之后会利用parameter来填充sql中的问号, cacheKey是Executor提供的缓存功能中的key相关对象, 有两种类型的缓存, 之后我们会进行介绍

Executor提供了所有的数据库操作方法(包括存储过程的调用, 省略了), 最基本的增删改查、事务的提交回滚等, 所以说Executor其实就是利用MappedStatement中保存的信息来完成sql的执行以及结果集的处理, 称为执行器, 我们接下来分析这一部分的源码

3.2、Executor接口继承层次分析

如下图所示为Executor的继承体系, BaseExecutor是公共实现, 完成了所有sql执行的公共功能, SimpleExecutor是最通 用的执行器, 封装的是大家熟悉的jdbc代码, 即通过PreparedStatement完成sql的执行, ReuseExecutor跟 SimpleExecutor不同的是, 其缓存了通过sql创建出来的PreparedStatement对象, BatchExecutor提供的是批量操作, 通常情况下, 我们使用的都是SimpleExecutor

在另一边, 可以看到与BaseExecutor处于同一级的CachingExecutor, 这是一个典型的装饰者模式, CachingExecutor提 供了另一层次的缓存(之后我们也会分析, 跟Executor接口中所说的缓存不是一回事, Executor接口本身规定了缓存功能), 除了缓存之外, 所有的数据库操作均是采用委托的形式让其他Executor来完成, 所以CachingExecutor类似于如下的结构:

public class CachingExecutor {

Executor delegate;

void query () {

if (存在缓存) {

返回缓存

} else {

Object result = delegate.query();

将result写入缓存

}

}

}

复制代码

01_Executor继承体系.png

3.3、SimpleExecutor源码分析

3.3.1、query方法分析

了解了Executor的继承层次之后, 我们开始分析SimpleExecutor接口的源码, 以BaseExecutor的query方法作为入口进行分析, 先分析公共部分, BaseExecutor定义了模板, SimpleExecutor则是执行模板中要求子类来实现的功能:

public List query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) {

BoundSql boundSql = ms.getBoundSql(parameter);

CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);

return query(ms, parameter, rowBounds, resultHandler, key, boundSql);

}

public List query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) {

if (queryStack == 0 && ms.isFlushCacheRequired()) {

clearLocalCache();

}

List list;

try {

queryStack++;

list = resultHandler == null ? (List) localCache.getObject(key) : null;

if (list != null) {

handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);

} else {

list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);

}

} finally {

queryStack–;

}

return list;

}

复制代码

BaseExecutor按照Executor的定义, 提供了缓存的功能, 我们来看看第一个query方法, 利用sql以及sql参数调用createCacheKey方法, 根据一定的规则构建了缓存key, 然后调用了第二个方法, 所以缓存的key我们也可以自定义规则

在第二个方法中, 第一个if判断说明如果不使用缓存(强制刷新缓存), 那么就调用clearLocalCache来清空缓存, queryStack是查询嵌套次数, 我们在实现结果集映射即定义resultMap时允许嵌套查询, queryStack就是指嵌套查询的层数, 每一次嵌套查询都会使得queryStack ++, 所以queryStack为0的时候即最开始执行mapper的时候(如果对嵌套查询不熟悉的同学可以跟着mybatis中文官网的例子写一个嵌套查询, 嵌套查询在我目前的接触中其实用的比较少)

localCache的实现为PerpetualCache, 所以在这一层次的缓存中, 其实就是利用Map完成了缓存功能而已, 如果拿到的list不为空, 说明存在缓存, 这个时候执行了handleLocallyCachedOutputParameters方法, 否则调用queryFromDatabase方法从数据库中查询数据, handleLocallyCachedOutputParameters方法我们不用关注, 这个跟存储过程有关, 里面会根据MappedStatement的类型, 如果是存储过程则执行一定的逻辑, 否则啥也不执行

缓存这一块后面我们会专门拿一篇文章来说明, 这个需要我们深刻的理解SqlSession这个组件才能更好的掌握mybatis中的两层 缓存是如何运转的

3.3.2、queryFromDatabase方法分析

private List queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) {

List list;

localCache.putObject(key, EXECUTION_PLACEHOLDER);

try {

list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);

} finally {

localCache.removeObject(key);

}

localCache.putObject(key, list);

return list;

}

复制代码

在真正查询数据库之前, 先往缓存中放入一个占位对象, 当从数据库中查询到数据后, 再将占位对象移除, 从而放入真正的缓存对象, 之所以有这个操作, 跟缓存所在的层次有关, 这个我们先不说, 等到我们揭秘完sqlSession, 真正开始缓存的分析时, 再来解释为啥mybatis为啥在这里会这样做, 这样做能够达到什么效果(小提示, sqlSession是会话的意思, localCache其实跟sqlSession是绑定的, 会话通常是一对一的, 如果出现了多个人与一个人同时使用一个会话(即多线程操作sqlsession), 需要抛出异常, 即类型转换异常…通过异常来告诉缓存的操作是非法的, 有点像集合遍历时对集合进行增删改查操作时引发的并发修改异常)

3.3.3、StatementHandler体系分析

在分析doQuery之前, 我们先来聊一下StatementHandler这个组件

public interface StatementHandler {

Statement prepare(Connection connection, Integer transactionTimeout);

void parameterize(Statement statement);

void batch(Statement statement);

int update(Statement statement);

List query(Statement statement, ResultHandler resultHandler);

Cursor queryCursor(Statement statement);

BoundSql getBoundSql();

ParameterHandler getParameterHandler();

}

复制代码

在执行doQuery之前, 我们手里有MappedStatement对象还有sql参数parameter, 前者保存了执行一个sql需要的相关信息, 后者是用来替换sql中问号的参数, 回想一下我们开始学习jdbc编程的时候, 有了这些, 我们要创建一个Statement对象, 然后设置每一个问号的参数, 最后调用其对应的sql方法, 而StatemetHandler正是利用手里已经存在的数据来完成这些jdbc操作的

  • prepare: 这个方法是用来创建Statement的, 可以创建预编译的PreparedStatement(即对问号进行sql参数替换, 防止sql注入), 也可以创建执行存储过程的CallableStatement, 还可以创建最普通的Statement(没有防止sql注入的功能)

  • parameterize: 进行问号替换, 或者说sql预编译, 如果是PreparedStatement, 即调用对应的setxxx方法而已, 如果是普通的Statement, 就不进行任何操作

  • batch、update、query、queryCursor就是执行对应的sql功能了

  • ParameterHandler: 参数处理器, 默认的实现类只做了一个功能, 利用java类型挑选对应的TypeHandler, 然后进行sql参数的设置, 其实就是完成了之前我们jdbc编程下调用PreparedStatement的setXXX方法而已

经过上面组件的分析, 我们可以联想到, StatementHandler其实就是用来完成jdbc编程的, 通用的流程应该是通过prepare方法创建Statement, 调用parameterize进行sql参数的映射, sql参数的映射在PreparedStatement情况下就是调用对应的setXXX方法, 而parameterize完成参数映射其实是利用ParameterHandler完成的, 所以需要用getParameterHandler方法获取参数处理器, 默认的ParameterHandler即利用java类型挑选对应的TypeHandler, 然后执行不同的setXXX方法, 因为参数处理器只有一个, 而且代码非常简单, 这里就不展开说明, 大家有兴趣可以去看看(利用typehandler完成功能)

分析完接口的功能后, 我们再来看继承体系就清晰多了, 如下图所示, BaseStatementHandler就是实现了上面我们说的这些操作, 因为这些操作是通用的, 如果是PreparedStatement那么就是完整的上述流程, 如果是普通的Statement, 那么parameterize方法就是空实现等等

PreparedStatementHandler则是利用PreparedStatement完成sql功能, SimpleStatementHandler则是创建最普通的Statement完成sql执行, CallableStatementHandler则是存储过程的调用, 非常清晰的三个实现类, 这些handler除了sql执行外还有对结果集的处理, 后面我们也会清晰的看到

RoutingStatementHandler与BaseStatementHandler处于同一级别, 其完成的是路由的功能, 根据当前的sql类型执行分别创建上面我们说的三个handler, 然后执行逻辑, 所以mybatis在创建StatementHandler的时候就是创建的RoutingStatementHandler, 然后通过不同的sql类型真正创建不同的StatementHandler来完成功能

public class RoutingStatementHandler implements StatementHandler {

private final StatementHandler delegate;

public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {

switch (ms.getStatementType()) {

case STATEMENT:

delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);

break;

case PREPARED:

delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);

break;

case CALLABLE:

delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);

break;

}

}

@Override

public Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException {

return delegate.prepare(connection, transactionTimeout);

}

}

复制代码

所以真正的功能操作RoutingStatementHandler啥也没做, 都是交给委派对象完成的

03_StatementHandler继承体系.png

  • 19
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值