Mybatis的插件设计源码分析

在这里插入图片描述

Mybatis的插件设计你知道多少?

本文主要分为两部分,第一部分我们看插件设计原理和如何从 Mybatis 中学习设计插件,第二部分我们学习如何开发Mybatis插件。

一、插件设计原理

Mybatis 中的插件都是通过代理方式来实现的,通过拦截执行器中指定的方法来达到改变核心执行代码的方式。举一个列子,查询方法核心都是通过 Executor来进行sql执行的。那么我们就可以通过拦截下面的方法来改变核心代码。基本原理就是这样,下面我们在来看 Mybatis 是如何处理插件。

public interface Executor {

  ResultHandler NO_RESULT_HANDLER = null;

  int update(MappedStatement ms, Object parameter) throws SQLException;

  <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql) throws SQLException;

  <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException;

  <E> Cursor<E> queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException;
  ...
}
在这里插入图片描述
名称 类型 描述
Interceptor 接口 插件都需要实现的接口,封装代理执行方法及参数信息
InterceptorChain 拦截链
InvocationHandler 接口 JDK代理的接口,凡是JDK中的代理都要实现该接口
@Intercepts 注解 用于声明要代理和 @Signature 配合使用
@Signature 注解 用于声明要代理拦截的方法
Plugin 代理的具体生成类

1. Interceptor

插件都需要实现的接口,封装代理执行方法及参数信息

public interface Interceptor {
    // 执行方法体的封装,所有的拦截方法逻辑都在这里面写。
  Object intercept(Invocation invocation) throws Throwable;
    // 如果要代理,就用Plugin.wrap(...),如果不代理就原样返回
  Object plugin(Object target);
    // 可以添加配置,主要是xml配置时候可以从xml中读取配置信息到拦截器里面自己解析
  void setProperties(Properties properties);
}

2. InterceptorChain

拦截链,为什么需要拦截链,假如我们要对A进行代理, 具体的代理类有B和C。 我们要同时将B和C的逻辑都放到代理类里面,那我们会首先将A和B生成代理类,然后在前面生成代理的基础上将C和前面生成的代理类在生成一个代理对象。这个类就是要做这件事 pluginAll

public class InterceptorChain {

  private final List<Interceptor> interceptors = new ArrayList<Interceptor>();
  
  // 这里target就是A,而List中的Interceptor就相当于B和C,通过循环方式生成统一代理类
  public Object pluginAll(Object target) {
    for (Interceptor interceptor : interceptors) {
      //1. 是否需要代理,需要代理生成代理类放回,不需要原样返回。通过for循环的方式将所有对应的插件整合成一个代理对象
      target = interceptor.plugin(target);
    }
    return target;
  }
  ...
}

3. InvocationHandler

JDK代理的接口,凡是JDK中的代理都要实现该接口。这个比较基础,如果这个不清楚,那么代理就看不懂了。所以就不说了。

public interface InvocationHandler {
      public Object invoke(Object proxy, Method method, Object[] args)
        throws Throwable;
}

4. @Intercepts@Signature

这两个注解是配合使用的,用于指定要代理的类和方法。前面①说了,插件的核心逻辑是拦截执行器的方法,那么这里我们看下如何声明要拦截的类和方法。我们看一下分页插件如何声明拦截。

Signaturetype 就是要拦截的类, method 要拦截的方法, args 要拦截的方法的入参(因为有相同的方法,所以要指定拦截的方法和方法参数)

@Intercepts(@Signature(type = Executor.class, method = "query", args = { MappedStatement.class, Object.class,
        RowBounds.class, ResultHandler.class }))
public class MybatisPagerPlugin implements Interceptor {
}

args 要拦截的方法的入参(因为有相同的方法,所以要指定拦截的方法和方法参数)
比如 Executor 中就有2个 query 方法。所以要通过args来确定要拦截哪一个。

在这里插入图片描述

Mybatis这种插件管理模式, 在 Mybatis 的架构中, 是有指定的,并不是说可以拦截任何类的任何方法,。它具体可以拦截什么类及方法,我们可以通过阅读官方文档 查看。

  • Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
  • ParameterHandler (getParameterObject, setParameters)
  • ResultSetHandler (handleResultSets, handleOutputParameters)
  • StatementHandler (prepare, parameterize, batch, update, query)

但是这种插件管理模式我们项目中也是可以用的。比如看下面例子。

public class Test {
    public static void main(String[] args) {
        InterceptorChain chain = new InterceptorChain();
        PrintInterceptor printInterceptor = new PrintInterceptor();
        Properties properties = new Properties();
        properties.setProperty("name","https://blog.springlearn.cn");
        printInterceptor.setProperties(properties);
        chain.addInterceptor(printInterceptor);
        Animal person = (Animal) chain.pluginAll(new Person());
        String nihao = person.say("nihao");
        System.out.println(nihao);
    }

    public interface Animal{
        String say(String message);
        String say(String name, String message);
    }

    public static class Person implements Animal {
        public String say(String message) {
            return message;
        }

        public String say(String name, String message) {
            return name + " say: " + message;
        }
    }

    @Intercepts(@Signature(type = Animal.class, method = "say", args = {String.class}))
    public static class PrintInterceptor implements Interceptor {
        private String name;

        @Override
        public Object intercept(Invocation invocation) throws Throwable {
            System.out.println(name + ": before print ...");
            Object proceed = invocation.proceed();
            System.out.println(name + ": after print ...");
            return proceed;
        }

        @Override
        public Object plugin(Object target) {
            if (target instanceof Person) {
                return Plugin.wrap(target, this);
            }
            return target;
        }

        @Override
        public void setProperties(Properties properties) {
            this.name = properties.getProperty("name");
        }
    }
}

5. Plugin

代理的具体生成类,解析 @Intercepts@Signature 注解生成代理。

我们看几个重要的方法。

方法名 处理逻辑
getSignatureMap 解析@Intercepts和@Signature,找到要拦截的方法
getAllInterfaces 找到代理类的接口,jdk代理必须要有接口
invoke 是否需要拦截判断
public class Plugin implements InvocationHandler {
  
  //解析@Intercepts和@Signature找到要拦截的方法
  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<Class<?>, Set<Method>>();
    for (Signature sig : sigs) {
      Set<Method> methods = signatureMap.get(sig.type());
      if (methods == null) {
        methods = new HashSet<Method>();
        signatureMap.put(sig.type(), methods);
      }
      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;
  }
  
  //因为是jdk代理所以必须要有接口,如果没有接口,就不会生成代理
  private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) {
    Set<Class<?>> interfaces = new HashSet<Class<?>>();
    while (type != null) {
      for (Class<?> c : type.getInterfaces()) {
        if (signatureMap.containsKey(c)) {
          interfaces.add(c);
        }
      }
      type = type.getSuperclass();
    }
    return interfaces.toArray(new Class<?>[interfaces.size()]);
  }
  
  @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);
    }
  }
}

6. 总结

以上就是本篇文章的第一部分,主要讲 "插件设计原理和如何从 Mybatis 中学习设计插件“

原理: 代理 ,并通过 @Intercepts@Signature 配合指定要代理的方法。 注意Mybatis中那些类能指定是有限制的哦。

在这里插入图片描述

我们可以通过阅读官方文档 查看。

  • Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
  • ParameterHandler (getParameterObject, setParameters)
  • ResultSetHandler (handleResultSets, handleOutputParameters)
  • StatementHandler (prepare, parameterize, batch, update, query)

Mybatis 的插件模式,我们在项目中可以直接引入使用。可以参考上面的例子。

二、如何开发Mybatis插件代码

如何开发 Mybatis 插件,首先要知道原理, Mybatis 的原理前面就说了就是代理核心类的核心方法。前面我们也知道如何定义一个插件了。即就是用 @Intercepts@Signature 来声明要拦截的类和方法。 但是知道这些只能说会定义插件了,具体插件代码怎么写。我们要在看下 Mybatis 官方限制的那几个类都有什么能力。

在这里插入图片描述

图片描述的不是很具体,但是大概意思是这样。 下面会一一简述。

1. Executor

public interface Executor {

  int update(MappedStatement ms, Object parameter) throws SQLException;

  <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql) throws SQLException;

}

数据库操作的第一步就是先调用 Executor , 如果要对sql语句进行增强 ,或者说是所有操作都进行增强都可以再这个里面处理。

2. ParameterHandler

sql入参会在这里被解析并进行操作,哎呀,这么说真的太抽象了。举例来说

public interface UserMapper {

    @Insert("insert into bbs_role (role_id,role_name,created_date,updated_date,created_by,updated_by) values(#{user" +
            ".roleId}," +
            "#{user.roleName},#{user.createdDate},#{user.updatedDate},#{user.createdBy},#{user.updatedBy})")
    Integer insert(@Param("user") User user);
}

insert 方法中的user对象,如何填充到 sql 中,就是在 ParameterHandler 里面完成的。

  1. 第一步将sql中占位符替换成 ? 符号, 然后解析参数类型到 ParameterMapping

    在这里插入图片描述

    最终这些信息都会在 BoundSql 中保存。 总的来说 Sql信息(包括入参的信息)都会放在 BoundSql 中保存。 这里我们认识了一个在ORM框架中非常重要的一个类
    BoundSql 如果想动态的修改sql就要跟着这个类的步伐。

  2. 将已经解析好的sql提交给 PreparedStatement 进行处理。
    ParameterHandler 重要的一步就是将 BoundSql 里面的sql及入参的放到 PreparedStatement 里面进行数据查询或者其他操作。 PreparedStatement 不解释了,学JDBC的时候老师应该都讲过了。

如果要对sql到PreparedStatement的过程进行增强就可以代理整个类。

3. StatementHandler

在这里插入图片描述
在这里插入图片描述

代理 StatementHandler 能做什么?

前面 ParameterHandler 已经可以将Sql信息写入到 Statement 中,但是调用的逻辑就在 StatementHandler里面来处理了。如果要对这部分代码做处理就可以拦截该方法。

在这里插入图片描述

4. ResultSetHandler

从名字就知道这个是对数据库查询后的记过进行处理的一个类。就是将jdbc的API返回数据转换成方法签名中的返回值。

public interface UserMapper {
    @Select("select * from bbs_role")
    List<User> query();
}

这里就是将 Statement 返回值转换成 List<User>

以上就是Mybatis给我们提供插件增强的地方,以及每个地方要做的事情

但是到这里真的会写插件了吗? 我们还必须要参与实践。如果我们要做一个功能将数据库的sql信息打印出来,应该知道在哪里处理了吧,只要获取BoundSql对象打印sql即可。如果我们要写分页那就是对sql后面加上分页的语法,这些说起来简单,其实并不简单,因为 Mybatis 提供对很多数据库的支持, 每个数据库的语法可能还不一样,所以在写插件时候要考虑的东西还是很多的, 如果我们不需要写插件,也没兴趣做开源项目其实了解到这里已经可以了。

但是如果感兴趣的话可以关注我哦!

image

感谢您的阅读,本文由 程序猿升级课 版权所有。如若转载,请注明出处:程序猿升级课(https://blog.springlearn.cn/

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容