mybatis xml 循环_Mybatis的插件设计源码分析

372cf470ded07533b61786de26d6ab13.png

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

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

一、插件设计原理

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

public interface Executor {  ResultHandler NO_RESULT_HANDLER = null;  int update(MappedStatement ms, Object parameter) throws SQLException;   List query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql) throws SQLException;   List query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException;   Cursor queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException;  ...}
fad355b62019d944b14749afda5bd009.png

名称类型描述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 interceptors = new ArrayList();  // 这里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

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

Signature 中 type 就是要拦截的类, 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来确定要拦截哪一个。

e4381b64a3e781e91656969e2228bdb8.png

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, Set> 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, Set> signatureMap = new HashMap, Set>();    for (Signature sig : sigs) {      Set methods = signatureMap.get(sig.type());      if (methods == null) {        methods = new HashSet();        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, Set> signatureMap) {    Set> interfaces = new HashSet>();    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 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中那些类能指定是有限制的哦。

a001110c66feec7f58e3dd87e8b96b62.png

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

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

7fa0f2c225ee93088d653ea97cb72421.png

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

1. Executor
public interface Executor {  int update(MappedStatement ms, Object parameter) throws SQLException;   List 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
a40f433eb709689788d3ebd2c679bbc2.png
33a4ebca1d4163d68ba56f04413746bc.png

代理 StatementHandler 能做什么?

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

7760c8ffb6b957318538ab980dbda37b.png
4. ResultSetHandler

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

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

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

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

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

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

47a53e6433e1cf3d1ca695c85273c95a.png

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值