前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >mybatis插件机制源码解析

mybatis插件机制源码解析

作者头像
用户7634691
发布2021-10-19 14:03:34
4320
发布2021-10-19 14:03:34
举报
文章被收录于专栏:犀牛饲养员的技术笔记

引言

本篇源码解析基于mybatis 3.5.8版本。

首先需要说明的是,本篇文章不是mybatis插件开发的教程,而是从源码层面分析mybatis是如何支持用户自定义插件开发的。

mybatis的插件机制,让其扩展能力大大增加。比如我们项目中经常用到的PageHelper,这就是一款基于mybatis插件能力开发的产品,它的功能是让基于mybatis的数据库分页查询更容易使用。

当然基于插件我们还可以开发其它功能,比如在执行sql前打印日志、做权限控制等。

正文

mybatis插件也叫mybatis拦截器,它支持从方法级别对mybatis进行拦截。整体架构图如下:

解释下几个相关概念:

  • Interceptor 拦截器接口,用户自定义的拦截器就是实现该接口。
  • InterceptorChain 拦截器链,其内部维护一个interceptors list,表示拦截器链中所有的拦截器,并提供增加或获取拦截器链的方法。比如有个核心的方法是pluginAll。该方法用来生成代理对象。
  • Invocation 拦截器执行时的上下文环境,其实就是目标方法的调用信息,包含目标对象、调用的方法信息、参数信息。核心方法是proceed。该方法的主要目的就是进行处理链的传播,执行完拦截器的方法后,最终需要调用目标方法的invoke方法。

mybatis支持在哪些地方进行拦截呢?你只需要在代码里搜索interceptorChain.pluginAll的使用位置就可以获取答案,一共有四处:

代码语言:javascript
复制
parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);

resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);

statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);

executor = (Executor) interceptorChain.pluginAll(executor);

这四处实现的原理都是一样的,我们只需要选择一个进行分析就可以了。

我们先来看下自定义的插件是如何加载进来的,比如我们使用PageHelper插件,通常会在mybatis-config.xml中加入如下的配置:

代码语言:javascript
复制
<plugins>
    <plugin interceptor="com.github.pagehelper.PageInterceptor">
        <!-- config params as the following -->
        <property name="param1" value="value1"/>
    </plugin>
</plugins>

mybatis在创建SqlSessionFactory的时候会加载配置文件,

代码语言:javascript
复制
public Configuration parse() {
    if (parsed) {
      throw new BuilderException("Each XMLConfigBuilder can only be used once.");
    }
    parsed = true;
    parseConfiguration(parser.evalNode("/configuration"));
    return configuration;
  }

parseConfiguration方法会加载包括plugins在内的很多配置,

代码语言:javascript
复制
private void parseConfiguration(XNode root) {
    try {
        ...
      pluginElement(root.evalNode("plugins"));
      ...

    } catch (Exception e) {
      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
  }
代码语言:javascript
复制
private void pluginElement(XNode parent) throws Exception {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
        String interceptor = child.getStringAttribute("interceptor");
        Properties properties = child.getChildrenAsProperties();
        Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).getDeclaredConstructor().newInstance();
        interceptorInstance.setProperties(properties);
        configuration.addInterceptor(interceptorInstance);
      }
    }
  }

pluginElement干了几件事情:

  • 创建Interceptor实例
  • 设置实例的属性变量
  • 添加到Configuration的interceptorChain拦截器链中

mybatis的插件是通过动态代理实现的,那肯定要生成代理对象,生成的逻辑就是前面提到的pluginAll方法,比如对于Executor生成代理对象就是,

代码语言:javascript
复制
executor = (Executor) interceptorChain.pluginAll(executor);

接着看pluginAll方法,

代码语言:javascript
复制
/**
   * 该方法会遍历用户定义的插件实现类(Interceptor),并调用Interceptor的plugin方法,对target进行插件化处理,
   * 即我们在实现自定义的Interceptor方法时,在plugin中需要根据自己的逻辑,对目标对象进行包装(代理),创建代理对象,
   * 那我们就可以在该方法中使用Plugin#wrap来创建代理类。
   */
  public Object pluginAll(Object target) {
    for (Interceptor interceptor : interceptors) {
      target = interceptor.plugin(target);
    }
    return target;
  }

这里遍历所有我们定义的拦截器,调用拦截器的plugin方法生成代理对象。有人可能有疑问:如果有多个拦截器,target不是被覆盖了吗?

其实不会,所以如果有多个拦截器的话,生成的代理对象会被另一个代理对象代理,从而形成一个代理链条,执行的时候,依次执行所有拦截器的拦截逻辑代码。

plugin方法是接口Interceptor的默认实现类,

代码语言:javascript
复制
default Object plugin(Object target) {
    return Plugin.wrap(target, this);
  }

然后进入org.apache.ibatis.plugin.Plugin#wrap

代码语言:javascript
复制
public static Object wrap(Object target, Interceptor interceptor) {
    Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
    Class<?> type = target.getClass();
    Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
    if (interfaces.length > 0) {
      return Proxy.newProxyInstance(
          type.getClassLoader(),
          interfaces,
          new Plugin(target, interceptor, signatureMap));
    }
    return target;
  }

首先是获取我们自己实现的Interceptor的方法签名映射表。然后获取需要代理的对象的Class上声明的所有接口。比如如果我们wrap的是Executor,就是Executor的所有接口。然后就是最关键的一步,用Proxy类创建一个代理对象(newProxyInstance)。

注意,newProxyInstance方法的第三个参数,接收的是一个InvocationHandler对象,表示的是当动态代理对象调用方法的时候会关联到哪一个InvocationHandler对象上,并最终由其调用。

我们这里传入的是Plugin类,故在动态运行过程中会执行Plugin的invoker方法。

如果对这一段不是很理解,建议先了解下java动态代理的原理。 java动态代理机制中有两个重要的角色:InvocationHandler(接口)和Proxy(类),这个是背景知识需要掌握的。

我们在深入看下上面的getSignatureMap方法,

代码语言:javascript
复制
private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {
    //从Interceptor的类上获取Intercepts注解,说明我们自定义拦截器需要带注解
    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<>();
    //解析Interceptor的values属性(Signature[])数组,存入HashMap, Set< Method>>
    for (Signature sig : sigs) {
      Set<Method> methods = MapUtil.computeIfAbsent(signatureMap, sig.type(), k -> new HashSet<>());
      try {
        Method method = sig.type().getMethod(sig.method(), sig.args());
        methods.add(method);
      } catch (NoSuchMethodException e) {
        throw new PluginException("Could not find method on " + sig.type() + " named " + sig.method() + ". Cause: " + e, e);
      }
    }
    return signatureMap;
  }

首先需要从Interceptor的类上获取Intercepts注解,说明我们自定义拦截器需要带注解,比如PageHelper插件的定义如下:

代码语言:javascript
复制
@Intercepts(
        {
                @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
                @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
        }
)
public class PageInterceptor implements Interceptor {
    private volatile Dialect dialect;
    private String countSuffix = "_COUNT";
    ...

所以我们可以知道,getSignatureMap其实就是拿到我们自定义拦截器声明需要拦截的类以及类对应的方法。

前面说过,当我们调用代理对象时,最终会执行Plugin类的invoker方法,我们看下Plugin的invoker方法,

代码语言:javascript
复制
@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)) {
        //如果需被拦截的方法集合包含当前执行的方法,则执行拦截器的intercept方法
        return interceptor.intercept(new Invocation(target, method, args));
      }
      //如果不是,则直接调用目标方法的Invoke方法。
      return method.invoke(target, args);
    } catch (Exception e) {
      throw ExceptionUtil.unwrapThrowable(e);
    }
  }

Interceptor接口的intercept方法就是我们自定义拦截器需要实现的逻辑,其参数为Invocation,可从Invocation参数中拿到执行方法的对象,方法,方法参数,比如我们可以从statementHandler拿到SQL语句,实现自己的特殊逻辑。

在该方法的结束需要调用invocation#proceed()方法,进行拦截器链的传播。


参考:

  • https://www.cnblogs.com/chenpi/p/10498921.html
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-10-03,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 犀牛的技术笔记 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 引言
  • 正文
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档