通過原始碼分析Mybatis的功能

weilence發表於2020-08-01

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,之後會快取

  • 建立時會根據方法簽名,解析出引數名,解析的規則順序是

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

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

    3. 如果配置項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。其繼承結構如圖:

Executor

其中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, parameter, rowBounds, resultHandler, key, 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, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    List<E> list;
    // 在快取中,新增佔位物件。此處的佔位符,和延遲載入有關,可見 `DeferredLoad#canLoad()` 方法
    localCache.putObject(key, EXECUTION_PLACEHOLDER);
    try {
        // 執行讀操作
        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;
}

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

@Override
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, 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, 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);
            if (list == null) {
                // 如果不存在,則從資料庫中查詢
                list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
                // 快取結果到二級快取中
                tcm.putObject(cache, key, list); // issue #578 and #116
            }
            // 如果存在,則直接返回結果
            return list;
        }
    }
    // 不使用快取,則從資料庫中查詢
    return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, 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, Object parameterObject, BoundSql boundSql) {
    // 建立 ParameterHandler 物件
    ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
    // 應用外掛
    parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
    return parameterHandler;
}

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

// 建立 StatementHandler 物件
public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
    // 建立 RoutingStatementHandler 物件
    StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, 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;
}

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

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

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

相關文章