mybatis 源码 | mybatis 插件及动态代理的使用

本贴最后更新于 1528 天前,其中的信息可能已经时移俗易

开头说两句

Java 基础 Demo 站: https://www.javastudy.cloud
Java 中高级开发博客: https://www.lixiang.red
Java 学习公众号: java 技术大本营
java_subscribe

学习背景

​ 最近公司在做一些数据库安全方面的事情,如数据库中不能存手机号明文,不能存身份证号明文, 但是项目已经进行了好几个月了, 这时候在应用层面去改显然不太现实, 所以就有了 Mybatis 的自定义插件就出场了!

插件知识点总述

  1. mybatis 的插件,使用拦截器链的方式调用,其代码抽象如下所示 org.apache.ibatis.plugin.InterceptorChain

    public Object pluginAll(Object target) {
      for (Interceptor interceptor : interceptors) {
        target = interceptor.plugin(target);
      }
      return target;
    }
    

    不停的把 target 传入到插件的 plugin 方法中, 让插件对参数/返回值/语句/执行器做修改,如下图所示

    4Z3WRd

  2. 使用动态代理的方式调用插件功能

    这是一个通用的思路,在想对一个原有的方法/功能进行加强的时候,首要的思路就是使用代理, 然后采用如下的代码格式来加强

    beforeInvoke();//在调用之前加强
    proxy.invoke();//原来的逻辑
    afterInvoke();// 在调用之后加强
    

    mybatis 的动态代理也是如此,关键代码如下 org.apache.ibatis.plugin.Plugin

    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);
    
  3. 可以单独拿出来脱离 mybatis 框架使用,小刀觉得, 这一点才是 Mybatis 插件的精华所在

    这一点很关键,不要被 Mybatis 框架给局限了,如下面的测试类,还可以使用 Mybatis 的插件去拦截 HashMap 的 get 方法,这样就可以提炼一个小型的 AOP 了.

插件的使用及原理

在这里我们用 Mybatis 的官方测试代码为例:

/**
 *单元测试类
 */
@Test
  void mapPluginShouldInterceptGet() {
    Map map = new HashMap();
    map = (Map) new AlwaysMapPlugin().plugin(map);
    assertEquals("Always", map.get("Anything"));
  }
/**
 *插件类,我们在自己的项目中也应该建这样的类
 */
  @Intercepts({@Signature(type = Map.class, method = "get", args = {Object.class})})
  public static class AlwaysMapPlugin implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) {
      return "Always";
    }
  }

这份代码中要注意以下几个地方

  • @Intercepts({@Signature(type = Map.class, method = "get", args = {Object.class})})

    和工具封装的思路类似,把描述和实现分开,这个注解就是描述性信息,描述我们要去拦截哪个类的哪个方法, 这个方法有什么参数,这样去唯一定位到那个方法

  • implements Interceptor

    为什么实现接口要单独提出来说呢, 我们点进去这个接口看一下, 发现有两个 default 方法, 其中 plugin 是 Mybatis 插件中的核心

    default Object plugin(Object target) {
        return Plugin.wrap(target, this);
      }
    

    我们在测试类中可以看到 map = (Map) new AlwaysMapPlugin().plugin(map); 调用了 plugin 方法之后,就可以强转成原来的类使用,这时候就会调用加强的方法, 所以这里的 plugin 方法应该就是生成动态代理的方法,我们跟踪进 Plugin.wrap 这个方法里面看一下,如下所示:

    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;
      }
    
  • getAllInterfaces 上面源码中的这个方法要注意

    我们都知道 jdk 的动态代理的前提是对接口做加强. 现有接口 A, 类 B 实现 A , 我们要加强 B, 动态代理会为 A 建一个代理类 C,然后把 B 的实例 b 传入 C 中做 target . 然后调用 C 中的同名方法, 来实现看起来调的都是同一个方法,而且功能也得到了加强. 所以 getAllInterfaces 中有一个很重要的逻辑,就是要判断是否是接口

    for (Class<?> c : type.getInterfaces()) {
            if (signatureMap.containsKey(c)) {
              interfaces.add(c);
            }
          }
    

    如上所示: 只有 signatureMap 中包含这个接口的时候, 动态代理的参数 interfaces 中才会加入这个接口. 同样也就是说我们的 @Intercepts 这个注解中的 type,只能是接口类型, 这个一定要注意, 在 mybatis 中使用还没什么,单独提出来的时候就容易出错.

  • public Object intercept 这个是开发人员自己实现的逻辑

    在这里要注意传入的参数 new Invocation(target, method, args),我们点击去 Invocation 的代码中可以看到, proceed 方法可以执行原有的逻辑

    public Object proceed() throws InvocationTargetException, IllegalAccessException {
        return method.invoke(target, args);
      }
    

    因为在重写的 intercept 方法中,可以调用:invocation.proceed() 来获取原来的逻辑的返回值,然后对返回值做修改,

mybatis 插件在源码中的调用流程

这一段是为了大家方便在源码中查看插件是从哪里开始,然后到哪里执行.对整体有个印象

  1. 从 xml 中读取插件配置,并加入到插件链中

    org.apache.ibatis.builder.xml.XMLConfigBuilder.parseConfiguration
    
    pluginElement(root.evalNode("plugins"));
    
  2. 在获取 ParameterHandler/ResultSetHandler/StatementHandler/Executor 的时候调用插件链

    org.apache.ibatis.session.Configuration
    
    public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
        ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
      // 在这里调用
        parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
        return parameterHandler;
      }
    
    
  3. pluginAll() 方法中如上所述生成代理对象,把我们自定义的加强逻辑给加上去. 然后在执行 Mybatis 的 mapper 的时候,就可以走我们插件里面逻辑了

总结

MyBatis 源码并不难,整理好逻辑好一步步的跟踪下去就可以了,建议阅读本文时把 mybatis 源码也打开,跟着一起看,紧紧抓住动态代理的实现就肯定会把这个弄清的

  • MyBatis

    MyBatis 本是 Apache 软件基金会 的一个开源项目 iBatis,2010 年这个项目由 Apache 软件基金会迁移到了 google code,并且改名为 MyBatis ,2013 年 11 月再次迁移到了 GitHub。

    170 引用 • 414 回帖 • 441 关注

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...