Mybatis的插件设计源码分析

举报
西魏陶渊明 发表于 2022/09/25 04:32:04 2022/09/25
【摘要】 在这里插入图片描述 Mybatis的插件设计你知道多少? 本文主要分为两部分,第一部分我们看插件设计原理和如何从 Mybatis 中学习设计插件,第二部分我们学习如何开发Mybatis插件。 一、插件设计原理 Mybatis 中的插件都是通过代理方式来实现的,通过...

4279695-3a843ab148159226
在这里插入图片描述

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;
  ...
}
4279695-9c9b18cf0b83674d
在这里插入图片描述
名称 类型 描述
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来确定要拦截哪一个。

4279695-ef2fa23579e9685f
在这里插入图片描述

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中那些类能指定是有限制的哦。

4279695-edbff849a264b762
在这里插入图片描述

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

  • 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 官方限制的那几个类都有什么能力。

4279695-cc3953888929e4eb
在这里插入图片描述

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

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

    4279695-72983dc751cbac0b
    在这里插入图片描述

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

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

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

3. StatementHandler

4279695-d35bb2457c95a4b2
在这里插入图片描述
4279695-ecc70296cca01b18
在这里插入图片描述

代理 StatementHandler 能做什么?

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

4279695-4820ea9fc3a1577d
在这里插入图片描述

4. ResultSetHandler

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

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

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

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

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

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

4279695-6e546481de7f92b6
image

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

文章来源: springlearn.blog.csdn.net,作者:西魏陶渊明,版权归原作者所有,如需转载,请联系作者。

原文链接:springlearn.blog.csdn.net/article/details/103695297

【版权声明】本文为华为云社区用户转载文章,如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。