該系列文件是本人在學習 Mybatis 的原始碼過程中總結下來的,可能對讀者不太友好,請結合我的原始碼註釋(Mybatis原始碼分析 GitHub 地址、Mybatis-Spring 原始碼分析 GitHub 地址、Spring-Boot-Starter 原始碼分析 GitHub 地址)進行閱讀
MyBatis 版本:3.5.2
MyBatis-Spring 版本:2.0.3
MyBatis-Spring-Boot-Starter 版本:2.1.4
MyBatis的SQL執行過程
在前面一系列的文件中,我已經分析了 MyBatis 的基礎支援層以及整個的初始化過程,此時 MyBatis 已經處於就緒狀態了,等待使用者發號施令了
那麼接下來我們來看看它執行SQL的整個過程,該過程比較複雜,涉及到二級快取,將返回結果轉換成 Java 物件以及延遲載入等等處理過程,這裡將一步一步地進行分析:
MyBatis中SQL執行的整體過程如下圖所示:
在 SqlSession 中,會將執行 SQL 的過程交由Executor
執行器去執行,過程大致如下:
- 通過
DefaultSqlSessionFactory
建立與資料庫互動的SqlSession
“會話”,其內部會建立一個Executor
執行器物件 - 然後
Executor
執行器通過StatementHandler
建立對應的java.sql.Statement
物件,並通過ParameterHandler
設定引數,然後執行資料庫相關操作 - 如果是資料庫更新操作,則可能需要通過
KeyGenerator
先設定自增鍵,然後返回受影響的行數 - 如果是資料庫查詢操作,則需要將資料庫返回的
ResultSet
結果集物件包裝成ResultSetWrapper
,然後通過DefaultResultSetHandler
對結果集進行對映,最後返回 Java 物件
上面還涉及到一級快取、二級快取和延遲載入等其他處理過程
SQL執行過程(四)之延遲載入
在前面SQL執行過程一系列的文件中,已經詳細地分析了在 MyBatis 的SQL執行過程中,SqlSession 會話將資料庫相關操作交由 Executor 執行器去完成,通過 StatementHandler 去執行資料庫的操作,並獲取到資料庫的執行結果,如果是查詢結果則通過 DefaultResultSetHandler 對結果集進行對映,轉換成 Java 物件
其中 MyBatis 也提供了延遲載入的功能,當呼叫實體類需要延遲載入的屬性的 getter 方法時,才會觸發其對應的子查詢,獲取到查詢結果,設定該物件的屬性值
在上一篇《SQL執行過程(三)之ResultSetHandler》文件中講到
-
如果存在巢狀子查詢且需要延遲載入,則會通過
ProxyFactory
動態代理工廠,為返回結果的例項物件建立一個動態代理物件(Javassist),也就是說返回結果實際上是一個動態代理物件可以回到上一篇文件的4.2.1createResultObject方法小節第
4
步看看resultObject = configuration.getProxyFactory().createProxy(resultObject, lazyLoader, configuration, objectFactory, constructorArgTypes, constructorArgs;
-
後續屬性對映的過程中,如果該屬性是巢狀子查詢並且需要延遲載入,則會建立一個
ResultLoader
物件新增到上面的ResultLoaderMap
物件lazyLoader
中可以回到上一篇文件的4.2.4.2getNestedQueryMappingValue方法小節第
6
步看看final ResultLoader resultLoader = new ResultLoader(configuration, executor, nestedQuery, nestedQueryParameterObject, targetType, key, nestedBoundSql); if (propertyMapping.isLazy()) { // <6.2> 如果要求延遲載入,則延遲載入 // <6.2.1> 如果該屬性配置了延遲載入,則將其新增到 `ResultLoader.loaderMap` 中,等待真正使用時再執行巢狀查詢並得到結果物件 lazyLoader.addLoader(property, metaResultObject, resultLoader); // <6.2.2> 返回延遲載入佔位符 value = DEFERRED; } else { // <6.3> 如果不要求延遲載入,則直接執行載入對應的值 value = resultLoader.loadResult(); }
那麼接下來我們來看看 MyBatis 中的延遲載入是如何實現的
ResultLoader
org.apache.ibatis.executor.loader.ResultLoader
:延遲載入的載入器,在上面你可以看到需要延遲載入的屬性會被封裝成該物件
構造方法
public class ResultLoader {
/**
* 全域性配置物件
*/
protected final Configuration configuration;
/**
* 執行器
*/
protected final Executor executor;
/**
* MappedStatement 查詢物件
*/
protected final MappedStatement mappedStatement;
/**
* 查詢的引數物件
*/
protected final Object parameterObject;
/**
* 目標的型別,返回結果的 Java Type
*/
protected final Class<?> targetType;
/**
* 例項工廠
*/
protected final ObjectFactory objectFactory;
protected final CacheKey cacheKey;
/**
* SQL 相關資訊
*/
protected final BoundSql boundSql;
/**
* 結果抽取器
*/
protected final ResultExtractor resultExtractor;
/**
* 建立 ResultLoader 物件時,所在的執行緒的 id
*/
protected final long creatorThreadId;
/**
* 是否已經載入
*/
protected boolean loaded;
/**
* 查詢的結果物件
*/
protected Object resultObject;
public ResultLoader(Configuration config, Executor executor, MappedStatement mappedStatement,
Object parameterObject, Class<?> targetType, CacheKey cacheKey, BoundSql boundSql) {
this.configuration = config;
this.executor = executor;
this.mappedStatement = mappedStatement;
this.parameterObject = parameterObject;
this.targetType = targetType;
this.objectFactory = configuration.getObjectFactory();
this.cacheKey = cacheKey;
this.boundSql = boundSql;
this.resultExtractor = new ResultExtractor(configuration, objectFactory);
this.creatorThreadId = Thread.currentThread().getId();
}
}
主要包含以下資訊:
executor
:執行器mappedStatement
:查詢語句的MappedStatement物件parameterObject
:子查詢的入參targetType
:返回結果的Java TypeboundSql
:SQL相關資訊resultExtractor
:查詢結果的抽取器loaded
:是否已經載入
loadResult方法
loadResult()
方法,延遲載入的執行器的執行方法,獲取到查詢結果,並提取出結果,方法如下:
public Object loadResult() throws SQLException {
// <1> 查詢結果
List<Object> list = selectList();
// <2> 提取結果
resultObject = resultExtractor.extractObjectFromList(list, targetType);
// <3> 返回結果
return resultObject;
}
selectList方法
selectList()
方法,執行延遲載入對應的子查詢,獲取到查詢結果,方法如下:
private <E> List<E> selectList() throws SQLException {
// <1> 獲得 Executor 物件
Executor localExecutor = executor;
if (Thread.currentThread().getId() != this.creatorThreadId || localExecutor.isClosed()) {
// 建立一個的 Executor 物件,保證執行緒安全
localExecutor = newExecutor();
}
try {
// <2> 執行查詢
return localExecutor.query(mappedStatement, parameterObject, RowBounds.DEFAULT,
Executor.NO_RESULT_HANDLER, cacheKey, boundSql);
} finally {
// <3> 關閉 Executor 物件
if (localExecutor != executor) {
localExecutor.close(false);
}
}
}
- 獲得 Executor 執行器,如果當前執行緒不是建立 ResultLoader 物件時所在的執行緒的,或者這個執行器被關閉了,那麼需要呼叫
newExecutor()
方法建立一個新的執行器 - 通過該執行器進行資料的查詢,並返回查詢結果
- 如果這個執行器是新建立的,則需要關閉它
newExecutor方法
newExecutor()
方法,建立一個新的Executor執行器用於執行延遲載入的子查詢,執行完後需要關閉,方法如下:
private Executor newExecutor() {
// 校驗 environment
final Environment environment = configuration.getEnvironment();
if (environment == null) {
throw new ExecutorException("ResultLoader could not load lazily. Environment was not configured.");
}
// 校驗 DataSource
final DataSource ds = environment.getDataSource();
if (ds == null) {
throw new ExecutorException("ResultLoader could not load lazily. DataSource was not configured.");
}
// 建立 Transaction 物件
final TransactionFactory transactionFactory = environment.getTransactionFactory();
final Transaction tx = transactionFactory.newTransaction(ds, null, false);
// 建立 Executor 物件
return configuration.newExecutor(tx, ExecutorType.SIMPLE);
}
ResultExtractor
org.apache.ibatis.executor.ResultExtractor
:結果提取器,用於提取延遲載入對應的子查詢的查詢結果,轉換成Java物件,程式碼如下:
public class ResultExtractor {
/**
* 全域性配置物件
*/
private final Configuration configuration;
/**
* 例項工廠
*/
private final ObjectFactory objectFactory;
public ResultExtractor(Configuration configuration, ObjectFactory objectFactory) {
this.configuration = configuration;
this.objectFactory = objectFactory;
}
/**
* 從 list 中,提取結果
*
* @param list list
* @param targetType 結果型別
* @return 結果
*/
public Object extractObjectFromList(List<Object> list, Class<?> targetType) {
Object value = null;
/*
* 從查詢結果中抽取資料轉換成目標型別
*/
if (targetType != null && targetType.isAssignableFrom(list.getClass())) { // <1> 場景1,List 型別
// 直接返回
value = list;
} else if (targetType != null && objectFactory.isCollection(targetType)) { // <2> 場景2,集合型別
// <2.1> 建立集合的例項物件
value = objectFactory.create(targetType);
// <2.2> 將結果新增到其中
MetaObject metaObject = configuration.newMetaObject(value);
// <2.3> 將查詢結果全部新增到集合物件中
metaObject.addAll(list);
} else if (targetType != null && targetType.isArray()) { // <3> 場景3,陣列型別
// <3.1> 獲取陣列的成員型別
Class<?> arrayComponentType = targetType.getComponentType();
// <3.2> 建立陣列物件,並設定大小
Object array = Array.newInstance(arrayComponentType, list.size());
if (arrayComponentType.isPrimitive()) { // <3.3> 如果是基本型別
for (int i = 0; i < list.size(); i++) {
// 一個一個新增到陣列中
Array.set(array, i, list.get(i));
}
value = array;
} else {
// <3.4> 將 List 轉換成 Array
value = list.toArray((Object[]) array);
}
} else { // <4> 場景4
if (list != null && list.size() > 1) {
throw new ExecutorException("Statement returned more than one row, where no more than one was expected.");
} else if (list != null && list.size() == 1) {
// 取首個結果
value = list.get(0);
}
}
return value;
}
}
從List<Object> list
查詢結果提取資料,轉換成目標型別,有以下四種場景:
-
List型別,則直接返回
-
集合型別,則為該集合型別建立一個例項物件,並把
list
全部新增到該物件中,然後返回 -
陣列型別
- 獲取陣列的成員型別
- 建立陣列物件,並設定大小
- 如果是基本型別則一個一個新增到陣列中,否則直接將
list
轉換成陣列,然後返回
-
其他型別,也就是一個實體類了,直接獲取
list
中的第一個元素返回(如果list
集合的個數大於1則丟擲異常)
ResultLoaderMap
org.apache.ibatis.executor.loader.ResultLoaderMap
:用於儲存某個物件中所有的延遲載入
構造方法
public class ResultLoaderMap {
/**
* 用於延遲載入的載入器
* key:屬性名稱
* value:ResultLoader 載入器的封裝物件 LoadPair
*/
private final Map<String, LoadPair> loaderMap = new HashMap<>();
}
addLoader方法
addLoader(String property, MetaObject metaResultObject, ResultLoader resultLoader)
方法,用於新增一個需要延遲載入屬性
入參分別表示:需要延遲載入的屬性名稱、該屬性所在的Java物件(也就是查詢返回的結果物件)、延遲載入對應的載入器,方法如下:
public void addLoader(String property, MetaObject metaResultObject, ResultLoader resultLoader) {
// 獲取第一個屬性名稱
String upperFirst = getUppercaseFirstProperty(property);
if (!upperFirst.equalsIgnoreCase(property) && loaderMap.containsKey(upperFirst)) {
throw new ExecutorException("省略...");
}
loaderMap.put(upperFirst, new LoadPair(property, metaResultObject, resultLoader));
}
-
如果
property
屬性名稱包含.
點,且最前面一部分已經有對應的延遲載入物件了,則出現重複新增,需要丟擲異常 -
將入參資訊封裝成
LoadPair
物件,並放入loaderMap
中關於
LoadPair
,是ResultLoaderMap的一個內部類,裡面有對序列化進行處理,最後還是呼叫ResultLoader
的load()
方法,這裡就不列出來了
load方法
load(String property)
方法,用於觸發該屬性的延遲載入,方法如下:
public boolean load(String property) throws SQLException {
LoadPair pair = loaderMap.remove(property.toUpperCase(Locale.ENGLISH));
if (pair != null) {
pair.load();
return true;
}
return false;
}
- 先將該屬性對應的延遲載入從
loaderMap
集合中刪除 - 然後呼叫
LoadPair
的load()
方法,觸發延遲載入,並設定查詢結果設定到物件的屬性中
loadAll方法
loadAll()
方法,用於觸發所有還沒載入的延遲載入,方法如下:
public void loadAll() throws SQLException {
final Set<String> methodNameSet = loaderMap.keySet();
String[] methodNames = methodNameSet.toArray(new String[methodNameSet.size()]);
for (String methodName : methodNames) {
load(methodName);
}
}
ProxyFactory
org.apache.ibatis.executor.loader.ProxyFactory
:動態代理工廠介面
public interface ProxyFactory {
default void setProperties(Properties properties) {
// NOP
}
Object createProxy(Object target, ResultLoaderMap lazyLoader, Configuration configuration,
ObjectFactory objectFactory, List<Class<?>> constructorArgTypes,
List<Object> constructorArgs);
}
- 就定義了一個
createProxy
建立動態代理物件的方法,交由不同的子類去實現
實現類如下圖所示:
回到Configuration全域性配置物件中,你會發現預設使用的是JavassistProxyFactory
實現類
// Configuration.java
protected ProxyFactory proxyFactory = new JavassistProxyFactory();
JavassistProxyFactory
org.apache.ibatis.executor.loader.javassist.JavassistProxyFactory
:實現ProxyFactory介面,基於javassist
(一個開源的分析、編輯和建立Java位元組碼的類庫)建立動態代理物件
構造方法
public class JavassistProxyFactory implements org.apache.ibatis.executor.loader.ProxyFactory {
private static final String FINALIZE_METHOD = "finalize";
private static final String WRITE_REPLACE_METHOD = "writeReplace";
public JavassistProxyFactory() {
try {
// 載入 javassist.util.proxy.ProxyFactory 類
Resources.classForName("javassist.util.proxy.ProxyFactory");
} catch (Throwable e) {
throw new IllegalStateException(
"Cannot enable lazy loading because Javassist is not available. Add Javassist to your classpath.",
e);
}
}
}
載入 javassist.util.proxy.ProxyFactory
類
createProxy方法
createProxy(Object target, ResultLoaderMap lazyLoader, Configuration configuration, ObjectFactory objectFactory, List<Class<?>> constructorArgTypes, List<Object> constructorArgs)
方法
建立動態代理物件的入口,方法如下:
@Override
public Object createProxy(Object target, ResultLoaderMap lazyLoader, Configuration configuration,
ObjectFactory objectFactory, List<Class<?>> constructorArgTypes, List<Object> constructorArgs) {
// <1> 建立動態代例項物件
return EnhancedResultObjectProxyImpl.createProxy(target, lazyLoader, configuration,
objectFactory, constructorArgTypes, constructorArgs);
}
內部直接呼叫EnhancedResultObjectProxyImpl
的createProxy
方法
crateProxy靜態方法
crateProxy(Class<?> type, MethodHandler callback, List<Class<?>> constructorArgTypes, List<Object> constructorArgs)
方法
用於建立一個動態代理的例項物件,並設定MethodHandler
方法增強器,方法如下:
static Object crateProxy(Class<?> type, MethodHandler callback, List<Class<?>> constructorArgTypes,
List<Object> constructorArgs) {
// <3.1> 建立 ProxyFactory 動態代理物件工廠
ProxyFactory enhancer = new ProxyFactory();
// <3.2> 設定父類,需要代理的類物件
enhancer.setSuperclass(type);
// <3.3> 和序列化相關
try {
// 獲取需要代理的類物件中的 writeReplace 方法
type.getDeclaredMethod(WRITE_REPLACE_METHOD);
// ObjectOutputStream will call writeReplace of objects returned by writeReplace
if (LogHolder.log.isDebugEnabled()) {
LogHolder.log.debug(WRITE_REPLACE_METHOD + " method was found on bean " + type + ", make sure it returns this");
}
} catch (NoSuchMethodException e) {
// 如果沒有 writeReplace 方法,則設定介面為 WriteReplaceInterface
enhancer.setInterfaces(new Class[] { WriteReplaceInterface.class });
} catch (SecurityException e) {
// nothing to do here
}
Object enhanced;
Class<?>[] typesArray = constructorArgTypes.toArray(new Class[constructorArgTypes.size()]);
Object[] valuesArray = constructorArgs.toArray(new Object[constructorArgs.size()]);
try {
// <3.4> 建立動態代理例項物件
enhanced = enhancer.create(typesArray, valuesArray);
} catch (Exception e) {
throw new ExecutorException("Error creating lazy proxy. Cause: " + e, e);
}
// <3.5> 設定動態代理例項物件的 MethodHandler 方法增強器
((Proxy) enhanced).setHandler(callback);
return enhanced;
}
- 建立 ProxyFactory 動態代理物件工廠
- 設定父類,需要代理的類物件
- 設定和序列化相關配置
- 建立動態代理例項物件,傳入代理類物件的構造方法的入參型別陣列和入引數組
- 設定動態代理例項物件的
MethodHandler
方法增強器
EnhancedResultObjectProxyImpl
JavassistProxyFactory的內部類,動態代理物件的MethodHandler
方法增強器
構造方法
private static class EnhancedResultObjectProxyImpl implements MethodHandler {
private final Class<?> type;
private final ResultLoaderMap lazyLoader;
/**
* 開啟時,任一方法的呼叫都會載入該物件的所有延遲載入屬性,預設false
*/
private final boolean aggressive;
private final Set<String> lazyLoadTriggerMethods;
private final ObjectFactory objectFactory;
private final List<Class<?>> constructorArgTypes;
private final List<Object> constructorArgs;
private EnhancedResultObjectProxyImpl(Class<?> type, ResultLoaderMap lazyLoader, Configuration configuration,
ObjectFactory objectFactory, List<Class<?>> constructorArgTypes, List<Object> constructorArgs) {
this.type = type;
this.lazyLoader = lazyLoader;
this.aggressive = configuration.isAggressiveLazyLoading();
this.lazyLoadTriggerMethods = configuration.getLazyLoadTriggerMethods();
this.objectFactory = objectFactory;
this.constructorArgTypes = constructorArgTypes;
this.constructorArgs = constructorArgs;
}
}
- 我們主要看到
ResultLoaderMap lazyLoader
屬性,裡面儲存了需要延遲載入的屬性和載入器
createProxy方法
createProxy(Object target, ResultLoaderMap lazyLoader, Configuration configuration, ObjectFactory objectFactory, List<Class<?>> constructorArgTypes, List<Object> constructorArgs)
方法
建立動態代理例項物件,方法如下:
public static Object createProxy(Object target, ResultLoaderMap lazyLoader, Configuration configuration,
ObjectFactory objectFactory, List<Class<?>> constructorArgTypes, List<Object> constructorArgs) {
final Class<?> type = target.getClass();
// <2> 建立方法的增強器
EnhancedResultObjectProxyImpl callback = new EnhancedResultObjectProxyImpl(type, lazyLoader, configuration,
objectFactory, constructorArgTypes, constructorArgs);
// <3> 建立動態代理例項物件,設定方法的增強器
Object enhanced = crateProxy(type, callback, constructorArgTypes, constructorArgs);
// <4> 將 target 的屬性值複製到 enhanced 動態代例項物件中
PropertyCopier.copyBeanProperties(type, target, enhanced);
return enhanced;
}
這個方法在JavassistProxyFactory
的createProxy
方法被呼叫,然後自己內部又呼叫JavassistProxyFactory
的靜態createProxy
方法,這裡我已經按序號標明瞭步驟
- 建立
EnhancedResultObjectProxyImpl
方法的增強器callback
- 建立動態代理例項物件,並設定方法的增強器為
callback
,呼叫的是上面的靜態createProxy
方法 - 將
target
的屬性值複製到enhanced
動態代例項物件中
invoke方法
javassist.util.proxy.MethodHandler
方法增強器的而實現方法,代理物件的方法都會進入這個方法
@Override
public Object invoke(Object enhanced, Method method, Method methodProxy, Object[] args) throws Throwable {
final String methodName = method.getName();
try {
synchronized (lazyLoader) {
// <1> 如果方法名為 writeReplace,和序列化相關
if (WRITE_REPLACE_METHOD.equals(methodName)) {
Object original;
if (constructorArgTypes.isEmpty()) {
original = objectFactory.create(type);
} else {
original = objectFactory.create(type, constructorArgTypes, constructorArgs);
}
// 從動態代理例項物件中複製屬性值到 original 中
PropertyCopier.copyBeanProperties(type, enhanced, original);
if (lazyLoader.size() > 0) {
return new JavassistSerialStateHolder(original, lazyLoader.getProperties(),
objectFactory,constructorArgTypes, constructorArgs);
} else {
return original;
}
} else { // <2> 載入延遲載入的屬性
if (lazyLoader.size() > 0 && !FINALIZE_METHOD.equals(methodName)) {
// <2.1> 如果開啟了任一方法的呼叫都會載入該物件的所有延遲載入屬性,或者是 "equals", "clone", "hashCode", "toString" 其中的某個方法
if (aggressive || lazyLoadTriggerMethods.contains(methodName)) {
// 載入所有延遲載入的屬性
lazyLoader.loadAll();
} else if (PropertyNamer.isSetter(methodName)) {
// <2.2> 如果為 setter 方法,從需要延遲載入屬性列表中移除
final String property = PropertyNamer.methodToProperty(methodName);
lazyLoader.remove(property);
} else if (PropertyNamer.isGetter(methodName)) {
// <2.3> 如果呼叫了 getter 方法,則執行延遲載入,從需要延遲載入屬性列表中移除
final String property = PropertyNamer.methodToProperty(methodName);
if (lazyLoader.hasLoader(property)) {
// 載入該屬性值
lazyLoader.load(property);
}
}
}
}
}
// <3> 繼續執行原方法
return methodProxy.invoke(enhanced, args);
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
先給ResultLoaderMap lazyLoader
新增synchronized
關鍵字,保證執行緒安全
-
如果加強的方法是
writeReplace
,則進行一些序列化相關的操作,暫不分析,其實是沒看懂~ -
如果
lazyLoader
中有延遲載入的屬性,並且加強的方法不是finalize
- 如果開啟了任一方法的呼叫都會載入該物件的所有延遲載入屬性,或者是
equals clone hashCode toString
其中的某個方法,則觸發所有的延遲載入 - 否則,如果是屬性的setter方法,則從
lazyLoader
中將該屬性的延遲載入刪除(如果存在),因為主動設定了這個屬性值,則需要取消該屬性的延遲載入 - 否則,如果是屬性的getter方法,則執行延遲載入(會將結果設定到該物件的這個屬性中),裡面也會從
lazyLoader
中將該屬性的延遲載入刪除
- 如果開啟了任一方法的呼叫都會載入該物件的所有延遲載入屬性,或者是
-
繼續執行原方法
到這裡,延遲載入已經實現了
CglibProxyFactory
org.apache.ibatis.executor.loader.cglib.CglibProxyFactory
:實現ProxyFactory介面,基於cglib
(一個強大的,高效能,高質量的Code生成類庫,它可以在執行期擴充套件Java類與實現Java介面)建立動態代理物件
實現方式和JavassistProxyFactory
類似,這裡就不進行分析了,感興趣的可以看一下
總結
本文分析了 MyBatis 中延遲載入的實現方法,在 DefaultResultSetHandler 對映結果集的過程中,如果返回物件有屬性是巢狀子查詢,且需要延遲載入,則通過JavassistProxyFactory
為返回結果建立一個動態代理物件,並設定MethodHandler
方法增強器為EnhancedResultObjectProxyImpl
物件
其中傳入ResultLoaderMap
物件,該物件儲存了這個結果物件中所有的ResultLoader
延遲載入
在EnhancedResultObjectProxyImpl
中攔截結果物件的方法,進行增強處理,通過ResultLoader
延遲載入器獲取到該屬性值,然後從ResultLoaderMap
中刪除,在你呼叫該屬性的getter方法時才載入資料,這樣就實現了延遲載入
好了,對於 MyBatis 的整個 SQL 執行過程我們已經全部分析完了,其中肯定有不對或者迷惑的地方,歡迎指正!!!感謝大家的閱讀!!!???
參考文章:芋道原始碼《精盡 MyBatis 原始碼分析》