該系列檔案是本人在學習 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 物件以及延遲加載等等處理程序,這里將一步一步地進行分析:
- 《SQL執行程序(一)之Executor》
- 《SQL執行程序(二)之StatementHandler》
- 《SQL執行程序(三)之ResultSetHandler》
- 《SQL執行程序(四)之延遲加載》
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 = https://www.cnblogs.com/lifullmoon/archive/2020/11/26/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 = https://www.cnblogs.com/lifullmoon/archive/2020/11/26/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 = https://www.cnblogs.com/lifullmoon/archive/2020/11/26/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 原始碼分析》
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/227800.html
標籤:其他
上一篇:Java基礎之:猜拳小程式
