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 。其繼承結構如圖:

其中 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操作的代理,畢竟動態代理還是有性能損耗的。

相關文章