文章目錄
- 懶加載
- 簡介
- 實作原理
- 快取
- 簡介
- 快取具體實作
- 二級快取
- 配置二級快取
- 二級快取實作
- 插件
- 簡介
- 實作原理
- 初始化
- 加載
- 呼叫
- 流式讀取
- 簡介
- 實作原理
- 標簽的id可以重復嗎
- 總結
懶加載
簡介
Mybatis在進行關聯查詢時,可以開啟懶加載的功能,懶加載避免了一開始就去加載關聯屬性,而是在需要時再通過關聯表來查詢關聯屬性,
開啟懶加載的配置
<settings>
<!--開啟懶加載-->
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="aggressiveLazyLoading" value="false"/>
</settings>
在resultMap標簽配置映射規則時,關聯查詢的子標簽中可以指定select和column屬性,select屬性代表延遲加載需要執行的statement的id,如果不在當前mapper檔案中,需要加上namespace,column屬性代表同select查詢關聯的欄位,
<!-- 配置延遲加載 -->
<association property="position" fetchType="lazy" column="position_id" select="com.stu.mapper.PositionMapper.selectByPrimaryKey" />
實作原理
Mybatis對查詢的結果集進行映射處理程序中,會讀取ResultSet中的每一行記錄然后呼叫getRowValue方法進行映射
private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap) throws SQLException {
final ResultLoaderMap lazyLoader = new ResultLoaderMap();
//根據resultMap的type屬性,實體化目標物件(并確認關聯屬性是否開啟懶加載,開啟則為當前物件創建代理物件)
Object rowValue = createResultObject(rsw, resultMap, lazyLoader, null);
if (rowValue != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
//對目標物件進行封裝得到metaObjcect,為后續的賦值操作做好準備
final MetaObject metaObject = configuration.newMetaObject(rowValue);
boolean foundValues = this.useConstructorMappings;//取得是否使用建構式初始化屬性值
if (shouldApplyAutomaticMappings(resultMap, false)) {//是否使用自動映射
//一般情況下 autoMappingBehavior默認值為PARTIAL,對未明確指定映射規則的欄位進行自動映射
foundValues = applyAutomaticMappings(rsw, resultMap, metaObject, null) || foundValues;
}
//映射resultMap中明確指定需要映射的列
foundValues = applyPropertyMappings(rsw, resultMap, metaObject, lazyLoader, null) || foundValues;
....
}
return rowValue;
}
映射程序中會先呼叫createResultObject方法根據ResultMap標簽中配置的type屬性指定的目標物件的類名,然后先通過反射實體化目標物件,接下來,會遍歷ResultMap物件中的所有ResultMapping(ResultMap標簽中的每一個子標簽,都會封裝成ResultMapping)物件,判斷關聯查詢的子標簽是否開啟了懶加載
private Object createResultObject(ResultSetWrapper rsw, ResultMap resultMap, ResultLoaderMap lazyLoader, String columnPrefix) throws SQLException {
...
//回傳實際的結果集物件(通過反射創建Type指定型別的物件)
Object resultObject = createResultObject(rsw, resultMap, constructorArgTypes, constructorArgs, columnPrefix);
if (resultObject != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
//獲得所有的ResultMapping
final List<ResultMapping> propertyMappings = resultMap.getPropertyResultMappings();
for (ResultMapping propertyMapping : propertyMappings) {
//這里是判斷association、collection子標簽是否開啟了懶加載
// issue gcode #109 && issue #149
//嵌套查詢id存在,并且開始懶加載
if (propertyMapping.getNestedQueryId() != null && propertyMapping.isLazy()) {
//這里創建了延遲加載的代理物件
resultObject = configuration.getProxyFactory().createProxy(resultObject, lazyLoader, configuration, objectFactory, constructorArgTypes, constructorArgs);
break;
}
}
}
this.useConstructorMappings = resultObject != null && !constructorArgTypes.isEmpty(); // set current mapping result
return resultObject;
}
如果開啟了懶加載就會通過Javassist或者Cglib為目標物件創建一個代理物件,并指定代理類的處理類EnhancedResultObjectProxyImpl,
static Object crateProxy(Class<?> type, MethodHandler callback, List<Class<?>> constructorArgTypes, List<Object> constructorArgs) {
ProxyFactory enhancer = new ProxyFactory();
//要生成代理物件的原生類
enhancer.setSuperclass(type);
...
try {
//創建帶引數的代理物件
enhanced = enhancer.create(typesArray, valuesArray);
} catch (Exception e) {
throw new ExecutorException("Error creating lazy proxy. Cause: " + e, e);
}
((Proxy) enhanced).setHandler(callback);//指定代理類的處理類
return enhanced;
}
當呼叫目標物件的指定方法時,就會被代理物件攔截,然后執行到EnhancedResultObjectProxyImpl該處理類invoke方法,其中PropertyNamer. isProperty(methodName)這段代碼,它會判斷呼叫的方法是不是get,is型別的方法或者lazyLoadTriggerMethods集合中指定的方法,如果是的話,就會觸發懶加載,
private static class EnhancedResultObjectProxyImpl implements MethodHandler {
....
@Override
public Object invoke(Object enhanced, Method method, Method methodProxy, Object[] args) throws Throwable {
final String methodName = method.getName();
try {
synchronized (lazyLoader) {
if (WRITE_REPLACE_METHOD.equals(methodName)) {
.....
} else {
if (lazyLoader.size() > 0 && !FINALIZE_METHOD.equals(methodName)) {
if (aggressive || lazyLoadTriggerMethods.contains(methodName)) {
//全部加載
lazyLoader.loadAll();
//判斷是否為set方法,set方法不需要延遲加載
} else if (PropertyNamer.isSetter(methodName)) {
final String property = PropertyNamer.methodToProperty(methodName);
lazyLoader.remove(property);
} else if (PropertyNamer.isGetter(methodName)) {
final String property = PropertyNamer.methodToProperty(methodName);
if (lazyLoader.hasLoader(property)) {
//延遲加載單個屬性
lazyLoader.load(property);
}
}
}
}
}
return methodProxy.invoke(enhanced, args);
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
}
懶加載程序會根據resultLoader記錄的嵌套查詢的資訊,呼叫具體的方法查詢,最后將查詢結果反射set到目標物件中,完成懶加載程序,
public void load(final Object userObject) throws SQLException {
.....
//關鍵點就在這,查詢出關聯物件,然后通過metaObject給目標物件的關聯屬性賦值
this.metaResultObject.setValue(property, this.resultLoader.loadResult());
}
public Object loadResult() throws SQLException {
//這里就會查詢出關聯物件
List<Object> list = selectList();
resultObject = resultExtractor.extractObjectFromList(list, targetType);
return resultObject;
}
其中ResultLoader的生成是在進行嵌套查詢映射時生成的,
private Object getNestedQueryMappingValue(ResultSet rs, MetaObject metaResultObject, ResultMapping propertyMapping, ResultLoaderMap lazyLoader, String columnPrefix)
throws SQLException {
final String nestedQueryId = propertyMapping.getNestedQueryId();
final String property = propertyMapping.getProperty();
final MappedStatement nestedQuery = configuration.getMappedStatement(nestedQueryId);
final Class<?> nestedQueryParameterType = nestedQuery.getParameterMap().getType();
final Object nestedQueryParameterObject = prepareParameterForNestedQuery(rs, propertyMapping, nestedQueryParameterType, columnPrefix);
Object value = null;
if (nestedQueryParameterObject != null) {
.....
if (executor.isCached(nestedQuery, key)) {
executor.deferLoad(nestedQuery, metaResultObject, property, key, targetType);
value = DEFERED;
} else {
//重點,ResultLoader 就在這里構造,記錄嵌套查詢的資訊
final ResultLoader resultLoader = new ResultLoader(configuration, executor, nestedQuery, nestedQueryParameterObject, targetType, key, nestedBoundSql);
//懶加載的處理
if (propertyMapping.isLazy()) {
//property:嵌套查詢配置的type物件 metaResultObject:目標代理物件 resultLoader: 嵌套查詢資訊
lazyLoader.addLoader(property, metaResultObject, resultLoader);
value = DEFERED; //標識為懶加載
} else {
value = resultLoader.loadResult();
}
}
}
return value;
}
快取
簡介
Mybatis的快取分為一級快取和二級快取,一級快取存在于SqlSession的生命周期中,默認會啟用,二級快取也叫應用快取,存在于SqlSessionFactory的生命周期中,可以理解為跨SqlSession的,快取是以 namespace為單位的,默認未啟用,
對一級快取來說同一個SqlSession在查詢時, Mybatis會把執行的方法和引數等資訊通過演算法生成快取的鍵值,將鍵值和查詢結果存入一個 Map 物件中,大多數情況下同一個SqlSession中執行的方法和引數完全一致,那么通過演算法會生成相同的鍵值,當Map快取物件中己經存在該鍵值時,則查詢時會回傳快取中的物件,任何的 INSERT 、UPDATE 、DELETE 操作都會清空一級快取;
快取具體實作
Mybatis快取的底層是基于一個HashMap進行存盤的,具體的實作為:
//快取的具體實作類
public class PerpetualCache implements Cache {
private final String id; //Mapper中namespace的值
//這個的key: cachekey value : sql陳述句處理程序
//cachekey: sql陳述句、入參、分頁資訊、MappedStatement的id
private Map<Object, Object> cache = new HashMap<>();
...
}
而Mybatis中快取的key生成是非常嚴謹的,在查詢時會呼叫一個createCacheKey方法創建一個CacheKey物件,CacheKey的生成主要由四大約束條件:
1、MappedStatement的id
2、Mybatis內置分頁物件的引數資訊
3、sql陳述句
4、sql陳述句對應的實際引數值
//創建CacheKey物件,做為快取Map中訪問的key
@Override
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
if (closed) {
throw new ExecutorException("Executor was closed.");
}
CacheKey cacheKey = new CacheKey();
cacheKey.update(ms.getId()); //MappedStatement的id加入計算
cacheKey.update(rowBounds.getOffset()); //分頁資訊
cacheKey.update(rowBounds.getLimit()); //分頁資訊
cacheKey.update(boundSql.getSql()); // 將sql陳述句加入計算
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings(); //sql陳述句的引數映射集
TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
// mimic DefaultParameterHandler logic
for (ParameterMapping parameterMapping : parameterMappings) {
if (parameterMapping.getMode() != ParameterMode.OUT) {
Object value;
String propertyName = parameterMapping.getProperty(); // 獲取引數的屬性
// 以下獲取引數的值
if (boundSql.hasAdditionalParameter(propertyName)) {
value = boundSql.getAdditionalParameter(propertyName);
} else if (parameterObject == null) {
value = null;
} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
value = parameterObject;
} else {
MetaObject metaObject = configuration.newMetaObject(parameterObject);
value = metaObject.getValue(propertyName);
}
cacheKey.update(value); // 將引數值加入計算
}
}
if (configuration.getEnvironment() != null) {
// issue #176
cacheKey.update(configuration.getEnvironment().getId()); // 存在Environment,則將Environment的id也加入計算
}
return cacheKey;
}
二級快取
配置二級快取
在 Mybatis 的核心組態檔中cacheEnabled引數是二級快取的全域開關,默認值是 true,如果把這個引數設定為false,即使有后面的二級快取配置,也不會生效,
要開啟二級快取,只需要在某一個Mapper檔案中添加配置,如下:
<cache eviction=“LRU" flushInterval="60000" size="512" readOnly="true"/>
注意: 二級快取是以namespace為單位的,屬于SqlSession共享的,容易出現臟讀現象,應該避免去使用二級快取,
二級快取實作
在Mapper檔案中開啟二級快取時,是可以動態的配置一些屬性,比如:淘汰策略、定時重繪、同步、日志、序列化功能等能力,Mybatis針對這種場景使用了裝飾器模式進行了二級快取功能的動態增強,二級快取實作類圖如下:

底層在初始化二級快取時,是通過XMLMapperBuilder在決議每一個mapper檔案時,會決議每一個cache標簽,然后為當前mapper檔案生成一個PerpetualCache快取具體實作類,之后會根據cache標簽的配置的屬性以及一些默認的屬性,創建對應的快取裝飾器物件(比如SynchronizedCache裝飾器物件,默認為二級快取添加同步功能),對PerpetualCache進行具體的裝飾,
public Cache build() {
//設定快取的主實作類為PerpetualCache
setDefaultImplementations();
//通過反射實體化PerpetualCache物件
Cache cache = newBaseCacheInstance(implementation, id);
setCacheProperties(cache);//根據cache節點下的<property>資訊,初始化cache
// issue #352, do not apply decorators to custom caches
if (PerpetualCache.class.equals(cache.getClass())) {//如果cache是PerpetualCache的實作,則為其添加標準的裝飾器
for (Class<? extends Cache> decorator : decorators) {//為cache物件添加裝飾器,這里主要處理快取清空策略的裝飾器
cache = newCacheDecoratorInstance(decorator, cache);
setCacheProperties(cache);
}
//通過一些屬性為cache物件添加裝飾器
cache = setStandardDecorators(cache);
} else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) {
//如果cache不是PerpetualCache的實作,則為其添加日志的能力
cache = new LoggingCache(cache);
}
return cache;
}
插件
簡介
插件是用來改變或者擴展Mybatis的原有的功能,Mybatis的插件就是通過繼承Interceptor攔截器來實作的,在沒有完全理解插件之前禁止使用插件對Mybatis進行擴展,有可能會導致嚴重的問題;
Mybatis中能使用插件進行攔截的介面和方法如下:
- Executor(update、query 、 flushStatment 、 commit 、 rollback 、 getTransaction 、 close 、 isClose)
- StatementHandler(prepare 、 paramterize 、 batch 、 update 、 query)
- ParameterHandler( getParameterObject 、 setParameters )
- ResultSetHandler( handleResultSets 、 handleCursorResultSets 、 handleOutputParameters )
實作原理
初始化
在Mybatis的組態檔中引入一個插件
<plugins>
<plugin interceptor="com.github.pagehelper.PageInterceptor">
<property name="pageSizeZero" value="true" />
</plugin>
</plugins>
之后,組態檔在決議時會通過XMLConfigBuilder這個類,決議所有的plugin標簽,然后根據指定的插件類名,將對應的插件進行反射實體化,最后添加到Configuration配置類中的InterceptorChain物件中,其內部通過list記錄所有的插件,
private void pluginElement(XNode parent) throws Exception {
if (parent != null) {
//遍歷所有的插件配置
for (XNode child : parent.getChildren()) {
//獲取插件的類名
String interceptor = child.getStringAttribute("interceptor");
//獲取插件的配置
Properties properties = child.getChildrenAsProperties();
//實體化插件物件
Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
//設定插件屬性
interceptorInstance.setProperties(properties);
//將插件添加到configuration物件,底層使用list保存所有的插件并記錄順序
configuration.addInterceptor(interceptorInstance);
}
}
}
加載
當創建Executor、StatementHandler、ParameterHandler、ResultSetHandler這些介面實作類時,就會嘗試添加插件功能,在Mybatis的Configuration配置類中提供了new這些物件的方法,其中創建Executor實作如下:
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
....
//通過interceptorChain遍歷所有的插件為executor增強,添加插件的功能
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
遍歷初始化時收集的所有插件,為目標物件添加插件功能
public class InterceptorChain {
private final List<Interceptor> interceptors = new ArrayList<>();
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
//為目標物件添加插件功能
target = interceptor.plugin(target);
}
return target;
}
...
}
通常情況下插件的plugin方法都會執行到Plugin這個代理處理類的wrap方法,通過這個wrap方法會決議當前插件上的@Intercepts注解內部的@Signature注解資訊,然后根據每個Signature攔截的型別來確認是否能夠攔截到當前目標物件,如果能就會基于JDK動態代理為當前目標物件創建一個代理物件,
//靜態方法,用于幫助Interceptor生成動態代理
public static Object wrap(Object target, Interceptor interceptor) {
//決議Interceptor上@Intercepts注解得到的signature資訊
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
Class<?> type = target.getClass();//獲取目標物件的型別
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);//獲取目標物件實作的介面(攔截器可以攔截4大物件實作的介面)
if (interfaces.length > 0) {
//使用jdk的方式創建動態代理
return Proxy.newProxyInstance(
type.getClassLoader(),
interfaces,
new Plugin(target, interceptor, signatureMap));
}
return target;
}
呼叫
加載程序我們知道如果目標物件能夠被任意一個插件攔截就會為其生成一個代理物件,之后當目標物件執行相應方法時,就會被代理物件攔截,然后執行到Plugin這個類的invoke方法,invoke執行程序中會先判斷當前呼叫的方法是否被攔截,如果被攔截就會執行插件的intercept方法,呼叫具體插件邏輯,
public class Plugin implements InvocationHandler {
//封裝的真正提供服務的物件
private final Object target;
//插件攔截器
private final Interceptor interceptor;
//決議@Intercepts注解得到的signature資訊
private final Map<Class<?>, Set<Method>> signatureMap;
....
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
//獲取當前介面可以被攔截的方法
Set<Method> methods = signatureMap.get(method.getDeclaringClass());
if (methods != null && methods.contains(method)) {//如果當前方法需要被攔截,則呼叫interceptor.intercept方法進行攔截處理
return interceptor.intercept(new Invocation(target, method, args));
}
//如果當前方法不需要被攔截,則呼叫物件自身的方法
return method.invoke(target, args);
} catch (Exception e) {
throw ExceptionUtil.unwrapThrowable(e);
}
}
}
流式讀取
簡介
Mybatis針對大數量讀取提供了一種流式讀取的方式,避免查詢資料過多,導致OOM,具體實作:
首先,自定義ResultHandler實作類來處理結果集
public class MyResultHandler<T> implements ResultHandler<T>{
//如果需要批量處理,可以定義一個容器進行保存每條資料
//當超過BATCH_SIZE再對容器中資料進行處理
private final int BATCH_SIZE= 100;
private List<T> list=new ArrayList<T>();
@Override
public void handleResult(ResultContext resultContext) {
// TODO 流式讀取,每次只回傳單條結果
Object result=resultContext.getResultObject();
// TODO 對獲取到的結果資料進行相應的業務處理
}
}
其次,在指定mapper介面中定義一個以ResultHandler作為入參的查詢方法,并且查詢方法不接識訓傳值
/**
* 流式讀取資料
* @param handler 回呼處理
*/
void seachUserDataList(ResultHandler handler);
最后,在呼叫seachUserDataList()查詢方法時,會將查詢到的每一條記錄都呼叫一次MyResultHandler的handleResult()方法,這就是流式讀取的效果,避免一次性在記憶體中加載過大的物件,
實作原理
DefaultResultSetHandler在進行查詢的結果集處理時,會判斷當前查詢方法是否有指定ResultHandler做為入參,如果有,就會使用指定的resultHandler進行后續處理,處理程序中會通過handleRowValuesForSimpleResultMap()對查詢的結果集的每條資料進行處理,將ResultSet結果集的每一行資料映射成目標物件,再呼叫storeObject方法保存映射目標物件,之后就會執行callResultHandler()方法,將目標物件添加到resultContext中,最后根據指定的resultHandler呼叫它的handleResult()方法,達到流式讀取的效果,
//處理結果集
private void handleResultSet(ResultSetWrapper rsw, ResultMap resultMap, List<Object> multipleResults, ResultMapping parentMapping) throws SQLException {
try {
if (parentMapping != null) {//處理多結果集的嵌套映射
handleRowValues(rsw, resultMap, null, RowBounds.DEFAULT, parentMapping);
} else {
if (resultHandler == null) {//如果resultHandler為空,實體化一個人默認的resultHandler
....
} else {
//使用指定的resultHandler進行處理
handleRowValues(rsw, resultMap, resultHandler, rowBounds, null);
}
}
} finally {
// issue #228 (close resultsets)
//呼叫resultset.close()關閉結果集
closeResultSet(rsw.getResultSet());
}
}
//簡單映射處理
private void handleRowValuesForSimpleResultMap(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping)
throws SQLException {
//創建結果背景關系,所謂的背景關系就是專門在回圈中快取結果物件的
DefaultResultContext<Object> resultContext = new DefaultResultContext<>();
skipRows(rsw.getResultSet(), rowBounds);
//shouldProcessMoreRows判斷是否需要映射后續的結果,實際還是翻頁處理,避免超過limit
while (shouldProcessMoreRows(resultContext, rowBounds) && rsw.getResultSet().next()) {
ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(rsw.getResultSet(), resultMap, null);
//讀取resultSet中的一行記錄并進行映射,轉化并回傳目標物件
Object rowValue = getRowValue(rsw, discriminatedResultMap);
//保存映射結果物件
storeObject(resultHandler, resultContext, rowValue, parentMapping, rsw.getResultSet());
}
}
private void storeObject(ResultHandler<?> resultHandler, DefaultResultContext<Object> resultContext, Object rowValue, ResultMapping parentMapping, ResultSet rs) throws SQLException {
if (parentMapping != null) {
linkToParents(rs, parentMapping, rowValue);
} else {//普通映射則把物件保存至resultHandler和resultContext
callResultHandler(resultHandler, resultContext, rowValue);
}
}
private void callResultHandler(ResultHandler<?> resultHandler, DefaultResultContext<Object> resultContext, Object rowValue) {
resultContext.nextResultObject(rowValue);
//流式讀取
((ResultHandler<Object>) resultHandler).handleResult(resultContext);
}
標簽的id可以重復嗎
Mybatis中約束了同一種型別的標簽(ResultMap、增刪改查)不能存在相同的namespace+id,原因是Mybatis的配置類Configuration中,會通過Map去記錄每個標簽封裝的物件,其中namespace+id 是作為Map的key使用的,而Map是使用的自定義StrictMap繼承與HashMap,在進行put()插入標簽元素時,會判斷namespace+id是否重復,重復就會拋出例外,
protected static class StrictMap<V> extends HashMap<String, V> {
...
public V put(String key, V value) {
//插入之前判斷namespace+id,是否已經存在,已經存在拋出例外
if (containsKey(key)) {
throw new IllegalArgumentException(name + " already contains value for " + key);
}
....
return super.put(key, value);
}
}
總結
以上介紹了一些Mybatis內部的一些技術點,也結合原始碼去簡單分析了技術點底層實作,在代碼截圖中對一些非關鍵的代碼進行了洗掉,避免關注到其它知識點,個人認為在原始碼學習的程序中,每一個點的學習,要去抓住關鍵點,去排除掉一些無用不相關的代碼,這樣整體的結構就會更清晰點,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/203072.html
標籤:其他
上一篇:金鴿工業以太網遠程I/O資料采集模塊 (產品系列:MxxxT)
下一篇:Java實作單鏈表的簡單操作
