从 Mybatis 源码到 Spring 动态数据源底层原理分析系列二、Mybatis 执行器源码分析
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) {
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}
List<E> list;
try {
queryStack++;
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--;
}
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 <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) {
List<E> 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);
<E> List<E> query(Statement statement, ResultHandler resultHandler);
<E> Cursor<E> 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, boun
dSql);
break;
}
}
@Override
public Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException {
return delegate.prepare(connection, transactionTimeout);
}
..............
}
复制代码
所以真正的功能操作 RoutingStatementHandler 啥也没做, 都是交给委派对象完成的
3.3.4、SimpleExecutor 的 doQuery 方法源码分析
再次回到我们的 doQuery 方法:
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
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);
}
}
复制代码
可以看到, 通过 MappedStatement 拿到 mybatis 最顶级的配置对象 Configuration, 然后创建 StatementHandler, 根据我们对 StatementHandler 的分析, 这里其实创建的是 RoutingStatementHandler
private Statement prepareStatement(StatementHandler handler, Log statementLog) {
Statement stmt;
Connection connection = getConnection(statementLog);
stmt = handler.prepare(connection, transaction.getTimeout());
handler.parameterize(stmt);
return stmt;
}
复制代码
prepareStatement 方法非常简单, 获取 Connection, 调用 StatementHandler 的 prepare 方法创建 Statement, 然后进行参数化, 如果是 PreparedStatement, 则大概完成的操作是:
PreparedStatement statement = connection.prepareStatement(sql);
然后利用 typehandler 调用 statement.setString(xxx)这样的方法完成参数的设置
复制代码
doQuery 最后调用了 StatementHandler 的 query 方法, 我们以 PreparedStatementHandler 来进行分析
public <E> List<E> query(Statement statement, ResultHandler resultHandler) {
PreparedStatement ps = (PreparedStatement) statement;
ps.execute();
return resultSetHandler.handleResultSets(ps);
}
复制代码
非常简单, 就是强转为 PreparedStatement, 然后调用 execute 方法执行 sql, 最后将 ResultSet 映射为 java 对象
3.3.5、getConnection 方法分析
protected Connection getConnection(Log statementLog) {
Connection connection = transaction.getConnection();
}
非常简单, 就是利用 transaction 来完成连接的获取, 我们来看看 Transaction:
评论