关于mybatis:Mybatis源码Executor的执行过程

44次阅读

共计 10066 个字符,预计需要花费 26 分钟才能阅读完成。

前言

在 Mybatis 源码 -SqlSession 获取文章中曾经晓得,Mybatis中获取 SqlSession 时会创立执行器 Executor 并存放在 SqlSession 中,通过 SqlSession 能够获取映射接口的动静代理对象,动静代理对象的生成能够参考 Mybatis 源码 - 加载映射文件与动静代理,能够用下图进行概括。

所以,映射接口的动静代理对象理论执行办法时,执行的申请最终会由 MapperMethodexecute()办法实现。本篇文章将以 MapperMethodexecute()办法作为终点,对 Mybatis 中的一次理论执行申请进行阐明,并联合源码对执行器 Executor 的原理进行阐释。本篇文章不会对 Mybatis 中的缓存进行阐明,对于 Mybatis 中的 一级缓存 二级缓存 相干内容,会在后续的文章中独自进行剖析,为了屏蔽 Mybatis 中的二级缓存的烦扰,须要在 Mybatis 的配置文件中增加如下配置以禁用二级缓存。

<settings>
    <setting name="cacheEnabled" value="false"/>
</settings>

注释

本节将以一个理论的查问例子,以单步跟踪并联合源码的办法,对 Mybatis 的一次理论执行申请进行阐明。给定映射接口如下所示。

public interface BookMapper {Book selectBookById(int id);

}

给定映射文件如下所示。

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.mybatis.learn.dao.BookMapper">
    <resultMap id="bookResultMap" type="com.mybatis.learn.entity.Book">
        <result column="b_name" property="bookName"/>
        <result column="b_price" property="bookPrice"/>
    </resultMap>

    <select id="selectBookById" resultMap="bookResultMap">
        SELECT
        b.id, b.b_name, b.b_price
        FROM
        book b
        WHERE
        b.id=#{id}
    </select>
</mapper>

Mybatis的执行代码如下所示。

public class MybatisTest {public static void main(String[] args) throws Exception {
        String resource = "mybatis-config.xml";
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder()
                .build(Resources.getResourceAsStream(resource));
        // 获取 SqlSession
        SqlSession sqlSession = sqlSessionFactory.openSession();
        // 获取映射接口的动静代理对象
        BookMapper bookMapper = sqlSession.getMapper(BookMapper.class);
        // 执行一次查问操作
        System.out.println(bookMapper.selectBookById(1));
    }

}

基于上述的映射接口,映射文件和执行代码,最终执行查问操作时,会调用到 MapperMethodexecute()办法并进入查问的逻辑分支,这部分源码如下所示。

public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;
    switch (command.getType()) {
        ......
        case SELECT:
            // 依据理论执行的办法的返回值的状况进入不同的逻辑分支
            if (method.returnsVoid() && method.hasResultHandler()) {
                // 无返回值状况
                executeWithResultHandler(sqlSession, args);
                result = null;
            } else if (method.returnsMany()) {
                // 返回值为汇合的状况
                result = executeForMany(sqlSession, args);
            } else if (method.returnsMap()) {
                // 返回值为 map 的状况
                result = executeForMap(sqlSession, args);
            } else if (method.returnsCursor()) {
                // 返回值为迭代器的状况
                result = executeForCursor(sqlSession, args);
            } else {
                // 上述情况之外的状况
                // 将办法的入参转换为 Sql 语句的参数
                Object param = method.convertArgsToSqlCommandParam(args);
                // 调用 DefaultSqlSession 的 selectOne()办法执行查问操作
                result = sqlSession.selectOne(command.getName(), param);
                if (method.returnsOptional()
                        && (result == null || !method.getReturnType().equals(result.getClass()))) {result = Optional.ofNullable(result);
                }
            }
            break;
        ......
    }
    ......
    return result;
}

已知映射接口中的每个办法都会对应一个 MapperMethodMapperMethod 中的 SqlCommand 会批示该办法对应的 MappedStatement 信息和类型信息(SELECTUPDATE等),MapperMethod中的 MethodSignature 会存储该办法的参数信息和返回值信息,所以在上述的 MapperMethodexecute()办法中,首先依据 SqlCommand 的批示的类型进入不同的逻辑分支,本示例中会进入 SELECT 的逻辑分支,而后又会依据 MethodSignature 中批示的办法返回值状况进入不同的查问分支,本示例中的办法返回值既不是汇合,map或迭代器,也不是空,所以会进入查问一条数据的查问分支。本示例中单步跟踪到这里时,数据如下所示。

MapperMethod 中的 execute() 办法中会调用到 DefaultSqlSessionselectOne()办法执行查问操作,该办法实现如下所示。

@Override
public <T> T selectOne(String statement) {return this.selectOne(statement, null);
}

@Override
public <T> T selectOne(String statement, Object parameter) {// 查问操作会由 selectList()实现
    List<T> list = this.selectList(statement, parameter);
    if (list.size() == 1) {
        // 查问后果只有一个时,返回查问后果
        return list.get(0);
    } else if (list.size() > 1) {
        // 查问后果大于一个时,报错
        throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found:" + list.size());
    } else {return null;}
}

DefaultSqlSessionselectOne() 办法中会将查问申请交由 DefaultSqlSessionselectList()办法实现,如果 selectList() 办法返回的后果汇合中只有一个返回值,就将这个返回值返回,如果多于一个返回值,就报错。DefaultSqlSessionselectList() 办法如下所示。

@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    try {
        // 从 Configuration 中的 mappedStatements 缓存中获取 MappedStatement
        MappedStatement ms = configuration.getMappedStatement(statement);
        // 调用 Executor 的 query()办法执行查问操作
        return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
    } catch (Exception e) {throw ExceptionFactory.wrapException("Error querying database.  Cause:" + e, e);
    } finally {ErrorContext.instance().reset();}
}

DefaultSqlSessionselectList()办法中,会先依据 statement 参数值在 Configuration 中的 mappedStatements 缓存中获取 MappedStatementstatement 参数值其实就是 MapperMethod 中的 SqlCommandname字段,是 MappedStatementmappedStatements缓存中的惟一标识。获取到 MappedStatement 后,就会调用 Executorquery()办法执行查问操作,因为禁用了二级缓存,所以这里的 Executor 实际上为SimpleExecutor。本示例中单步跟踪到这里时,数据如下所示。

SimpleExecutor的类图如下所示。

SimpleExecutorBaseExecutor 之间应用了模板设计模式,调用 SimpleExecutorquery()办法时会调用到 BaseExecutorquery()办法,如下所示。

@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, 
                       ResultHandler resultHandler) throws SQLException {
    // 获取 Sql 语句
    BoundSql boundSql = ms.getBoundSql(parameter);
    // 生成 CacheKey
    CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
    // 调用重载的 query()办法
    return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}

持续看 BaseExecutor 中的重载的 query() 办法,如下所示。

@Override
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());
    if (closed) {throw new ExecutorException("Executor was closed.");
    }
    if (queryStack == 0 && ms.isFlushCacheRequired()) {clearLocalCache();
    }
    List<E> list;
    try {
        queryStack++;
        // 先从一级缓存中依据 CacheKey 命中查问后果
        list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
        if (list != null) {
            // 胜利命中,则返回缓存中的查问后果
            handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
        } else {
            // 未命中,则间接查数据库
            list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
        }
    } finally {queryStack--;}
    if (queryStack == 0) {for (BaseExecutor.DeferredLoad deferredLoad : deferredLoads) {deferredLoad.load();
        }
        deferredLoads.clear();
        if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {clearLocalCache();
        }
    }
    return list;
}

上述的 query() 办法大部分逻辑是在为 Mybatis 中的一级缓存服务,这里临时不剖析,除开缓存的逻辑,上述 query() 办法做的事件能够概括为:先从缓存中获取查问后果,获取到则返回缓存中的查问后果,否则间接查询数据库。上面剖析间接查询数据库的逻辑,queryFromDatabase()办法的实现如下所示。

private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, 
                    ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    List<E> list;
    localCache.putObject(key, EXECUTION_PLACEHOLDER);
    try {// 调用 doQuery()进行查问操作
        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;
}

上述 queryFromDatabase() 办法中,会调用 BaseExecutor 定义的形象办法 doQuery() 进行查问,本示例中,doQuery()办法由 SimpleExecutor 进行了实现,如下所示。

@Override
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();
        // 创立 RoutingStatementHandler
        StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
        // 实例化 Statement
        stmt = prepareStatement(handler, ms.getStatementLog());
        // 执行查问
        return handler.query(stmt, resultHandler);
    } finally {closeStatement(stmt);
    }
}

上述的 doQuery() 办法中,做了三件事件,第一件事件 是创立 RoutingStatementHandler,实际上RoutingStatementHandler 正如其名字所示,仅仅只是做一个路由转发的作用,在创立 RoutingStatementHandler 时,会依据映射文件中 CURD 标签上的 statementType 属性决定创立什么类型的 StatementHandler 并赋值给 RoutingStatementHandler 中的 delegate 字段,后续对 RoutingStatementHandler 的所有操作均会被其转发给 delegate,此外在初始化SimpleStatementHandlerPreparedStatementHandlerCallableStatementHandler时还会一并初始化 ParameterHandlerResultSetHandler。映射文件中 CURD 标签上的 statementType 属性与 StatementHandler 的对应关系如下。

statementType 属性 对应的StatementHandler 作用
STATEMENT SimpleStatementHandler 间接操作SQL,不进行预编译
PREPARED PreparedStatementHandler 预编译SQL
CALLABLE CallableStatementHandler 执行存储过程

RoutingStatementHandlerSimpleStatementHandlerPreparedStatementHandlerCallableStatementHandler的关系能够用下图示意。

在创立 RoutingStatementHandler 之后,还会为 RoutingStatementHandler 植入插件逻辑。ConfigurationnewStatementHandler() 办法实现如下。

public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, 
            Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
    // 创立 RoutingStatementHandler
    StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
    // 为 RoutingStatementHandler 植入插件逻辑
    statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
    return statementHandler;
}

持续剖析 doQuery() 办法中的 第二件事件 ,即实例化StatementprepareStatement() 办法实现如下所示。

private Statement prepareStatement(StatementHandler handler, Log statementLog) 
                throws SQLException {
    Statement stmt;
    // 获取到 Connection 对象并为 Connection 对象生成动静代理对象
    Connection connection = getConnection(statementLog);
    // 通过 Connection 对象的动静代理对象实例化 Statement
    stmt = handler.prepare(connection, transaction.getTimeout());
    handler.parameterize(stmt);
    return stmt;
}

prepareStatement()办法中首先会从 Transaction 中将数据库连贯对象 Connection 对象获取进去并为其生成动静代理对象以实现日志打印性能的加强,而后会通过 Connection 的动静代理对象实例化 Statement,最初会解决Statement 中的占位符,比方将 PreparedStatement 中的 ? 替换为理论的参数值。

持续剖析 doQuery() 办法中的 第三件事件 ,即执行查问。本篇文章的示例中,映射文件的CURD 标签没有对 statementType 属性进行设置,因而查问的操作最终会被 RoutingStatementHandler 路由转发给 PreparedStatementHandlerquery()办法,如下所示。

@Override
public <E> List<E> query(Statement statement, ResultHandler resultHandler) 
            throws SQLException {PreparedStatement ps = (PreparedStatement) statement;
    // 调用到 JDBC 的逻辑了
    ps.execute();
    // 调用 ResultSetHandler 解决查问后果
    return resultSetHandler.handleResultSets(ps);
}

如上所示,在 PreparedStatementHandlerquery()办法中就会调用到 JDBC 的逻辑向数据库进行查问,最初还会应用曾经初始化好并植入了插件逻辑的 ResultSetHandler 解决查问后果并返回。

至此,对 Mybatis 的一次理论执行申请的阐明到此为止,本篇文章中的示例以查问为例,增删改大体相似,故不再赘述。

总结

Mybatis中的执行器 Executor 会在创立 SqlSession 时一并被创立进去并被寄存于 SqlSession 中,如果禁用了二级缓存,则 Executor 理论为 SimpleExecutor,否则为CachingExecutorMybatis 中的一次理论执行,会由所执行办法对应的 MapperMethodexecute()办法实现,在 execute() 办法中,会依据执行操作的类型(增改删查 )调用SqlSession 中的相应的办法,例如 insert()update()delete()select()等,MapperMethod在这其中的作用就是 MapperMethod 关联着本次执行办法所对应的 SQL 语句以及入参和出参等信息。在 SqlSessioninsert()update()delete()select() 等办法中,SqlSession会将与数据库的操作交由执行器 Executor 来实现,无论是在 SimpleExecutor 还是 CachingExecutor 中,如果抛开缓存相干的逻辑,这些 Executor 均会先依据映射文件中 CURD 标签的 statementType 字段创立相应的 StatementHandler,创立StatementHandler 的过程中还会一并将解决参数和处理结果的 ParameterHandlerResultSetHandler创立进去,创立好 StatementHandler 之后,会基于 StatementHandler 实例化 Statement,最初在StatementHandler 中基于实例化好的 Statement 实现和数据库的交互,基于创立好的 ResultSetHandler 解决交互后果并将后果返回。

正文完
 0