日常開發使用mybatis進行CURD操作十分簡便,我們只需要在Mapper接口定義好方法,然后在mapper.xml中寫好SQL語句,就能在業務代碼中使用了。這簡單到令人發指步驟,讓我們麻痹到以為這個過程是非常簡單的,時間久了腦子都笨了。今天,接著上一篇文章中的例子,結合源碼學習一下mybatis中一條SQL的執行過程,了解下背后發生的故事。
# 從Demo開始
還是之前的例子,代碼如下。
## 示例代碼
~~~xml
<!--CompanyDAO.xml-->
<select id="selectById" resultMap="baseResultMap" >
select
<include refid="BaseColumns"></include>
from company
where id= #{id}
</select>
復制代碼
~~~
CompanyDAO.xml,按照id查詢數據表company的記錄,id為主鍵,selectById為statementId。
~~~java
public interface CompanyDao {
/**
* 根據id查詢CompanyDO
*
* @param id 主鍵id
* @return 查詢結果
*/
CompanyDO selectById(@Param("id") Integer id);
}
復制代碼
~~~
CompanyDao.java,Mapper接口,僅包含一個方法selectById,按照mybatis要求,與mapper的xml配置文件一一對應。
~~~java
public class Program {
public static void main(String[] args) throws IOException {
// mybatis配置文件
String path = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(path);
// 獲取 SqlSessionFactory
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
// 創建 SqlSession
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
//獲取Mapper:CompanyDao
CompanyDao dao = sqlSession.getMapper(CompanyDao.class);
//調用selectById方法
CompanyDO companyDO = dao.selectById(1);
System.out.println(companyDO);
}
}
}
復制代碼
~~~
Program.java業務代碼,調用邏輯比較簡單,再啰嗦一下:
* 通過mybatis-config.xml創建SqlSessionFactory;
* 使用SqlSessionFactory創建并打開SqlSession;
* 通過sqlSession獲取CompanyDao的Mapper實例對象;
* 調用selectById方法獲取查詢結果;
## 示例分析
由以上示例代碼可知,CompanyDao是一個接口,我們并沒有編寫它的實現類,那為啥可以創建它的對象呢?我們現在得到的到底是哪個類的實例?
打個斷點!下面調試界面截圖,我們發現dao是org.apache.ibatis.binding.MapperProxy的實例。

熟悉動態代理的同學,看一眼就知道MapperProxy是一個代理類。我們就從這里入手,順藤摸瓜。
# 流程分析
## Mapper對象怎么來的?
MapperProxy對象是通過SqlSession#getMapper方法創建的,這里的sqlSession是DefaultSqlSession的實例,跟著源碼走一下。
~~~java
//org.apache.ibatis.session.defaults.DefaultSqlSession#getMapper
@Override
public <T> T getMapper(Class<T> type) {
//這里調用了configuration的方法,繼續走
return configuration.<T>getMapper(type, this);
}
//org.apache.ibatis.session.Configuration#getMapper
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
//所有的Mapper都注冊在Configuration#mapperRegistry(不過是存儲的MapperProxyFactory),所以是通過它來獲取Mapper對象
//下一步進入MapperRegistry#getMapper
return mapperRegistry.getMapper(type, sqlSession);
}
//org.apache.ibatis.binding.MapperRegistry#getMapper
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
//通過type從map中獲取MapperProxyFactory實例
final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
//如果mapperProxyFactory,則拋出異常
if (mapperProxyFactory == null) {
throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
}
try {
//通過mapperProxyFactory的newInstance創建實例,繼續走
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception e) {
throw new BindingException("Error getting mapper instance. Cause: " + e, e);
}
}
//org.apache.ibatis.binding.MapperProxyFactory#newInstance(org.apache.ibatis.session.SqlSession)
public T newInstance(SqlSession sqlSession) {
//先創建了MapperProxy對象
final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
//調用了newInstance方法
return newInstance(mapperProxy);
}
//org.apache.ibatis.binding.MapperProxyFactory#newInstance
protected T newInstance(MapperProxy<T> mapperProxy) {
//到這里就揭開了代理對象創建的面紗
//使用Proxy創建了代理對象
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
復制代碼
~~~
為了方便梳理,上面把Mapper創建過程的代碼放在一起了,我們可以發現整個過程是通過Configuration串在一起的(Configuration是mybatis運行時的基礎)。簡單描述一下過程:
* 入口在DefaultSqlSession#getMapper,整個方式只是一個中繼,需要通過Configuration#getMapper來創建;
* 在解析mybatis配置時,所有的Mapper注冊信息由Configuration#mapperRegistry存儲,但是存儲的其實是一個與Mapper類型相關的MapperProxyFactory,既然是Factory,那就要生產點啥了。
* MapperProxyFactory創建了MapperProxy對象,然后通過Proxy創建了代理對象,由MapperProxy偽裝了一個Mapper實例。
所以,Mapper對象(示例中的dao)其實一個“披著Mapper皮的MapperProxy對象”。上述過程雖然跳轉的類比較多,一層一層走下來最主要的就是Java動態代理相關的內容。通過下面的時序圖再來看下Mapper對象的創建過程吧。

## 代理對象invoke方法執行
創建Mapper對象后,接下來調用Mapper方法,從下面的代碼開始:
~~~java
CompanyDO companyDO = dao.selectById(1);
復制代碼
~~~
dao為MapperProxy對象,當調用selectById時,會先調用org.apache.ibatis.binding.MapperProxy#invoke方法(這里可以看下Java動態代理相關的文章,保存出代理類的動態實現,有機會寫篇文章)。
~~~java
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
//判斷是否為Object類
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
}
//判斷是否為默認方法
else if (isDefaultMethod(method)) {
return invokeDefaultMethod(proxy, method, args);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
//走到這里說明是被代理接口的方法,獲取或設置MapperMethod緩存,得到MapperMethod對象
final MapperMethod mapperMethod = cachedMapperMethod(method);
//方法執行
return mapperMethod.execute(sqlSession, args);
}
private MapperMethod cachedMapperMethod(Method method) {
//如果緩存中不存在method,就創建一個對象添加到緩存。
return methodCache.computeIfAbsent(method, k -> new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
}
復制代碼
~~~
到這里,我們可以知道方法invoke最終執行的是org.apache.ibatis.binding.MapperMethod#execute,接著看MapperMethod這個類。
~~~java
public class MapperMethod {
private final SqlCommand command;
private final MethodSignature method;
public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
//sql命令,包含mapper接口、mapper方法和configuration
this.command = new SqlCommand(config, mapperInterface, method);
//方法簽名
this.method = new MethodSignature(config, mapperInterface, method);
}
}
復制代碼
~~~
MapperMethod構造方法內步創建了SqlCommand和MethodSignature,這兩個類都是MapperMethod的內部類。等下看org.apache.ibatis.binding.MapperMethod#execute時會發現,它的執行與這兩個類有很大關系,我們先來看下這兩個做了什么。
~~~java
public static class SqlCommand {
private final String name;
//sql命令類型枚舉:UNKNOWN, INSERT, UPDATE, DELETE, SELECT, FLUSH;
private final SqlCommandType type;
//構造方法:主要邏輯
public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) {
//獲取方法名稱:示例中就是:selectById
final String methodName = method.getName();
//方法所在類的全路徑限定名稱:com.raysonxin.dao.CompanyDao
final Class<?> declaringClass = method.getDeclaringClass();
//解析獲取MappedStatement:看下面方法的注釋。
//這樣就可以拿到MappedStatement對象了
MappedStatement ms = resolveMappedStatement(mapperInterface, methodName, declaringClass,
configuration);
if (ms == null) {
if(method.getAnnotation(Flush.class) != null){
name = null;
type = SqlCommandType.FLUSH;
} else {
throw new BindingException("Invalid bound statement (not found): "
+ mapperInterface.getName() + "." + methodName);
}
} else {
//獲取id
name = ms.getId();
//獲取命令類型
type = ms.getSqlCommandType();
if (type == SqlCommandType.UNKNOWN) {
throw new BindingException("Unknown execution method for: " + name);
}
}
}
public String getName() {
return name;
}
public SqlCommandType getType() {
return type;
}
//解析獲取MappedStatement
private MappedStatement resolveMappedStatement(Class<?> mapperInterface, String methodName,
Class<?> declaringClass, Configuration configuration) {
//拼接獲取statement,示例中是:com.raysonxin.dao.CompanyDao.selectById
//這其實就是Configuration中MappedStatement注冊的key
String statementId = mapperInterface.getName() + "." + methodName;
//判斷configuration是否存在這個statementId
if (configuration.hasStatement(statementId)) {
//如果存在,獲取并返回
return configuration.getMappedStatement(statementId);
} else if (mapperInterface.equals(declaringClass)) {
return null;
}
//這里是從父接口中獲取的邏輯
for (Class<?> superInterface : mapperInterface.getInterfaces()) {
if (declaringClass.isAssignableFrom(superInterface)) {
MappedStatement ms = resolveMappedStatement(superInterface, methodName,
declaringClass, configuration);
if (ms != null) {
return ms;
}
}
}
return null;
}
}
復制代碼
~~~
SqlCommand就是根據接口和方法的簽名信息,從Configuration中獲取對應的MappedStatement,MappedStatement是在解析mybatis-config.xml是添加的,大家可以翻閱Configuration解析代碼,或者查看上一篇文章《Mybatis源碼之Configuration》。SqlCommand創建后,我們就可以清楚知道了其對應的MappedStatement,也就知道了它是查詢、更新、刪除還是添加這樣的命令類型。
~~~java
public static class MethodSignature {
//返回結果是否為多個值,即列表
private final boolean returnsMany;
//返回結果是否為Map類型
private final boolean returnsMap;
//返回結果是否為void
private final boolean returnsVoid;
//返回結果是否為游標
private final boolean returnsCursor;
//返回結果是否為Optioanl類型
private final boolean returnsOptional;
//返回值類型
private final Class<?> returnType;
private final String mapKey;
//
private final Integer resultHandlerIndex;
//查詢數據范圍參數索引
private final Integer rowBoundsIndex;
private final ParamNameResolver paramNameResolver;
public MethodSignature(Configuration configuration, Class<?> mapperInterface, Method method) {
Type resolvedReturnType = TypeParameterResolver.resolveReturnType(method, mapperInterface);
if (resolvedReturnType instanceof Class<?>) {
this.returnType = (Class<?>) resolvedReturnType;
} else if (resolvedReturnType instanceof ParameterizedType) {
this.returnType = (Class<?>) ((ParameterizedType) resolvedReturnType).getRawType();
} else {
this.returnType = method.getReturnType();
}
this.returnsVoid = void.class.equals(this.returnType);
this.returnsMany = configuration.getObjectFactory().isCollection(this.returnType) || this.returnType.isArray();
this.returnsCursor = Cursor.class.equals(this.returnType);
this.returnsOptional = Optional.class.equals(this.returnType);
this.mapKey = getMapKey(method);
this.returnsMap = this.mapKey != null;
this.rowBoundsIndex = getUniqueParamIndex(method, RowBounds.class);
this.resultHandlerIndex = getUniqueParamIndex(method, ResultHandler.class);
this.paramNameResolver = new ParamNameResolver(configuration, method);
}
}
復制代碼
~~~
MethodSignature是通過反射獲取其方法描述信息,也就是簽名,代碼太多,僅通過其字段和構造方法看下吧。MethodSignature主要的邏輯是獲取其返回值類型,比如單個、多個、Map等。
對SqlCommand和MethodSignature兩個內部類有一定的認識后,我們繼續看下MapperMethod#execute方法。execute方法會根據SqlCommandType處理不同的命令:
* INSERT、UPDATE、DELETE的處理方式應該說是基本相同,參數轉換后調用對應的SqlSession方法去執行;
* 對于SELECT命令,需要根據返回值特點,轉換參數、處理查詢范圍等,然后調用SqlSession不同的查詢方法;
~~~java
public class MapperMethod {
//...
//方法執行邏輯
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
//判斷命令類型:間接從MappedStatement獲取的
switch (command.getType()) {
//insert數據
//INSERT、UPDATE、DELETE的處理方式一毛一樣的;
//1、參數處理:把參數列表轉為Map結構,同時會復制一份參數按照param0、param1這樣put到Map
//2、調用SqlSession對應的方法執行命令,command.getName()就是我們示例中的com.raysonxin.dao.CompanyDao.selectById
case INSERT: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.insert(command.getName(), param));
break;
}
//更新數據
case UPDATE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.update(command.getName(), param));
break;
}
//刪除數據
case DELETE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.delete(command.getName(), param));
break;
}
//查詢命令:會根據返回值的類型調用不同的處理方法,進而路由到SqlSession不同的查詢方法,在此之前需要做一些準備工作:
//selectOne、selectList、selectCursor等
case SELECT:
if (method.returnsVoid() && method.hasResultHandler()) {
executeWithResultHandler(sqlSession, args);
result = null;
} else if (method.returnsMany()) {
result = executeForMany(sqlSession, args);
} else if (method.returnsMap()) {
result = executeForMap(sqlSession, args);
} else if (method.returnsCursor()) {
result = executeForCursor(sqlSession, args);
} else {
Object param = method.convertArgsToSqlCommandParam(args);
result = sqlSession.selectOne(command.getName(), param);
if (method.returnsOptional() &&
(result == null || !method.getReturnType().equals(result.getClass()))) {
result = Optional.ofNullable(result);
}
}
break;
case FLUSH:
result = sqlSession.flushStatements();
break;
default:
throw new BindingException("Unknown execution method for: " + command.getName());
}
if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
throw new BindingException("Mapper method '" + command.getName()
+ " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
}
return result;
}
//...
}
復制代碼
~~~
MapperMethod#execute最終調用了SqlSession接口的方法,在我們的示例中會調用SqlSession#selectOne。

走到這里是不是有點懵了,怎么調來調去,繞了一圈又回到SqlSession了?為啥不直接調用SqlSession接口呢?這里插一句:*直接使用SqlSession確實是可以,而且也是早起Mybatis所支持的方式;但是它不符合面向對象語言的概念和面向接口編程的編程習慣。由于面向接口的編程是面向對象的大趨勢,MyBatis?為了適應這一趨勢,增加了第二種使用MyBatis?支持接口(Interface)調用方式,即Mapper接口。*
*
這一段過程的主要工作就是:**采用動態代理方法,根據我們所調用的接口和方法,結合Configuration所加載的配置信息,動態的把請求路由到SqlSession對應的接口上來。**
*\_
## Sql命令執行
繞了一圈回到SqlSession后,才算是真正開始sql命令語句的執行。這個過程也是很復雜的,需要依次經過SqlSession、Executor、StatementHandler、ParamterHandler、ResultSetHandler等,流程較長,不能面面俱到,我們隨著代碼走一遍流程,然后通過后續的文章逐個補齊細節。先看個大體的流程圖:

### SqlSession
按照示例,DefaultSqlSession為具體實現,從DefaultSqlSession#selectOne()開始。
~~~java
@Override
public <T> T selectOne(String statement, Object parameter) {
// Popular vote was to return null on 0 results and throw exception on too many.
//調用selectList方法,獲取返回結果list
List<T> list = this.selectList(statement, parameter);
//如果結果剛好是1個,返回;若為多個,拋出異常;若為空,返回null;
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;
}
}
@Override
public <E> List<E> selectList(String statement, Object parameter) {
//中轉一下,RowBounds給了默認值,不設限。
return this.selectList(statement, parameter, RowBounds.DEFAULT);
}
@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
//從configuration獲取MappedStatement,
//示例中statement=com.raysonxin.dao.CompanyDao.selectById
MappedStatement ms = configuration.getMappedStatement(statement);
//通過執行器Executor執行查詢
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代碼走下來,從selectOne走到selectList,在selectList中調用了Executor#query方法。根據Executor的原理,當前executor為CachingExecutor實例,并且SimpleExecutor為其代理執行器(如下圖),所以接下來進入Executor執行器的流程。

### Executor
CachingExecutor是Executor的緩存設計,若緩存中存在所要查詢的內容會直接返回,否則會交給其代理執行器通過數據庫進行查詢。從CachingExecutor#query(MappedStatement, Object, RowBounds, ResultHandler)開始:
~~~java
//org.apache.ibatis.executor.CachingExecutor#query(org.apache.ibatis.mapping.MappedStatement, java.lang.Object, org.apache.ibatis.session.RowBounds, org.apache.ibatis.session.ResultHandler)
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
//獲取BoundSql
BoundSql boundSql = ms.getBoundSql(parameterObject);
//獲取緩存key
CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
//調用重載方法
return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
//org.apache.ibatis.executor.CachingExecutor#query(org.apache.ibatis.mapping.MappedStatement, java.lang.Object, org.apache.ibatis.session.RowBounds, org.apache.ibatis.session.ResultHandler, org.apache.ibatis.cache.CacheKey, org.apache.ibatis.mapping.BoundSql)
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
//查詢緩存
Cache cache = ms.getCache();
//緩存存在
if (cache != null) {
//如果需要刷新緩存
flushCacheIfRequired(ms);
//如果啟用緩存
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, boundSql);
@SuppressWarnings("unchecked")
List<E> list = (List<E>) tcm.getObject(cache, key);
//數據為空還是要調用委托執行器的query
if (list == null) {
list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
//緩存不存在時,調用委托執行器的query
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
復制代碼
~~~
我們看到如果未啟用或者未命中緩存的情況,CachingExecutor會通過其委托執行器繼續查詢,也就是SimpleExecutor#query。SimpleExecutor繼承自BaseExecutor,在BaseExecutor也同樣具有緩存的處理邏輯,我們暫時跳過,僅貼出主要代碼:
~~~java
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
//省略。。。
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--;
}
//....
}
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,這個是抽象方法,具體實現在SimpleExecutor
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;
}
@Override
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
Statement stmt = null;
try {
//獲取configuration
Configuration configuration = ms.getConfiguration();
//創建StatementHandler,默認為PreparedStatementHandler
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
//創建并初始化statement
stmt = prepareStatement(handler, ms.getStatementLog());
//由StatementHandler執行查詢,使用ResultSetHandler處理查詢結果
return handler.query(stmt, resultHandler);
} finally {
closeStatement(stmt);
}
}
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
Statement stmt;
Connection connection = getConnection(statementLog);
//創建Statement,BaseStatementHandler#prepare
stmt = handler.prepare(connection, transaction.getTimeout());
//設置參數,使用ParameterHandler
handler.parameterize(stmt);
return stmt;
}
復制代碼
~~~
Executor過程的執行就到這里了,CachingExecutor->BaseExecutor->SimpleExecutor,并且完成了StatementHandler執行的準備工作。CachingExecutor充當了緩存一層,當未命中緩存時會執行數據庫查詢,此時交給BaseExecutor及其子類執行。我們通過doQuery方法看到了StatementHandler創建、參數設置、執行與關閉整個生命周期,可以說Executor是StatementHandler的調度者。
從SimpleExecutor#doQuery、SimpleExecutor#prepareStatement方法看,可以把StatementHandler的工作分為幾個過程:創建Statement對象、設置Statement參數、執行Statement查詢并返回結果、關閉Statement對象。其中“設置Statement參數”需要使用ParameterHandler進行參數設置、“執行Statement查詢并返回結果”需要使用ResultSetHandler處理查詢結果。接下來重點看下參數設置、查詢、結果處理三個過程。
### ParameterHandler
prepareStatement方法中,調用了StatementHandler#parameterize進行參數設置,我們看下PreparedStatementHandler的實現。
~~~java
@Override
public void parameterize(Statement statement) throws SQLException {
parameterHandler.setParameters((PreparedStatement) statement);
}
復制代碼
~~~
這里的parameterHandler是ParameterHandler實例。在mybatis中,ParameterHandler僅有一個默認實現,即:DefaultParameterHandler。所以,直接看DefaultParameterHandler#setParameters方法:
~~~java
@Override
public void setParameters(PreparedStatement ps) {
ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
//獲取參數映射列表,它來自SqlSource的解析過程,目前了解即可。
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
if (parameterMappings != null) {
//遍歷參數映射
for (int i = 0; i < parameterMappings.size(); i++) {
//獲取當前參數映射對象
ParameterMapping parameterMapping = parameterMappings.get(i);
//如果參數模式不是OUT,即IN、INOUT時,繼續執行
if (parameterMapping.getMode() != ParameterMode.OUT) {
Object value;
//獲取參數名稱:示例中為:id
String propertyName = parameterMapping.getProperty();
if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params
value = boundSql.getAdditionalParameter(propertyName);
}
//參數值為空,
else if (parameterObject == null) {
value = null;
}
//類型處理器是否存在當前參數類型。如果只有一個參數,這里一般會命中。
//我們編寫sql語句時,沒有標明入參類型,mybatis會把參數類型解析為Object
else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
value = parameterObject;
}
//多個參數時,會走到這里。
else {
//轉為MetaObject類似于一個map
MetaObject metaObject = configuration.newMetaObject(parameterObject);
//獲取當前參數對應的值
value = metaObject.getValue(propertyName);
}
//獲取類型處理器
TypeHandler typeHandler = parameterMapping.getTypeHandler();
JdbcType jdbcType = parameterMapping.getJdbcType();
if (value == null && jdbcType == null) {
jdbcType = configuration.getJdbcTypeForNull();
}
try {
//通過類型處理器設置Statement參數。
typeHandler.setParameter(ps, i + 1, value, jdbcType);
} catch (TypeException e) {
throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
} catch (SQLException e) {
throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
}
}
}
}
}
復制代碼
~~~
setParameters設置參數的過程比較簡單:獲取參數映射列表后,依次遍歷參數映射對象,逐個參數類型獲取類型處理器,然后使用類型處理器設置Statement參數。當然,這里面會設置到類型處理器的查詢中轉操作(UnKnown->Integer),大家可以調試看下過程。
### StatementHandler
參數設置完成后,就到了執行操作了。示例中將會走到PreparedStatementHandler#query,如下:
~~~java
@Override
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
//轉為PreparedStatement類型
PreparedStatement ps = (PreparedStatement) statement;
//執行
ps.execute();
//處理查詢結果
return resultSetHandler.handleResultSets(ps);
}
復制代碼
~~~
呃,這個太簡潔了。把statement類型轉為PreparedStatement,執行execute,處理查詢結果并返回。Statement相關的內容屬于傳統JDBC范疇了,大家比較熟悉,不再贅述。
### ResultSetHandler
這應該是查詢過程中的最后一步了!由PreparedStatementHandler#query方法可知,執行java.sql.PreparedStatement#execute后,mybatis使用ResultSetHandler#handleResultSets處理查詢結果。mybatis對ResultSetHandler也僅有一個默認實現DefaultResultSetHandler,直接看代碼吧:
~~~java
@Override
public List<Object> handleResultSets(Statement stmt) throws SQLException {
ErrorContext.instance().activity("handling results").object(mappedStatement.getId());
final List<Object> multipleResults = new ArrayList<>();
int resultSetCount = 0;
//獲取第一個結果集
ResultSetWrapper rsw = getFirstResultSet(stmt);
//獲取結果映射
List<ResultMap> resultMaps = mappedStatement.getResultMaps();
int resultMapCount = resultMaps.size();
//如果rsw為空,或者resultMapCount<1,會拋出異常
validateResultMapsCount(rsw, resultMapCount);
while (rsw != null && resultMapCount > resultSetCount) {
//獲取結果集映射對象
ResultMap resultMap = resultMaps.get(resultSetCount);
//獲取結果,完成查詢結果映射
handleResultSet(rsw, resultMap, multipleResults, null);
//處理下一個,多個結果時有用
rsw = getNextResultSet(stmt);
cleanUpAfterHandlingResultSet();
resultSetCount++;
}
//獲取配置的結果集信息,我們沒有使用,不用管了。
String[] resultSets = mappedStatement.getResultSets();
if (resultSets != null) {
while (rsw != null && resultSetCount < resultSets.length) {
ResultMapping parentMapping = nextResultMaps.get(resultSets[resultSetCount]);
if (parentMapping != null) {
String nestedResultMapId = parentMapping.getNestedResultMapId();
ResultMap resultMap = configuration.getResultMap(nestedResultMapId);
handleResultSet(rsw, resultMap, null, parentMapping);
}
rsw = getNextResultSet(stmt);
cleanUpAfterHandlingResultSet();
resultSetCount++;
}
}
return collapseSingleResultList(multipleResults);
}
復制代碼
~~~
總結一下這個階段的處理過程,看圖吧,一圖勝千言!

# 總結
本文從一個簡答的Mapper查詢方法開始,詳細分析了其執行過程。按照我的個人理解,把執行過程分為了三個階段:
* Mapper接口實例的動態實現階段:Mybatis基于動態代理,動態實現Mapper接口,其實一個“披著Mapper皮的MapperProxy對象”;
* Mapper接口命令解析階段:這個過程中基于接口、方法簽名,結合反射技術、Configuration配置信息,確認了對應的SqlSession方法入口。
* SQL命令執行階段:從SqlSession入口開始,依次經過Executor、StatementHandler、ParameterHandler、ResultSetHandler四大組件的層層處理,最終完成查詢過程。
作者:碼路印記
鏈接:https://juejin.cn/post/6879710920534884360
來源:掘金
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。
- 一.JVM
- 1.1 java代碼是怎么運行的
- 1.2 JVM的內存區域
- 1.3 JVM運行時內存
- 1.4 JVM內存分配策略
- 1.5 JVM類加載機制與對象的生命周期
- 1.6 常用的垃圾回收算法
- 1.7 JVM垃圾收集器
- 1.8 CMS垃圾收集器
- 1.9 G1垃圾收集器
- 2.面試相關文章
- 2.1 可能是把Java內存區域講得最清楚的一篇文章
- 2.0 GC調優參數
- 2.1GC排查系列
- 2.2 內存泄漏和內存溢出
- 2.2.3 深入理解JVM-hotspot虛擬機對象探秘
- 1.10 并發的可達性分析相關問題
- 二.Java集合架構
- 1.ArrayList深入源碼分析
- 2.Vector深入源碼分析
- 3.LinkedList深入源碼分析
- 4.HashMap深入源碼分析
- 5.ConcurrentHashMap深入源碼分析
- 6.HashSet,LinkedHashSet 和 LinkedHashMap
- 7.容器中的設計模式
- 8.集合架構之面試指南
- 9.TreeSet和TreeMap
- 三.Java基礎
- 1.基礎概念
- 1.1 Java程序初始化的順序是怎么樣的
- 1.2 Java和C++的區別
- 1.3 反射
- 1.4 注解
- 1.5 泛型
- 1.6 字節與字符的區別以及訪問修飾符
- 1.7 深拷貝與淺拷貝
- 1.8 字符串常量池
- 2.面向對象
- 3.關鍵字
- 4.基本數據類型與運算
- 5.字符串與數組
- 6.異常處理
- 7.Object 通用方法
- 8.Java8
- 8.1 Java 8 Tutorial
- 8.2 Java 8 數據流(Stream)
- 8.3 Java 8 并發教程:線程和執行器
- 8.4 Java 8 并發教程:同步和鎖
- 8.5 Java 8 并發教程:原子變量和 ConcurrentMap
- 8.6 Java 8 API 示例:字符串、數值、算術和文件
- 8.7 在 Java 8 中避免 Null 檢查
- 8.8 使用 Intellij IDEA 解決 Java 8 的數據流問題
- 四.Java 并發編程
- 1.線程的實現/創建
- 2.線程生命周期/狀態轉換
- 3.線程池
- 4.線程中的協作、中斷
- 5.Java鎖
- 5.1 樂觀鎖、悲觀鎖和自旋鎖
- 5.2 Synchronized
- 5.3 ReentrantLock
- 5.4 公平鎖和非公平鎖
- 5.3.1 說說ReentrantLock的實現原理,以及ReentrantLock的核心源碼是如何實現的?
- 5.5 鎖優化和升級
- 6.多線程的上下文切換
- 7.死鎖的產生和解決
- 8.J.U.C(java.util.concurrent)
- 0.簡化版(快速復習用)
- 9.鎖優化
- 10.Java 內存模型(JMM)
- 11.ThreadLocal詳解
- 12 CAS
- 13.AQS
- 0.ArrayBlockingQueue和LinkedBlockingQueue的實現原理
- 1.DelayQueue的實現原理
- 14.Thread.join()實現原理
- 15.PriorityQueue 的特性和原理
- 16.CyclicBarrier的實際使用場景
- 五.Java I/O NIO
- 1.I/O模型簡述
- 2.Java NIO之緩沖區
- 3.JAVA NIO之文件通道
- 4.Java NIO之套接字通道
- 5.Java NIO之選擇器
- 6.基于 Java NIO 實現簡單的 HTTP 服務器
- 7.BIO-NIO-AIO
- 8.netty(一)
- 9.NIO面試題
- 六.Java設計模式
- 1.單例模式
- 2.策略模式
- 3.模板方法
- 4.適配器模式
- 5.簡單工廠
- 6.門面模式
- 7.代理模式
- 七.數據結構和算法
- 1.什么是紅黑樹
- 2.二叉樹
- 2.1 二叉樹的前序、中序、后序遍歷
- 3.排序算法匯總
- 4.java實現鏈表及鏈表的重用操作
- 4.1算法題-鏈表反轉
- 5.圖的概述
- 6.常見的幾道字符串算法題
- 7.幾道常見的鏈表算法題
- 8.leetcode常見算法題1
- 9.LRU緩存策略
- 10.二進制及位運算
- 10.1.二進制和十進制轉換
- 10.2.位運算
- 11.常見鏈表算法題
- 12.算法好文推薦
- 13.跳表
- 八.Spring 全家桶
- 1.Spring IOC
- 2.Spring AOP
- 3.Spring 事務管理
- 4.SpringMVC 運行流程和手動實現
- 0.Spring 核心技術
- 5.spring如何解決循環依賴問題
- 6.springboot自動裝配原理
- 7.Spring中的循環依賴解決機制中,為什么要三級緩存,用二級緩存不夠嗎
- 8.beanFactory和factoryBean有什么區別
- 九.數據庫
- 1.mybatis
- 1.1 MyBatis-# 與 $ 區別以及 sql 預編譯
- Mybatis系列1-Configuration
- Mybatis系列2-SQL執行過程
- Mybatis系列3-之SqlSession
- Mybatis系列4-之Executor
- Mybatis系列5-StatementHandler
- Mybatis系列6-MappedStatement
- Mybatis系列7-參數設置揭秘(ParameterHandler)
- Mybatis系列8-緩存機制
- 2.淺談聚簇索引和非聚簇索引的區別
- 3.mysql 證明為什么用limit時,offset很大會影響性能
- 4.MySQL中的索引
- 5.數據庫索引2
- 6.面試題收集
- 7.MySQL行鎖、表鎖、間隙鎖詳解
- 8.數據庫MVCC詳解
- 9.一條SQL查詢語句是如何執行的
- 10.MySQL 的 crash-safe 原理解析
- 11.MySQL 性能優化神器 Explain 使用分析
- 12.mysql中,一條update語句執行的過程是怎么樣的?期間用到了mysql的哪些log,分別有什么作用
- 十.Redis
- 0.快速復習回顧Redis
- 1.通俗易懂的Redis數據結構基礎教程
- 2.分布式鎖(一)
- 3.分布式鎖(二)
- 4.延時隊列
- 5.位圖Bitmaps
- 6.Bitmaps(位圖)的使用
- 7.Scan
- 8.redis緩存雪崩、緩存擊穿、緩存穿透
- 9.Redis為什么是單線程、及高并發快的3大原因詳解
- 10.布隆過濾器你值得擁有的開發利器
- 11.Redis哨兵、復制、集群的設計原理與區別
- 12.redis的IO多路復用
- 13.相關redis面試題
- 14.redis集群
- 十一.中間件
- 1.RabbitMQ
- 1.1 RabbitMQ實戰,hello world
- 1.2 RabbitMQ 實戰,工作隊列
- 1.3 RabbitMQ 實戰, 發布訂閱
- 1.4 RabbitMQ 實戰,路由
- 1.5 RabbitMQ 實戰,主題
- 1.6 Spring AMQP 的 AMQP 抽象
- 1.7 Spring AMQP 實戰 – 整合 RabbitMQ 發送郵件
- 1.8 RabbitMQ 的消息持久化與 Spring AMQP 的實現剖析
- 1.9 RabbitMQ必備核心知識
- 2.RocketMQ 的幾個簡單問題與答案
- 2.Kafka
- 2.1 kafka 基礎概念和術語
- 2.2 Kafka的重平衡(Rebalance)
- 2.3.kafka日志機制
- 2.4 kafka是pull還是push的方式傳遞消息的?
- 2.5 Kafka的數據處理流程
- 2.6 Kafka的腦裂預防和處理機制
- 2.7 Kafka中partition副本的Leader選舉機制
- 2.8 如果Leader掛了的時候,follower沒來得及同步,是否會出現數據不一致
- 2.9 kafka的partition副本是否會出現腦裂情況
- 十二.Zookeeper
- 0.什么是Zookeeper(漫畫)
- 1.使用docker安裝Zookeeper偽集群
- 3.ZooKeeper-Plus
- 4.zk實現分布式鎖
- 5.ZooKeeper之Watcher機制
- 6.Zookeeper之選舉及數據一致性
- 十三.計算機網絡
- 1.進制轉換:二進制、八進制、十六進制、十進制之間的轉換
- 2.位運算
- 3.計算機網絡面試題匯總1
- 十四.Docker
- 100.面試題收集合集
- 1.美團面試常見問題總結
- 2.b站部分面試題
- 3.比心面試題
- 4.騰訊面試題
- 5.哈羅部分面試
- 6.筆記
- 十五.Storm
- 1.Storm和流處理簡介
- 2.Storm 核心概念詳解
- 3.Storm 單機版本環境搭建
- 4.Storm 集群環境搭建
- 5.Storm 編程模型詳解
- 6.Storm 項目三種打包方式對比分析
- 7.Storm 集成 Redis 詳解
- 8.Storm 集成 HDFS 和 HBase
- 9.Storm 集成 Kafka
- 十六.Elasticsearch
- 1.初識ElasticSearch
- 2.文檔基本CRUD、集群健康檢查
- 3.shard&replica
- 4.document核心元數據解析及ES的并發控制
- 5.document的批量操作及數據路由原理
- 6.倒排索引
- 十七.分布式相關
- 1.分布式事務解決方案一網打盡
- 2.關于xxx怎么保證高可用的問題
- 3.一致性hash原理與實現
- 4.微服務注冊中心 Nacos 比 Eureka的優勢
- 5.Raft 協議算法
- 6.為什么微服務架構中需要網關
- 0.CAP與BASE理論
- 十八.Dubbo
- 1.快速掌握Dubbo常規應用
- 2.Dubbo應用進階
- 3.Dubbo調用模塊詳解
- 4.Dubbo調用模塊源碼分析
- 6.Dubbo協議模塊