1. 程式人生 > 程式設計 >通過原始碼分析Mybatis的功能流程詳解

通過原始碼分析Mybatis的功能流程詳解

SQL解析

Mybatis在初始化的時候,會讀取xml中的SQL,解析後會生成SqlSource物件,SqlSource物件分為兩種。

  • DynamicSqlSource,動態SQL,獲取SQL(getBoundSQL方法中)的時候生成引數化SQL。
  • RawSqlSource,原始SQL,建立物件時直接生成引數化SQL。

因為RawSqlSource不會重複去生成引數化SQL,呼叫的時候直接傳入引數並執行,而DynamicSqlSource則是每次執行的時候引數化SQL,所以RawSqlSourceDynamicSqlSource的效能要好的。

解析的時候會先解析include標籤和selectkey

標籤,然後判斷是否是動態SQL,判斷取決於以下兩個條件:

  • SQL中有動態拼接字串,簡單來說就是是否使用了${}表示式。注意這種方式存在SQL注入,謹慎使用。
  • SQL中有trimwheresetforeachifchoosewhenotherwisebind標籤

相關程式碼如下:

protected MixedSqlNode parseDynamicTags(XNode node) {
 // 建立 SqlNode 陣列
 List<SqlNode> contents = new ArrayList<>();
 // 遍歷 SQL 節點的所有子節點
 NodeList children = node.getNode().getChildNodes();
 for (int i = 0; i < children.getLength(); i++) {
  // 當前子節點
  XNode child = node.newXNode(children.item(i));
  // 如果型別是 Node.CDATA_SECTION_NODE 或者 Node.TEXT_NODE 時
  if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
   // 獲得內容
   String data = child.getStringBody("");
   // 建立 TextSqlNode 物件
   TextSqlNode textSqlNode = new TextSqlNode(data);
   // 如果是動態的 TextSqlNode 物件(是否使用了${}表示式)
   if (textSqlNode.isDynamic()) {
    // 新增到 contents 中
    contents.add(textSqlNode);
    // 標記為動態 SQL
    isDynamic = true;
    // 如果是非動態的 TextSqlNode 物件
   } else {
    // 建立 StaticTextSqlNode 新增到 contents 中
    contents.add(new StaticTextSqlNode(data));
   }
   // 如果型別是 Node.ELEMENT_NODE,其實就是XMl中<where>等那些動態標籤
  } else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628
   // 根據子節點的標籤,獲得對應的 NodeHandler 物件
   String nodeName = child.getNode().getNodeName();
   NodeHandler handler = nodeHandlerMap.get(nodeName);
   if (handler == null) { // 獲得不到,說明是未知的標籤,丟擲 BuilderException 異常
    throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
   }
   // 執行 NodeHandler 處理
   handler.handleNode(child,contents);
   // 標記為動態 SQL
   isDynamic = true;
  }
 }
 // 建立 MixedSqlNode 物件
 return new MixedSqlNode(contents);
}

引數解析

Mybais中用於解析Mapper方法的引數的類是ParamNameResolver,它主要做了這些事情:

  • 每個Mapper方法第一次執行時會去建立ParamNameResolver,之後會快取
  • 建立時會根據方法簽名,解析出引數名,解析的規則順序是

如果引數型別是RowBounds或者ResultHandler型別或者他們的子類,則不處理。

如果引數中有Param註解,則使用Param中的值作為引數名

如果配置項useActualParamName=true,argn(n>=0)標作為引數名,如果你是Java8以上並且開啟了-parameters`,則是實際的引數名

如果配置項useActualParamName=false,則使用n(n>=0)作為引數名

相關原始碼:

public ParamNameResolver(Configuration config,Method method) {
 final Class<?>[] paramTypes = method.getParameterTypes();
 final Annotation[][] paramAnnotations = method.getParameterAnnotations();
 final SortedMap<Integer,String> map = new TreeMap<Integer,String>();
 int paramCount = paramAnnotations.length;
 // 獲取方法中每個引數在SQL中的引數名
 for (int paramIndex = 0; paramIndex < paramCount; paramIndex++) {
  // 跳過RowBounds、ResultHandler型別
  if (isSpecialParameter(paramTypes[paramIndex])) {
   continue;
  }
  String name = null;
  // 遍歷引數上面的所有註解,如果有Param註解,使用它的值作為引數名
  for (Annotation annotation : paramAnnotations[paramIndex]) {
   if (annotation instanceof Param) {
    hasParamAnnotation = true;
    name = ((Param) annotation).value();
    break;
   }
  }
  // 如果沒有指定註解
  if (name == null) {
   // 如果開啟了useActualParamName配置,則引數名為argn(n>=0),如果是Java8以上並且開啟-parameters,則為實際的引數名
   if (config.isUseActualParamName()) {
    name = getActualParamName(method,paramIndex);
   }
   // 否則為下標
   if (name == null) {
    name = String.valueOf(map.size());
   }
  }
  map.put(paramIndex,name);
 }
 names = Collections.unmodifiableSortedMap(map);
}

而在使用這個names構建xml中引數物件和值的對映時,還進行了進一步的處理。

public Object getNamedParams(Object[] args) {
 final int paramCount = names.size();
 // 無引數,直接返回null
 if (args == null || paramCount == 0) {
  return null;
 } else if (!hasParamAnnotation && paramCount == 1) {
  // 一個引數,並且沒有註解,直接返回這個物件
  return args[names.firstKey()];
 } else {
  // 其他情況則返回一個Map物件
  final Map<String,Object> param = new ParamMap<Object>();
  int i = 0;
  for (Map.Entry<Integer,String> entry : names.entrySet()) {
   // 先直接放入name的鍵和對應位置的引數值,其實就是建構函式中存入的值
   param.put(entry.getValue(),args[entry.getKey()]);
   // add generic param names (param1,param2,...)
   final String genericParamName = GENERIC_NAME_PREFIX + String.valueOf(i + 1);
   // 防止覆蓋 @Param 的引數值
   if (!names.containsValue(genericParamName)) {
    // 然後放入GENERIC_NAME_PREFIX + index + 1,其實就是param1,params2,paramn
    param.put(genericParamName,args[entry.getKey()]);
   }
   i++;
  }
  return param;
 }
}

另外值得一提的是,對於集合型別,最後還有一個特殊處理

private Object wrapCollection(final Object object) {
 // 如果物件是集合屬性
 if (object instanceof Collection) {
  StrictMap<Object> map = new StrictMap<Object>();
  // 加入一個collection引數
  map.put("collection",object);
  // 如果是一個List集合
  if (object instanceof List) {
   // 額外加入一個list屬性使用
   map.put("list",object);
  }
  return map;
 } else if (object != null && object.getClass().isArray()) {
  // 陣列使用array
  StrictMap<Object> map = new StrictMap<Object>();
  map.put("array",object);
  return map;
 }
 return object;
}

由此我們可以得出使用引數的結論:

  • 如果引數加了@Param註解,則使用註解的值作為引數
  • 如果只有一個引數,並且不是集合型別和陣列,且沒有加註解,則使用物件的屬性名作為引數如果只有一個引數,並且是集合型別,則使用collection引數,如果是List物件,可以額外使用list引數。
  • 如果只有一個引數,並且是陣列,則可以使用array引數如果有多個引數,沒有加@Param註解的可以使用argn或者n(n>=0,取決於useActualParamName配置項)作為引數,加了註解的使用註解的值。
  • 如果有多個引數,任意引數只要不是和@Param中的值覆蓋,都可以使用paramn(n>=1)

延遲載入

Mybatis是支援延遲載入的,具體的實現方式根據resultMap建立返回物件時,發現fetchType=“lazy”,則使用代理物件,預設使用Javassist(MyBatis 3.3 以上,可以修改為使用CgLib)。程式碼處理邏輯在處理返回結果集時,具體程式碼呼叫關係如下:

PreparedStatementHandler.query=> handleResultSets =>handleResultSet=>handleRowValues=>handleRowValuesForNestedResultMap=>getRowValue

getRowValue中,有一個方法createResultObject建立返回物件,其中的關鍵程式碼建立了代理物件:

if (propertyMapping.getNestedQueryId() != null && propertyMapping.isLazy()) {
 resultObject = configuration.getProxyFactory().createProxy(resultObject,lazyLoader,configuration,objectFactory,constructorArgTypes,constructorArgs);
}

另一方面,getRowValue會呼叫applyPropertyMappings方法,其內部會呼叫getPropertyMappingValue,繼續追蹤到getNestedQueryMappingValue方法,在這裡,有幾行關鍵程式碼:

// 如果要求延遲載入,則延遲載入
if (propertyMapping.isLazy()) {
 // 如果該屬性配置了延遲載入,則將其新增到 `ResultLoader.loaderMap` 中,等待真正使用時再執行巢狀查詢並得到結果物件。
 lazyLoader.addLoader(property,metaResultObject,resultLoader);
 // 返回已定義
 value = DEFERED;
 // 如果不要求延遲載入,則直接執行載入對應的值
} else {
 value = resultLoader.loadResult();
}

這幾行的目的是跳過屬性值的載入,等真正需要值的時候,再獲取值。

Executor

Executor是一個介面,其直接實現的類是BaseExecutorCachingExecutorBaseExecutor又派生了BatchExecutorReuseExecutorSimpleExecutorClosedExecutor。其繼承結構如圖:

通過原始碼分析Mybatis的功能流程詳解

其中ClosedExecutor是一個私有類,使用者不直接使用它。

  • BaseExecutor:模板類,裡面有各個Executor的公用的方法。
  • SimpleExecutor:最常用的Executor,預設是使用它去連線資料庫,執行SQL語句,沒有特殊行為。ReuseExecutor:SQL語句執行後會進行快取,不會關閉Statement,下次執行時會複用,快取的key值是BoundSql解析後SQL,清空快取使用doFlushStatements。其他與SimpleExecutor相同。
  • BatchExecutor:當有連續的InsertUpdateDelete的操作語句,並且語句的BoundSql相同,則這些語句會批量執行。使用doFlushStatements方法獲取批量操作的返回值。
  • CachingExecutor:當你開啟二級快取的時候,會使用CachingExecutor裝飾SimpleExecutorReuseExecutorBatchExecutor,Mybatis通過CachingExecutor來實現二級快取。

快取

一級快取

Mybatis一級快取的實現主要是在BaseExecutor中,在它的查詢方法裡,會優先查詢快取中的值,如果不存在,再查詢資料庫,查詢部分的程式碼如下,關鍵程式碼在17-24行:

@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());
 // 已經關閉,則丟擲 ExecutorException 異常
 if (closed) {
  throw new ExecutorException("Executor was closed.");
 }
 // 清空本地快取,如果 queryStack 為零,並且要求清空本地快取。
 if (queryStack == 0 && ms.isFlushCacheRequired()) {
  clearLocalCache();
 }
 List<E> list;
 try {
  // queryStack + 1
  queryStack++;
  // 從一級快取中,獲取查詢結果
  list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
  // 獲取到,則進行處理
  if (list != null) {
   handleLocallyCachedOutputParameters(ms,key,parameter,boundSql);
   // 獲得不到,則從資料庫中查詢
  } else {
   list = queryFromDatabase(ms,rowBounds,resultHandler,boundSql);
  }
 } finally {
  // queryStack - 1
  queryStack--;
 }
 if (queryStack == 0) {
  // 執行延遲載入
  for (DeferredLoad deferredLoad : deferredLoads) {
   deferredLoad.load();
  }
  // issue #601
  // 清空 deferredLoads
  deferredLoads.clear();
  // 如果快取級別是 LocalCacheScope.STATEMENT ,則進行清理
  if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
   // issue #482
   clearLocalCache();
  }
 }
 return list;
}

而在queryFromDatabase中,則會將查詢出來的結果放到快取中。

// 從資料庫中讀取操作
private <E> List<E> queryFromDatabase(MappedStatement ms,BoundSql boundSql) throws SQLException {
 List<E> list;
 // 在快取中,新增佔位物件。此處的佔位符,和延遲載入有關,可見 `DeferredLoad#canLoad()` 方法
 localCache.putObject(key,EXECUTION_PLACEHOLDER);
 try {
  // 執行讀操作
  list = doQuery(ms,boundSql);
 } finally {
  // 從快取中,移除佔位物件
  localCache.removeObject(key);
 }
 // 新增到快取中
 localCache.putObject(key,list);
 // 暫時忽略,儲存過程相關
 if (ms.getStatementType() == StatementType.CALLABLE) {
  localOutputParameterCache.putObject(key,parameter);
 }
 return list;
}

而一級快取的Key,從方法的引數可以看出,與呼叫方法、引數、rowBounds分頁引數、最終生成的sql有關。

@Override
public CacheKey createCacheKey(MappedStatement ms,Object parameterObject,BoundSql boundSql) {
 if (closed) {
  throw new ExecutorException("Executor was closed.");
 }
 // 建立 CacheKey 物件
 CacheKey cacheKey = new CacheKey();
 // 設定 id、offset、limit、sql 到 CacheKey 物件中
 cacheKey.update(ms.getId());
 cacheKey.update(rowBounds.getOffset());
 cacheKey.update(rowBounds.getLimit());
 cacheKey.update(boundSql.getSql());
 // 設定 ParameterMapping 陣列的元素對應的每個 value 到 CacheKey 物件中
 List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
 TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
 // mimic DefaultParameterHandler logic 這塊邏輯,和 DefaultParameterHandler 獲取 value 是一致的。
 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);
  }
 }
 // 設定 Environment.id 到 CacheKey 物件中
 if (configuration.getEnvironment() != null) {
  // issue #176
  cacheKey.update(configuration.getEnvironment().getId());
 }
 return cacheKey;
}

通過檢視一級快取類的實現,可以看出一級快取是通過HashMap結構儲存的:

/**
 * 一級快取的實現類,部分原始碼
 */
public class PerpetualCache implements Cache {
 /**
  * 快取容器
  */
 private Map<Object,Object> cache = new HashMap<>();

 @Override
 public void putObject(Object key,Object value) {
  cache.put(key,value);
 }

 @Override
 public Object getObject(Object key) {
  return cache.get(key);
 }

 @Override
 public Object removeObject(Object key) {
  return cache.remove(key);
 }
}

通過配置項,我們可以控制一級快取的使用範圍,預設是Session級別的,也就是SqlSession的範圍內有效。也可以配製成Statement級別,當本次查詢結束後立即清除快取。

當進行插入、更新、刪除操作時,也會在執行SQL之前清空以及快取。

二級快取

Mybatis二級快取的實現是依靠CachingExecutor裝飾其他的Executor實現。原理是在查詢的時候先根據CacheKey查詢快取中是否存在值,如果存在則返回快取的值,沒有則查詢資料庫。

CachingExecutorquery方法中,就有快取的使用:

public <E> List<E> query(MappedStatement ms,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);
   if (list == null) {
    // 如果不存在,則從資料庫中查詢
    list = delegate.query(ms,parameterObject,boundSql);
    // 快取結果到二級快取中
    tcm.putObject(cache,list); // issue #578 and #116
   }
   // 如果存在,則直接返回結果
   return list;
  }
 }
 // 不使用快取,則從資料庫中查詢
 return delegate.query(ms,boundSql);
}

那麼這個Cache是在哪裡建立的呢?通過呼叫的追溯,可以找到它的建立:

public Cache useNewCache(Class<? extends Cache> typeClass,Class<? extends Cache> evictionClass,Long flushInterval,Integer size,boolean readWrite,boolean blocking,Properties props) {
 // 建立 Cache 物件
 Cache cache = new CacheBuilder(currentNamespace)
  .implementation(valueOrDefault(typeClass,PerpetualCache.class))
  .addDecorator(valueOrDefault(evictionClass,LruCache.class))
  .clearInterval(flushInterval)
  .size(size)
  .readWrite(readWrite)
  .blocking(blocking)
  .properties(props)
  .build();
 // 新增到 configuration 的 caches 中
 configuration.addCache(cache);
 // 賦值給 currentCache
 currentCache = cache;
 return cache;
}

從方法的第一行可以看出,Cache物件的範圍是namespace,同一個namespace下的所有mapper方法共享Cache物件,也就是說,共享這個快取。

另一個建立方法是通過CacheRef裡面的:

public Cache useCacheRef(String namespace) {
 if (namespace == null) {
  throw new BuilderException("cache-ref element requires a namespace attribute.");
 }
 try {
  unresolvedCacheRef = true; // 標記未解決
  // 獲得 Cache 物件
  Cache cache = configuration.getCache(namespace);
  // 獲得不到,丟擲 IncompleteElementException 異常
  if (cache == null) {
   throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found.");
  }
  // 記錄當前 Cache 物件
  currentCache = cache;
  unresolvedCacheRef = false; // 標記已解決
  return cache;
 } catch (IllegalArgumentException e) {
  throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found.",e);
 }
}

這裡的話會通過CacheRef中的引數namespace,找到那個Cache物件,且這裡使用了unresolvedCacheRef,因為Mapper檔案的載入是有順序的,可能當前載入時引用的那個namespace的Mapper檔案還沒有載入,所以用這個標記一下,延後載入。

二級快取通過TransactionalCache來管理,內部使用的是一個HashMap。Key是Cache物件,預設的實現是PerpetualCache,一個namespace下共享這個物件。Value是另一個Cache的物件,預設實現是TransactionalCache,是前面那個Key值的裝飾器,擴充套件了事務方面的功能。

通過檢視TransactionalCache的原始碼我們可以知道,預設查詢後新增的快取儲存在待提交物件裡。

public void putObject(Object key,Object object) {
 // 暫存 KV 到 entriesToAddOnCommit 中
 entriesToAddOnCommit.put(key,object);
}

只有等到commit的時候才會去刷入快取。

public void commit() {
 // 如果 clearOnCommit 為 true ,則清空 delegate 快取
 if (clearOnCommit) {
  delegate.clear();
 }
 // 將 entriesToAddOnCommit、entriesMissedInCache 刷入 delegate 中
 flushPendingEntries();
 // 重置
 reset();
}

檢視clear程式碼,只是做了標記,並沒有真正釋放物件。在查詢時根據標記直接返回空,在commit才真正釋放物件:

public void clear() {
 // 標記 clearOnCommit 為 true
 clearOnCommit = true;
 // 清空 entriesToAddOnCommit
 entriesToAddOnCommit.clear();
}

public Object getObject(Object key) {
 // issue #116
 // 從 delegate 中獲取 key 對應的 value
 Object object = delegate.getObject(key);
 // 如果不存在,則新增到 entriesMissedInCache 中
 if (object == null) {
  entriesMissedInCache.add(key);
 }
 // issue #146
 // 如果 clearOnCommit 為 true ,表示處於持續清空狀態,則返回 null
 if (clearOnCommit) {
  return null;
  // 返回 value
 } else {
  return object;
 }
}

rollback會清空這些臨時快取:

public void rollback() {
 // 從 delegate 移除出 entriesMissedInCache
 unlockMissedEntries();
 // 重置
 reset();
}

private void reset() {
 // 重置 clearOnCommit 為 false
 clearOnCommit = false;
 // 清空 entriesToAddOnCommit、entriesMissedInCache
 entriesToAddOnCommit.clear();
 entriesMissedInCache.clear();
}

根據二級快取程式碼可以看出,二級快取是基於namespace的,可以跨SqlSession。也正是因為基於namespace,如果在不同的namespace中修改了同一個表的資料,會導致髒讀的問題。

外掛

Mybatis的外掛是通過代理物件實現的,可以代理的物件有:

  • Executor:執行器,執行器是執行過程中第一個代理物件,它內部呼叫StatementHandler返回SQL結果。
  • StatementHandler:語句處理器,執行SQL前呼叫ParameterHandler處理引數,執行SQL後呼叫ResultSetHandler處理返回結果
  • ParameterHandler:引數處理器
  • ResultSetHandler:返回物件處理器

這四個物件的介面的所有方法都可以用外掛攔截。

外掛的實現程式碼如下:

// 建立 ParameterHandler 物件
public ParameterHandler newParameterHandler(MappedStatement mappedStatement,BoundSql boundSql) {
 // 建立 ParameterHandler 物件
 ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement,boundSql);
 // 應用外掛
 parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
 return parameterHandler;
}

// 建立 ResultSetHandler 物件
public ResultSetHandler newResultSetHandler(Executor executor,MappedStatement mappedStatement,ParameterHandler parameterHandler,BoundSql boundSql) {
 // 建立 DefaultResultSetHandler 物件
 ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor,mappedStatement,parameterHandler,boundSql,rowBounds);
 // 應用外掛
 resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);
 return resultSetHandler;
}

// 建立 StatementHandler 物件
public StatementHandler newStatementHandler(Executor executor,BoundSql boundSql) {
 // 建立 RoutingStatementHandler 物件
 StatementHandler statementHandler = new RoutingStatementHandler(executor,boundSql);
 // 應用外掛
 statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
 return statementHandler;
}

/**
  * 建立 Executor 物件
  *
  * @param transaction 事務物件
  * @param executorType 執行器型別
  * @return Executor 物件
  */
public Executor newExecutor(Transaction transaction,ExecutorType executorType) {
 // 獲得執行器型別
 executorType = executorType == null ? defaultExecutorType : executorType; // 使用預設
 executorType = executorType == null ? ExecutorType.SIMPLE : executorType; // 使用 ExecutorType.SIMPLE
 // 建立對應實現的 Executor 物件
 Executor executor;
 if (ExecutorType.BATCH == executorType) {
  executor = new BatchExecutor(this,transaction);
 } else if (ExecutorType.REUSE == executorType) {
  executor = new ReuseExecutor(this,transaction);
 } else {
  executor = new SimpleExecutor(this,transaction);
 }
 // 如果開啟快取,建立 CachingExecutor 物件,進行包裝
 if (cacheEnabled) {
  executor = new CachingExecutor(executor);
 }
 // 應用外掛
 executor = (Executor) interceptorChain.pluginAll(executor);
 return executor;
}

可以很明顯的看到,四個方法內都有interceptorChain.pluginAll()方法的呼叫,繼續檢視這個方法:

/**
 * 應用所有外掛
 *
 * @param target 目標物件
 * @return 應用結果
 */
public Object pluginAll(Object target) {
 for (Interceptor interceptor : interceptors) {
  target = interceptor.plugin(target);
 }
 return target;
}

這個方法比較簡單,就是遍歷interceptors列表,然後呼叫器plugin方法。interceptors是在解析XML配置檔案是通過反射建立的,而建立後會立即呼叫setProperties方法

我們通常配置外掛時,會在interceptor.plugin呼叫Plugin.wrap,這裡面通過Java的動態代理,攔截方法的實現:

/**
 * 建立目標類的代理物件
 *
 * @param target 目標類
 * @param interceptor 攔截器物件
 * @return 代理物件
 */
public static Object wrap(Object target,Interceptor interceptor) {
 // 獲得攔截的方法對映
 Map<Class<?>,Set<Method>> signatureMap = getSignatureMap(interceptor);
 // 獲得目標類的型別
 Class<?> type = target.getClass();
 // 獲得目標類的介面集合
 Class<?>[] interfaces = getAllInterfaces(type,signatureMap);
 // 若有介面,則建立目標物件的 JDK Proxy 物件
 if (interfaces.length > 0) {
  return Proxy.newProxyInstance(
   type.getClassLoader(),interfaces,new Plugin(target,interceptor,signatureMap)); // 因為 Plugin 實現了 InvocationHandler 介面,所以可以作為 JDK 動態代理的呼叫處理器
 }
 // 如果沒有,則返回原始的目標物件
 return target;
}

@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)) {
   // 如果是,則攔截處理該方法
   return interceptor.intercept(new Invocation(target,method,args));
  }
  // 如果不是,則呼叫原方法
  return method.invoke(target,args);
 } catch (Exception e) {
  throw ExceptionUtil.unwrapThrowable(e);
 }
}

而攔截的引數傳了Plugin物件,Plugin本身是實現了InvocationHandler介面,其invoke方法裡面呼叫了interceptor.intercept,這個方法就是我們實現攔截處理的地方。

注意到裡面有個getSignatureMap方法,這個方法實現的是查詢我們自定義攔截器的註解,通過註解確定哪些方法需要被攔截:

private static Map<Class<?>,Set<Method>> getSignatureMap(Interceptor interceptor) {
 Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);
 // issue #251
 if (interceptsAnnotation == null) {
  throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName());
 }
 Signature[] sigs = interceptsAnnotation.value();
 Map<Class<?>,Set<Method>> signatureMap = new HashMap<>();
 for (Signature sig : sigs) {
  Set<Method> methods = signatureMap.computeIfAbsent(sig.type(),k -> new HashSet<>());
  try {
   Method method = sig.type().getMethod(sig.method(),sig.args());
   methods.add(method);
  } catch (NoSuchMethodException e) {
   throw new PluginException("Could not find method on " + sig.type() + " named " + sig.method() + ". Cause: " + e,e);
  }
 }
 return signatureMap;
}

通過原始碼我們可以知道,建立一個外掛需要做以下事情:

  • 建立一個類,實現Interceptor介面。
  • 這個類必須使用@Intercepts@Signature來表明要攔截哪個物件的哪些方法。
  • 這個類的plugin方法中呼叫Plugin.wrap(target,this)
  • (可選)這個類的setProperties方法設定一些引數。
  • XML中<plugins>節點配置<plugin interceptor="你的自定義類的全名稱"></plugin>

可以在第三點中根據具體的業務情況不進行本次SQL操作的代理,畢竟動態代理還是有效能損耗的。

到此這篇關於通過原始碼分析Mybatis的功能的文章就介紹到這了,更多相關原始碼分析Mybatis內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!