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

前言

在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解决交互后果并将后果返回。

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理