乐趣区

关于mybatis-plus:假装是小白之重学MyBatis二

前言

本篇咱们来介绍 MyBatis 插件的开发,这个也是来源于我之前的一个面试经验,面试官为我如何统计 Dao 层的慢 SQL,我过后的答复是借助于 Spring 的 AOP 机制,拦挡 Dao 层所有的办法,但面试官又问,这事实上不齐全是 SQL 的执行工夫,这其中还有其余代码的工夫,问我还有其余思路吗?我想了想说没有,面试官接着问,有接触过 MyBatis 插件的开发吗?我说没接触过。但前面也给我过了,我认为这个问题是有价值的问题,所以也放在了我的学习打算中。

看本篇之前倡议先看:

  • 《代理模式 -AOP 绪论》
  • 《伪装是小白之重学 MyBatis(一)》

如果有人问下面两篇文章在哪里能够找的到,能够去掘金或者思否翻翻,目前公众号还没有,预计年中会将三个平台的文章对立一下。

概述

翻阅官网文档的话,MyBatis 并没有给处插件的具体定义,但基本上还是拦截器,MyBatis 的插件就是一些可能拦挡某些 MyBats 外围组件办法,加强性能的拦截器。官网文档中列出了四种可供加强的切入点:

  • Executor

执行 SQL 的外围组件。拦挡 Executor 意味着要烦扰或加强底层执行的 CRUD 操作

  • ParameterHandler

拦挡该 ParameterHandler,意味着要烦扰 SQL 参数注入、读取的动作。

  • ResultSetHandler

拦挡该 ParameterHandler, 要烦扰 / 加强封装后果集的动作

  • StatementHandler

拦挡 StatementHandler,则意味着要烦扰 / 加强 Statement 的创立和执行的动作

当然还是从 HelloWorld 开始

要做 MyBatis 的插件,首先要实现 MyBatis 的 Interceptor 接口 , 留神类不要导错了,Interceptor 很热门,该类位于 org.apache.ibatis.plugin.Interceptor 下。实现该接口,MyBatis 会将该实现类当作 MyBatis 的拦截器,那拦挡哪些办法,该怎么指定呢? 通过@Intercepts 注解来实现,上面是应用示例:

@Intercepts(@Signature(type = Executor.class, method = "query",
        args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}))
public class MyBatisPluginDemo implements Interceptor {
    
    @Override
    public Object intercept(Invocation invocation) throws Throwable {System.out.println("into invocation ..........");
        System.out.println(invocation.getTarget());
        System.out.println(invocation.getMethod().getName());
        System.out.println(Arrays.toString(invocation.getArgs()));
        return invocation.proceed();}
}
@Intercepts 能够填多个 @Signature,@Signature 是办法签名,type 用于定位类,method 定位办法名,args 用于指定办法的参数类型。三者加在一起就能够定位到具体的办法。留神写完还须要将此插件注册到 MyBatis 的配置文件中, 让 MyBatis 加载该插件。

留神这个标签肯定要放在 environments 下面,MyBatis 严格限制住了标签的程序。

<plugins>
    <plugin interceptor="org.example.mybatis.MyBatisPluginDemo"></plugin>
</plugins>

咱们来看下执行后果:

性能剖析插件走起

那拦挡谁呢?目前也只有 Executor 和 StatementHandler 供咱们抉择,咱们自身是要看 SQL 耗时,Executor 离 SQL 执行还有些远,一层套一层才走到 SQL 执行,MyBatis 中标签的执行过程在《MyBatis 源码学习笔记(一) 初遇篇》曾经讲述过了,这里不再赘述,目前来看 StatementHandler 是离 SQL 最近的, 它的实现类就间接走到 JDBC 了,所以咱们拦挡 StatementHandler,那有的插入插了很多值,咱们要不要拦挡,当然也要拦挡, 咱们的插件办法如下:

@Intercepts({@Signature(type = StatementHandler.class, method = "query",
        args = {Statement.class, ResultHandler.class}), @Signature(type = StatementHandler.class,method =  "update" ,args = Statement.class)})
public class MyBatisSlowSqlPlugin implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {System.out.println("----- 开始进入性能剖析插件中 ----");
        long startTime = System.currentTimeMillis();
        Object result = invocation.proceed();
        long endTime = System.currentTimeMillis();
       // query 办法入参是 statement, 所以咱们能够将其转为 Statement
        if (endTime - startTime > 1000){ }
        return result;
    }
}

那对应的 SQL 该怎么拿?咱们还是到 StatementHandler 去看下:

咱们还是得通过 Statement 这个入参来拿, 咱们试试看, 你会发现在日志级别为 DEBUG 之上,会输入 SQL,像上面这样:

如果日志级别为 DEBUG 输入会是上面这样:

这是为什么呢?如果看过《MyBatis 源码学习笔记(一) 初遇篇》这篇的可能会想到,MyBatis 架构中的日志模块,为了接入日志框架,就会用到代理,那么这个必定就是代理类,咱们打断点来验证一下咱们的想法:

代理剖析

我本来的想法是 PreparedStatementLogger 的代理类,认真一想,感觉不对,感觉本人还是对代理模式理解不大透,于是我就又把之前的文章《代理模式 -AOP 绪论》看了一下,动静代理模式的指标:

  • 咱们有一批类,而后咱们想在不扭转它们的根底之上,加强它们, 咱们还心愿只着眼于编写加强指标对象代码的编写。
  • 咱们还心愿由程序来编写这些类,而不是由程序员来编写,因为太多了。

在《代理模式 -AOP 绪论》中咱们做的是很简略的代理:

public interface IRentHouse {void rentHouse();
    void study();}
public class RentHouse implements IRentHouse{
    @Override
    public void rentHouse() {System.out.println("sayHello.....");
    }
    @Override
    public void study() {System.out.println("say Study");
    }
}

咱们当初的需要是加强 IRentHouse 中的办法,用动态代理就是为 IRentHouse 再做一个实现类,相当于在 RentHouse 上再包装一层。但如果我有很多想加强的类呢,这样去包装,事实上对代码的侵入性是很大的。对于这种情况,咱们最终的抉择是动静代理,在运行时产生接口实现类的代理类,咱们最终产生代理对象的办法是:

/**
   * @param target 为须要加强的类
   * @return 返回的对象在调用接口中的任意办法都会走到 Lambda 回调中。*/
private static  Object getProxy(Object  target){Object proxy = Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), (proxy1, method, args) -> {System.out.println("办法开始执行..........");
            Object obj = method.invoke(target, args);
            System.out.println("办法执行完结..........");
            return obj;
        });
        return proxy;
  }

接下来咱们来看下 MyBatis 是怎么包装的,咱们还是从 PreparedStatementLogger 开始看:

InvocationHandler 是动静代理的接口,BaseJdbcLogger 这个先不关注。值得关注的是:

public static PreparedStatement newInstance(PreparedStatement stmt, Log statementLog, int queryStack) {InvocationHandler handler = new PreparedStatementLogger(stmt, statementLog, queryStack);
  ClassLoader cl = PreparedStatement.class.getClassLoader();
  return (PreparedStatement) Proxy.newProxyInstance(cl, new Class[]{PreparedStatement.class, CallableStatement.class}, handler);
}

可能有同学会问 newProxyInstance 为什么给了两个参数, 因为 CallableStatement 继承了 PreparedStatement。这里是一层,事实上还能点进去另外一层,在 ConnectionLogger 的回调中(ConnectionLogger 也实现了 InvocationHandler,所以这个也是个代理回调类),ConnectionLogger 的实例化在 BaseExecutor 这个类外面实现,如果你还能回顾 JDBC 产生 SQL 的话,过后的流程事实上是这样的:

    public static boolean execute(String sql, Object... param) throws Exception {
        boolean result = true;
        Connection connection = null;
        PreparedStatement preparedStatement = null;
        try {
            // 获取数据库连贯
            connection = getConnection();
            connection.setAutoCommit(false);
            preparedStatement = connection.prepareStatement(sql);
            // 设置参数 
            for (int i = 0; i < param.length; i++) {preparedStatement.setObject(i, param[i]);
                preparedStatement.addBatch();}
            preparedStatement.executeBatch();
            // 提交事务
            connection.commit();} catch (SQLException e) {e.printStackTrace();
            if (connection != null) {
                try {connection.rollback();
                } catch (SQLException ex) {ex.printStackTrace();
                    // 日志记录事务回滚失败
                    result = false;
                    return result;
                }
            }
            result = false;
        } finally {close(preparedStatement, connection);
        }
        return result;
    }

咱们来捋一下,ConnectionLogger 是读 Connection 的代理,然而 Connection 接口中有许多办法, 所以 ConnectionLogger 在回调的时候做了判断:

@Override
public Object invoke(Object proxy, Method method, Object[] params)
    throws Throwable {
  try {if (Object.class.equals(method.getDeclaringClass())) {return method.invoke(this, params);
    }
    if ("prepareStatement".equals(method.getName()) || "prepareCall".equals(method.getName())) {if (isDebugEnabled()) {debug("Preparing:" + removeExtraWhitespace((String) params[0]), true);
      }
      // Connection 的 prepareStatement 办法、prepareCall 会产生 PreparedStatement
      PreparedStatement stmt = (PreparedStatement) method.invoke(connection, params);
      // 而后 PreparedStatementLogger 产生的还是 stmt 的代理类
      // 咱们在 plugin 中拿到的就是  
      stmt = PreparedStatementLogger.newInstance(stmt, statementLog, queryStack);
      return stmt;
    } else if ("createStatement".equals(method.getName())) {Statement stmt = (Statement) method.invoke(connection, params);
      stmt = StatementLogger.newInstance(stmt, statementLog, queryStack);
      return stmt;
    } else {return method.invoke(connection, params);
    }
  } catch (Throwable t) {throw ExceptionUtil.unwrapThrowable(t);
  }
}

PreparedStatementLogger 是回调类,这个 PreparedStatementLogger 有对应的 Statement,咱们通过 Statement 就能够拿到对应的 SQL。那回调类和代理类是什么关系呢,咱们来看下 Proxy 类的大抵结构:

所以我最后的想法是 JDK 为咱们产生的类外面有回调类实例这个对象会有 InvocationHandler 成员变量,然而如果你用 getClass().getDeclaredField(“h”)去获取发现获取不到,那么代理类就没有这个回调类实例,那咱们钻研一下 getProxyClass0 这个办法:

private static Class<?> getProxyClass0(ClassLoader loader,
                                       Class<?>... interfaces) {if (interfaces.length > 65535) {throw new IllegalArgumentException("interface limit exceeded");
    }

    // If the proxy class defined by the given loader implementing
    // the given interfaces exists, this will simply return the cached copy;
    // otherwise, it will create the proxy class via the ProxyClassFactory
    // proxyClassCache 是 new WeakCache<>(new KeyFactory(), new ProxyClassFactory()) 的实例
    // 最终会调用 ProxyClassFactory 的 apply 办法。// 在 ProxyClassFactory 的 apply 办法中有 ProxyGenerator.generateProxyClass() 
    // 答案就在其中, 最初调用的是 ProxyGenerator 的 generateClassFile 办法
    // 中产生代理类时, 让代理类继承 Proxy 类。return proxyClassCache.get(loader, interfaces);
}

所以破案了,在 Proxy 里的 InvocationHandler 是 protected,所以咱们取变量该当这么取:

@Intercepts({@Signature(type = StatementHandler.class, method = "query",
        args = {Statement.class, ResultHandler.class}), @Signature(type = StatementHandler.class,method =  "update" ,args = Statement.class)})
public class MyBatisSlowSqlPlugin implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {System.out.println("----- 开始进入性能剖析插件中 ----");
        long startTime = System.currentTimeMillis();
        Object result = invocation.proceed();
        long endTime = System.currentTimeMillis();
       // query 办法入参是 statement, 所以咱们能够将其转为 Statement
        Statement statement = (Statement)invocation.getArgs()[0];
        if (Proxy.isProxyClass(statement.getClass())){Class<?> statementClass = statement.getClass().getSuperclass();
            Field targetField = statementClass.getDeclaredField("h");
            targetField.setAccessible(true);
            PreparedStatementLogger  loggerStatement  = (PreparedStatementLogger) targetField.get(statement);
            PreparedStatement preparedStatement = loggerStatement.getPreparedStatement();
            if (endTime - startTime > 1){System.out.println(preparedStatement.toString());
            }
        }else {if (endTime - startTime > 1){System.out.println(statement.toString());
            }
        }
        return result;
    }
}

最初输入如下:

然而这个插件还不是那么完满,就是这个慢 SQL 查问工夫了,咱们当初是写死的

这两个问题在 MyBatis 外面都能够失去解决,咱们能够看 Interceptor 这个接口:

public interface Interceptor {Object intercept(Invocation invocation) throws Throwable;
 
  default Object plugin(Object target) {return Plugin.wrap(target, this);
  }

  default void setProperties(Properties properties) {// NOP}
}

setProperties 用于从配置文件中取值, plugin 将以后插件退出,intercept 是真正加强办法。那下面的两个问题曾经被解决了:

  • 硬编码

首先在配置文件外面配置

 <plugins>
        <plugin interceptor="org.example.mybatis.MyBatisSlowSqlPlugin">
            <property name = "maxTolerate" value = "10"/>
        </plugin>
 </plugins>

而后重写:

@Override
public void setProperties(Properties properties) {
    //maxTolerate 是 MyBatisSlowSqlPlugin 的成员变量
    this.maxTolerate = Long.parseLong(properties.getProperty("maxTolerate"));
}

回顾一下 JDBC 咱们执行 SQl 事实上有两种形式:

  • Connection 中的 prepareStatement 办法
  • Connection 中的 createStatement

在 MyBatis 中这两种办法对应不同的 StatementType, 下面的 PreparedStatementLogger 对应 Connection 中的 prepareStatement 办法,如果说你在 MyBatis 中将语句申明为 Statement,则咱们的 SQL 监控语句就会出错,所以这里咱们还须要在独自适配一下 Statement 语句类型。

@Intercepts({@Signature(type = StatementHandler.class, method = "query",
        args = {Statement.class, ResultHandler.class}), @Signature(type = StatementHandler.class,method =  "update" ,args = Statement.class)})
public class MyBatisSlowSqlPlugin implements Interceptor {

    private  long  maxTolerate;

    @Override
    public Object intercept(Invocation invocation) throws Throwable {System.out.println("----- 开始进入性能剖析插件中 ----");
        long startTime = System.currentTimeMillis();
        Object result = invocation.proceed();
        SystemMetaObject
        long endTime = System.currentTimeMillis();
       // query 办法入参是 statement, 所以咱们能够将其转为 Statement
        Statement statement = (Statement)invocation.getArgs()[0];
        if (Proxy.isProxyClass(statement.getClass())){Class<?> statementClass = statement.getClass().getSuperclass();
            Field targetField = statementClass.getDeclaredField("h");
            targetField.setAccessible(true);
            Object object = targetField.get(statement);
            if (object instanceof PreparedStatementLogger) {PreparedStatementLogger  loggerStatement  = (PreparedStatementLogger) targetField.get(statement);
                PreparedStatement preparedStatement = loggerStatement.getPreparedStatement();
                if (endTime - startTime > maxTolerate){System.out.println(preparedStatement.toString());
                }
            }else {
                // target 是对应的语句处理器
                // 为什么不反射拿? Statement 对应的实现类未重写 toString 办法
                // 然而在 RoutingStatementHandler 中提供了 getBoundSql 办法
                RoutingStatementHandler handler = (RoutingStatementHandler) invocation.getTarget();
                BoundSql boundSql = handler.getBoundSql();
                if (endTime - startTime > maxTolerate){System.out.println(boundSql);
                }
            }
        }else {if (endTime - startTime > maxTolerate){System.out.println(statement.toString());
            }
        }
        return result;
    }

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

事实上 MyBatis 外面写好了反射工具类,这个就是 SystemMetaObject,用法示例如下:

@Intercepts({@Signature(type = StatementHandler.class, method = "query",
        args = {Statement.class, ResultHandler.class}), @Signature(type = StatementHandler.class,method =  "update" ,args = Statement.class)})
public class MyBatisSlowSqlPlugin implements Interceptor {

    private  long  maxTolerate;

    @Override
    public Object intercept(Invocation invocation) throws Throwable {System.out.println("----- 开始进入性能剖析插件中 ----");
        long startTime = System.currentTimeMillis();
        Object result = invocation.proceed();
        long endTime = System.currentTimeMillis();
       // query 办法入参是 statement, 所以咱们能够将其转为 Statement
        Statement statement = (Statement)invocation.getArgs()[0];
        MetaObject metaObject = SystemMetaObject.forObject(statement);
        if (Proxy.isProxyClass(statement.getClass())){Object object = metaObject.getValue("h");
            if (object instanceof PreparedStatementLogger) {PreparedStatementLogger  loggerStatement  = (PreparedStatementLogger) object;
                PreparedStatement preparedStatement = loggerStatement.getPreparedStatement();
                if (endTime - startTime > maxTolerate){System.out.println(preparedStatement.toString());
                }
            }else {
                // target 是对应的语句处理器
                // 为什么不反射拿? Statement 对应的实现类未重写 toString 办法
                // 然而在 RoutingStatementHandler 中提供了 getBoundSql 办法
                RoutingStatementHandler handler = (RoutingStatementHandler) invocation.getTarget();
                BoundSql boundSql = handler.getBoundSql();
                if (endTime - startTime > maxTolerate){System.out.println(boundSql);
                }
            }
        }else {if (endTime - startTime > maxTolerate){System.out.println(statement.toString());
            }
        }
        return result;
    }

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

那我有多个插件,如何指定程序呢? 在配置文件中指定,从上往下顺次执行

 <plugins>
        <plugin interceptor="org.example.mybatis.MyBatisSlowSqlPlugin01">
            <property name = "maxTolerate" value = "10"/>
        </plugin> 
     <plugin interceptor="org.example.mybatis.MyBatisSlowSqlPlugin02">
            <property name = "maxTolerate" value = "10"/>
        </plugin> 
</plugins>

如下面所配置执行程序就是 MyBatisSlowSqlPlugin01、MyBatisSlowSqlPlugin02。插件的几个办法执行程序呢

写在最初

感叹颇深,本来预计两个小时就能写完的,而后写了一下午,颇有种学海无涯的感觉。

参考资料

  • mybatis 拦截器插件实例 - 获取不带占位符的可间接执行的 sql B 站教学视频 https://www.bilibili.com/vide…
  • MyBatis 高级 https://www.bilibili.com/vide…
退出移动版