經過源代碼分析Mybatis的功能

SQL解析

Mybatis在初始化的時候,會讀取xml中的SQL,解析後會生成SqlSource對象,SqlSource對象分爲兩種。java

  • DynamicSqlSource,動態SQL,獲取SQL(getBoundSQL方法中)的時候生成參數化SQL。node

  • RawSqlSource,原始SQL,建立對象時直接生成參數化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,它主要作了這些事情:app

  • 每一個Mapper方法第一次運行時會去建立ParamNameResolver,以後會緩存ide

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

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

相關文章
相關標籤/搜索