詳解MyBatis中Executor執(zhí)行SQL語句的過程
前言
在詳解MyBatis的SqlSession獲取流程文章中已經(jīng)知道,MyBatis中獲取SqlSession時(shí)會創(chuàng)建執(zhí)行器Executor并存放在SqlSession中,通過SqlSession可以獲取映射接口的動態(tài)代理對象,可以用下圖進(jìn)行概括

所以,映射接口的動態(tài)代理對象實(shí)際執(zhí)行方法時(shí),執(zhí)行的請求最終會由MapperMethod的execute() 方法完成。從MapperMethod的execute() 方法開始,后續(xù)執(zhí)行流程,可以用下圖進(jìn)行示意。

本篇文章將以MapperMethod的execute() 方法作為起點(diǎn),對MyBatis中的一次實(shí)際執(zhí)行請求進(jìn)行說明,并結(jié)合源碼對執(zhí)行器Executor的原理進(jìn)行闡釋。
本篇文章不會對MyBatis中的緩存進(jìn)行說明,關(guān)于MyBatis中的一級緩存和二級緩存相關(guān)內(nèi)容,會在后續(xù)的文章中單獨(dú)進(jìn)行分析,為了屏蔽MyBatis中的二級緩存的干擾,需要在MyBatis的配置文件中添加如下配置以禁用二級緩存。
<settings>
<setting name="cacheEnabled" value="false"/>
</settings>MyBatis版本:3.5.6
正文
本節(jié)將以一個(gè)實(shí)際的查詢例子,以單步跟蹤并結(jié)合源碼的方法,對MyBatis的一次實(shí)際執(zhí)行請求進(jìn)行說明。給定映射接口如下所示。
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的執(zhí)行代碼如下所示。
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();
// 獲取映射接口的動態(tài)代理對象
BookMapper bookMapper = sqlSession.getMapper(BookMapper.class);
// 執(zhí)行一次查詢操作
System.out.println(bookMapper.selectBookById(1));
}
}基于上述的映射接口,映射文件和執(zhí)行代碼,最終執(zhí)行查詢操作時(shí),會調(diào)用到MapperMethod的execute() 方法并進(jìn)入查詢的邏輯分支,這部分源碼如下所示。
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
switch (command.getType()) {
// ......
case SELECT:
// 根據(jù)實(shí)際執(zhí)行的方法的返回值的情況進(jìn)入不同的邏輯分支
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 {
// 上述情況之外的情況
// 將方法的入?yún)⑥D(zhuǎn)換為Sql語句的參數(shù)
Object param = method.convertArgsToSqlCommandParam(args);
// 調(diào)用DefaultSqlSession的selectOne()方法執(zhí)行查詢操作
result = sqlSession.selectOne(command.getName(), param);
if (method.returnsOptional()
&& (result == null || !method.getReturnType().equals(result.getClass()))) {
result = Optional.ofNullable(result);
}
}
break;
// ......
}
// ......
return result;
}已知映射接口中的每個(gè)方法都會對應(yīng)一個(gè)MapperMethod,MapperMethod中的SqlCommand會指示該方法對應(yīng)的MappedStatement信息和類型信息(SELECT,UPDATE等),MapperMethod中的MethodSignature會存儲該方法的參數(shù)信息和返回值信息,所以在上述的MapperMethod的execute() 方法中,首先根據(jù)SqlCommand的指示的類型進(jìn)入不同的邏輯分支,本示例中會進(jìn)入SELECT的邏輯分支,然后又會根據(jù)MethodSignature中指示的方法返回值情況進(jìn)入不同的查詢分支,本示例中的方法返回值既不是集合,map或迭代器,也不是空,所以會進(jìn)入查詢一條數(shù)據(jù)的查詢分支。
在MapperMethod中的execute() 方法中會調(diào)用到DefaultSqlSession的selectOne() 方法執(zhí)行查詢操作,該方法實(shí)現(xiàn)如下所示。
@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) {
// 查詢結(jié)果只有一個(gè)時(shí),返回查詢結(jié)果
return list.get(0);
} else if (list.size() > 1) {
// 查詢結(jié)果大于一個(gè)時(shí),報(bào)錯(cuò)
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() 方法返回的結(jié)果集合中只有一個(gè)返回值,就將這個(gè)返回值返回,如果多于一個(gè)返回值,就報(bào)錯(cuò)。DefaultSqlSession的selectList() 方法如下所示。
@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
// 從Configuration中的mappedStatements緩存中獲取MappedStatement
MappedStatement ms = configuration.getMappedStatement(statement);
// 調(diào)用Executor的query()方法執(zhí)行查詢操作
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() 方法中,會先根據(jù)statement參數(shù)值在Configuration中的mappedStatements緩存中獲取MappedStatement,statement參數(shù)值其實(shí)就是MapperMethod中的SqlCommand的name字段,是MappedStatement在mappedStatements緩存中的唯一標(biāo)識。獲取到MappedStatement后,就會調(diào)用Executor的query() 方法執(zhí)行查詢操作,因?yàn)榻昧硕壘彺?,所以這里的Executor實(shí)際上為SimpleExecutor。本示例中單步跟蹤到這里時(shí),數(shù)據(jù)如下所示。

SimpleExecutor的類圖如下所示。

SimpleExecutor和BaseExecutor之間使用了模板設(shè)計(jì)模式,調(diào)用SimpleExecutor的query() 方法時(shí)會調(diào)用到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);
// 調(diào)用重載的query()方法
return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}繼續(xù)看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++;
// 先從一級緩存中根據(jù)CacheKey命中查詢結(jié)果
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
if (list != null) {
// 成功命中,則返回緩存中的查詢結(jié)果
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
// 未命中,則直接查數(shù)據(jù)庫
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中的一級緩存服務(wù),這里暫時(shí)不分析,除開緩存的邏輯,上述query() 方法做的事情可以概括為:
- 先從緩存中獲取查詢結(jié)果;
- 獲取到則返回緩存中的查詢結(jié)果;
- 否則直接查詢數(shù)據(jù)庫。
下面分析直接查詢數(shù)據(jù)庫的邏輯,queryFromDatabase() 方法的實(shí)現(xiàn)如下所示。
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 {
// 調(diào)用doQuery()進(jìn)行查詢操作
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
localCache.removeObject(key);
}
// 將查詢結(jié)果添加到一級緩存中
localCache.putObject(key, list);
if (ms.getStatementType() == StatementType.CALLABLE) {
localOutputParameterCache.putObject(key, parameter);
}
// 返回查詢結(jié)果
return list;
}上述queryFromDatabase() 方法中,會調(diào)用BaseExecutor定義的抽象方法doQuery() 進(jìn)行查詢,本示例中,doQuery() 方法由SimpleExecutor進(jìn)行了實(shí)現(xiàn),如下所示。
@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();
// 創(chuàng)建RoutingStatementHandler
StatementHandler handler = configuration.newStatementHandler(
wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
// 實(shí)例化Statement
stmt = prepareStatement(handler, ms.getStatementLog());
// 執(zhí)行查詢
return handler.query(stmt, resultHandler);
} finally {
closeStatement(stmt);
}
}上述的doQuery() 方法中,做了三件事情:
第一件事情是創(chuàng)建RoutingStatementHandler;第二件事情是實(shí)例化Statement;第三件事情是執(zhí)行查詢。
第一件事情:創(chuàng)建RoutingStatementHandler
實(shí)際上RoutingStatementHandler正如其名字所示,僅僅只是做一個(gè)路由轉(zhuǎn)發(fā)的作用,在創(chuàng)建RoutingStatementHandler時(shí),會根據(jù)映射文件中CURD標(biāo)簽上的statementType屬性決定創(chuàng)建什么類型的StatementHandler并賦值給RoutingStatementHandler中的delegate字段,后續(xù)對RoutingStatementHandler的所有操作均會被其轉(zhuǎn)發(fā)給delegate。
此外在初始化SimpleStatementHandler,PreparedStatementHandler和CallableStatementHandler時(shí)還會一并初始化ParameterHandler和ResultSetHandler。
映射文件中CURD標(biāo)簽上的statementType屬性與StatementHandler的對應(yīng)關(guān)系如下。
| statementType屬性 | 對應(yīng)的StatementHandler | 作用 |
|---|---|---|
| STATEMENT | SimpleStatementHandler | 直接操作SQL,不進(jìn)行預(yù)編譯 |
| PREPARED | PreparedStatementHandler | 預(yù)編譯SQL |
| CALLABLE | CallableStatementHandler | 執(zhí)行存儲過程 |
RoutingStatementHandler與SimpleStatementHandler,PreparedStatementHandler和CallableStatementHandler的關(guān)系可以用下圖示意。

在創(chuàng)建RoutingStatementHandler之后,還會為RoutingStatementHandler植入插件邏輯。Configuration的newStatementHandler() 方法實(shí)現(xiàn)如下。
public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement,
Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
// 創(chuàng)建RoutingStatementHandler
StatementHandler statementHandler = new RoutingStatementHandler(
executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
// 為RoutingStatementHandler植入插件邏輯
statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
return statementHandler;
}第二件事情:實(shí)例化Statement
prepareStatement() 方法實(shí)現(xiàn)如下所示。
private Statement prepareStatement(StatementHandler handler, Log statementLog)
throws SQLException {
Statement stmt;
// 獲取到Connection對象并為Connection對象生成動態(tài)代理對象
Connection connection = getConnection(statementLog);
// 通過Connection對象的動態(tài)代理對象實(shí)例化Statement
stmt = handler.prepare(connection, transaction.getTimeout());
handler.parameterize(stmt);
return stmt;
}prepareStatement() 方法中首先會從Transaction中將數(shù)據(jù)庫連接對象Connection對象獲取出來并為其生成動態(tài)代理對象以實(shí)現(xiàn)日志打印功能的增強(qiáng),然后會通過Connection的動態(tài)代理對象實(shí)例化Statement,最后會處理Statement中的占位符,比如將PreparedStatement中的?替換為實(shí)際的參數(shù)值。
第三件事情:執(zhí)行查詢
本篇文章的示例中,映射文件的CURD標(biāo)簽沒有對statementType屬性進(jìn)行設(shè)置,因此查詢的操作最終會被RoutingStatementHandler路由轉(zhuǎn)發(fā)給PreparedStatementHandler的query() 方法,如下所示。
@Override
public <E> List<E> query(Statement statement, ResultHandler resultHandler)
throws SQLException {
PreparedStatement ps = (PreparedStatement) statement;
// 調(diào)用到JDBC的邏輯了
ps.execute();
// 調(diào)用ResultSetHandler處理查詢結(jié)果
return resultSetHandler.handleResultSets(ps);
}如上所示,在PreparedStatementHandler的query() 方法中就會調(diào)用到JDBC的邏輯向數(shù)據(jù)庫進(jìn)行查詢,最后還會使用已經(jīng)初始化好并植入了插件邏輯的ResultSetHandler處理查詢結(jié)果并返回。
至此,對MyBatis的一次實(shí)際執(zhí)行請求的說明到此為止,本篇文章中的示例以查詢?yōu)槔?,增刪改大體類似,故不再贅述。
總結(jié)
MyBatis中的執(zhí)行器Executor會在創(chuàng)建SqlSession時(shí)一并被創(chuàng)建出來并被存放于SqlSession中,如果禁用了二級緩存,則Executor實(shí)際為SimpleExecutor,否則為CachingExecutor。
MyBatis中的一次實(shí)際執(zhí)行,會由所執(zhí)行方法對應(yīng)的MapperMethod的execute() 方法完成。在execute() 方法中,會根據(jù)執(zhí)行操作的類型(增改刪查)調(diào)用SqlSession中的相應(yīng)的方法,例如insert(),update(),delete() 和select() 等。MapperMethod在這其中的作用就是MapperMethod關(guān)聯(lián)著本次執(zhí)行方法所對應(yīng)的SQL語句以及入?yún)⒑统鰠⒌刃畔ⅰ?/p>
在SqlSession的insert(),update(),delete() 和select() 等方法中,SqlSession會將與數(shù)據(jù)庫的操作交由執(zhí)行器Executor來完成。無論是在SimpleExecutor還是CachingExecutor中,如果拋開緩存相關(guān)的邏輯,這些Executor均會先根據(jù)映射文件中CURD標(biāo)簽的statementType字段創(chuàng)建相應(yīng)的StatementHandler,創(chuàng)建StatementHandler的過程中還會一并將處理參數(shù)和處理結(jié)果的ParameterHandler和ResultSetHandler創(chuàng)建出來,創(chuàng)建好StatementHandler之后,會基于StatementHandler實(shí)例化Statement,最后在StatementHandler中基于實(shí)例化好的Statement完成和數(shù)據(jù)庫的交互,基于創(chuàng)建好的ResultSetHandler處理交互結(jié)果并將結(jié)果返回。
以上就是詳解MyBatis中Executor執(zhí)行SQL語句的過程的詳細(xì)內(nèi)容,更多關(guān)于MyBatis Executor執(zhí)行SQL語句的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
redis.clients.jedis.exceptions.JedisAskDataException異常解決
redis.clients.jedis.exceptions.JedisAskDataExceptio異常是在使用Jedis客戶端與Redis集群交互時(shí)遇到的一種重定向異常,本文就來介紹一下解決方法,感興趣的可以了解一下2024-05-05
RocketMQ中消費(fèi)者的消費(fèi)進(jìn)度管理
這篇文章主要介紹了RocketMQ中消費(fèi)者的消費(fèi)進(jìn)度管理,業(yè)務(wù)實(shí)現(xiàn)消費(fèi)回調(diào)的時(shí)候,當(dāng)且僅當(dāng)此回調(diào)函數(shù)返回ConsumeConcurrentlyStatus.CONSUME_SUCCESS ,RocketMQ才會認(rèn)為這批消息(默認(rèn)是1條)是消費(fèi)完成的,需要的朋友可以參考下2023-10-10
Hibernate實(shí)現(xiàn)悲觀鎖和樂觀鎖代碼介紹
這篇文章主要介紹了Hibernate實(shí)現(xiàn)悲觀鎖和樂觀鎖的有關(guān)內(nèi)容,涉及hibernate的隔離機(jī)制,以及實(shí)現(xiàn)悲觀鎖和樂觀鎖的代碼實(shí)現(xiàn),需要的朋友可以了解下。2017-09-09
Java concurrency之Condition條件_動力節(jié)點(diǎn)Java學(xué)院整理
Condition的作用是對鎖進(jìn)行更精確的控制。下面通過本文給大家分享Java concurrency之Condition條件的相關(guān)知識,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友參考下吧2017-06-06
Java 如何從list中刪除符合條件的數(shù)據(jù)
這篇文章主要介紹了Java 如何從list中刪除符合條件的數(shù)據(jù),具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-11-11
SpringBoot 引?MybatisGenerator的實(shí)現(xiàn)步驟
本文主要介紹了SpringBoot 引?MybatisGenerator的實(shí)現(xiàn)步驟,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2025-02-02
解決Java 部署Tomcat時(shí)使用jni和jna調(diào)用DLL文件的問題
這篇文章主要介紹了解決Java 部署Tomcat時(shí)使用jni和jna調(diào)用DLL文件的問題,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-11-11
Java類的序列化版本唯一標(biāo)識符serialVersionUID使用
serialVersionUID是一個(gè)類的序列化版本唯一標(biāo)識符,用于確保在反序列化過程中類的實(shí)例與序列化文件中的類版本相匹配,它在版本兼容性和安全性方面起著關(guān)鍵作用2025-01-01
如何寫好一個(gè)Spring組件的實(shí)現(xiàn)步驟
這篇文章主要介紹了如何寫好一個(gè)Spring組件的實(shí)現(xiàn)步驟,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-06-06

