38

通过源代码分析Mybatis的功能

 3 years ago
source link: http://www.cnblogs.com/Weilence/p/13416986.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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操作的代理,毕竟动态代理还是有性能损耗的。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK