共计 10192 个字符,预计需要花费 26 分钟才能阅读完成。
前言
在详解 MyBatis 的 SqlSession 获取流程文章中曾经晓得,MyBatis 中获取 SqlSession 时会创立执行器 Executor 并存放在 SqlSession 中,通过 SqlSession 能够获取映射接口的动静代理对象,动静代理对象的生成能够参考详解 MyBatis 加载映射文件和动静代理,能够用下图进行概括。
所以,映射接口的动静代理对象理论执行办法时,执行的申请最终会由 MapperMethod 的 execute() 办法实现。从 MapperMethod 的 execute() 办法开始,后续执行流程,能够用下图进行示意。
本篇文章将以 MapperMethod 的 execute() 办法作为终点,对 MyBatis 中的一次理论执行申请进行阐明,并联合源码对执行器 Executor 的原理进行阐释。
本篇文章不会对 MyBatis 中的缓存进行阐明,对于 MyBatis 中的一级缓存和二级缓存相干内容,会在后续的文章中独自进行剖析,为了屏蔽 MyBatis 中的二级缓存的烦扰,须要在 MyBatis 的配置文件中增加如下配置以禁用二级缓存。
<settings>
<setting name="cacheEnabled" value="false"/>
</settings>
复制代码
MyBatis 版本:3.5.6
注释
本节将以一个理论的查问例子,以单步跟踪并联合源码的办法,对 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 property="bookName" column="b_name"/> | |
<result property="bookPrice" column="b_price"/> | |
</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)); | |
} |
}
复制代码
基于上述的映射接口,映射文件和执行代码,最终执行查问操作时,会调用到 MapperMethod 的 execute() 办法并进入查问的逻辑分支,这部分源码如下所示。
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; |
}
复制代码
已知映射接口中的每个办法都会对应一个 MapperMethod,MapperMethod 中的 SqlCommand 会批示该办法对应的 MappedStatement 信息和类型信息(SELECT,UPDATE 等),MapperMethod 中的 MethodSignature 会存储该办法的参数信息和返回值信息,所以在上述的 MapperMethod 的 execute() 办法中,首先依据 SqlCommand 的批示的类型进入不同的逻辑分支,本示例中会进入 SELECT 的逻辑分支,而后又会依据 MethodSignature 中批示的办法返回值状况进入不同的查问分支,本示例中的办法返回值既不是汇合,map 或迭代器,也不是空,所以会进入查问一条数据的查问分支。
在 MapperMethod 中的 execute() 办法中会调用到 DefaultSqlSession 的 selectOne() 办法执行查问操作,该办法实现如下所示。
@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;} |
}
复制代码
DefaultSqlSession 的 selectOne() 办法中会将查问申请交由 DefaultSqlSession 的 selectList() 办法实现,如果 selectList() 办法返回的后果汇合中只有一个返回值,就将这个返回值返回,如果多于一个返回值,就报错。DefaultSqlSession 的 selectList() 办法如下所示。
@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();} |
}
复制代码
在 DefaultSqlSession 的 selectList() 办法中,会先依据 statement 参数值在 Configuration 中的 mappedStatements 缓存中获取 MappedStatement,statement 参数值其实就是 MapperMethod 中的 SqlCommand 的 name 字段,是 MappedStatement 在 mappedStatements 缓存中的惟一标识。获取到 MappedStatement 后,就会调用 Executor 的 query() 办法执行查问操作,因为禁用了二级缓存,所以这里的 Executor 实际上为 SimpleExecutor。本示例中单步跟踪到这里时,数据如下所示。
SimpleExecutor 的类图如下所示。
SimpleExecutor 和 BaseExecutor 之间应用了模板设计模式,调用 SimpleExecutor 的 query() 办法时会调用到 BaseExecutor 的 query() 办法,如下所示。
@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;
第二件事件是实例化 Statement;
第三件事件是执行查问。
第一件事件:创立 RoutingStatementHandler
实际上 RoutingStatementHandler 正如其名字所示,仅仅只是做一个路由转发的作用,在创立 RoutingStatementHandler 时,会依据映射文件中 CURD 标签上的 statementType 属性决定创立什么类型的 StatementHandler 并赋值给 RoutingStatementHandler 中的 delegate 字段,后续对 RoutingStatementHandler 的所有操作均会被其转发给 delegate。
此外在初始化 SimpleStatementHandler,PreparedStatementHandler 和 CallableStatementHandler 时还会一并初始化 ParameterHandler 和 ResultSetHandler。
映射文件中 CURD 标签上的 statementType 属性与 StatementHandler 的对应关系如下。
statementType 属性对应的 StatementHandler 作用 STATEMENTSimpleStatementHandler 间接操作 SQL,不进行预编译 PREPAREDPreparedStatementHandler 预编译 SQLCALLABLECallableStatementHandler 执行存储过程
RoutingStatementHandler 与 SimpleStatementHandler,PreparedStatementHandler 和 CallableStatementHandler 的关系能够用下图示意。
在创立 RoutingStatementHandler 之后,还会为 RoutingStatementHandler 植入插件逻辑。Configuration 的 newStatementHandler() 办法实现如下。
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; |
}
复制代码
第二件事件:实例化 Statement
prepareStatement() 办法实现如下所示。
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 中的? 替换为理论的参数值。
第三件事件:执行查问
本篇文章的示例中,映射文件的 CURD 标签没有对 statementType 属性进行设置,因而查问的操作最终会被 RoutingStatementHandler 路由转发给 PreparedStatementHandler 的 query() 办法,如下所示。
@Override
public <E> List<E> query(Statement statement, ResultHandler resultHandler)
throws SQLException {PreparedStatement ps = (PreparedStatement) statement; | |
// 调用到 JDBC 的逻辑了 | |
ps.execute(); | |
// 调用 ResultSetHandler 解决查问后果 | |
return resultSetHandler.handleResultSets(ps); |
}
复制代码
如上所示,在 PreparedStatementHandler 的 query() 办法中就会调用到 JDBC 的逻辑向数据库进行查问,最初还会应用曾经初始化好并植入了插件逻辑的 ResultSetHandler 解决查问后果并返回。
至此,对 MyBatis 的一次理论执行申请的阐明到此为止,本篇文章中的示例以查问为例,增删改大体相似,故不再赘述。
总结
MyBatis 中的执行器 Executor 会在创立 SqlSession 时一并被创立进去并被寄存于 SqlSession 中,如果禁用了二级缓存,则 Executor 理论为 SimpleExecutor,否则为 CachingExecutor。
MyBatis 中的一次理论执行,会由所执行办法对应的 MapperMethod 的 execute() 办法实现。在 execute() 办法中,会依据执行操作的类型(增改删查)调用 SqlSession 中的相应的办法,例如 insert(),update(),delete() 和 select() 等。MapperMethod 在这其中的作用就是 MapperMethod 关联着本次执行办法所对应的 SQL 语句以及入参和出参等信息。
在 SqlSession 的 insert(),update(),delete() 和 select() 等办法中,SqlSession 会将与数据库的操作交由执行器 Executor 来实现。无论是在 SimpleExecutor 还是 CachingExecutor 中,如果抛开缓存相干的逻辑,这些 Executor 均会先依据映射文件中 CURD 标签的 statementType 字段创立相应的 StatementHandler,创立 StatementHandler 的过程中还会一并将解决参数和处理结果的 ParameterHandler 和 ResultSetHandler 创立进去,创立好 StatementHandler 之后,会基于 StatementHandler 实例化 Statement,最初在 StatementHandler 中基于实例化好的 Statement 实现和数据库的交互,基于创立好的 ResultSetHandler 解决交互后果并将后果返回。