MyBatis7:MyBatis插件及示例----打印每条SQL语句及其执行时间

Posted 五月的仓颉

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了MyBatis7:MyBatis插件及示例----打印每条SQL语句及其执行时间相关的知识,希望对你有一定的参考价值。

Plugins

摘一段来自MyBatis官方文档的文字。

MyBatis允许你在某一点拦截已映射语句执行的调用。默认情况下,MyBatis允许使用插件来拦截方法调用

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

这些类中方法的详情可以通过查看每个方法的签名来发现,而且它们的源代码存在于MyBatis发行包中。你应该理解你所覆盖方法的行为,假设你所做的要比监视调用要多。如果你尝试修改或覆盖一个给定的方法,你可能会打破MyBatis的核心。这是低层次的类和方法,要谨慎使用插件。

 

插件示例:打印每条SQL语句及其执行时间

以下通过代码来演示一下如何使用MyBatis的插件,要演示的场景是:打印每条真正执行的SQL语句及其执行的时间。这是一个非常有用的需求,MyBatis本身的日志可以记录SQL,但是有以下几个问题:

  1. MyBatis日志打印出来的SQL日志,参数都被占位符"?"替换,无法知道真正执行的SQL语句是什么
  2. MyBatis日志打印出来的SQL日志,有大量的换行符,通常一句SQL语句要通过十几行显示,阅读体验非常差
  3. 无法记录SQL执行时间,有SQL执行时间就可以精准定位到执行时间比较慢的SQL

写MyBatis插件非常简单,只需要实现Interceptor接口即可,我这里将我的Interceptor命名为SqlCostInterceptor:

  1 /**
  2  * Sql执行时间记录拦截器 
  3  */
  4 @Intercepts({@Signature(type = StatementHandler.class, method = "query", args = {Statement.class, ResultHandler.class}),
  5     @Signature(type = StatementHandler.class, method = "update", args = {Statement.class})})
  6 public class SqlCostInterceptor implements Interceptor {
  7 
  8     @Override
  9     public Object intercept(Invocation invocation) throws Throwable {
 10         Object target = invocation.getTarget();
 11         if (target instanceof StatementHandler) {
 12             long startTime = System.currentTimeMillis();
 13             StatementHandler statementHandler = (StatementHandler) target;
 14             try {
 15                 return invocation.proceed();
 16             } finally {
 17                 long endTime = System.currentTimeMillis();
 18                 long sqlCost = endTime - startTime;
 19                 
 20                 BoundSql boundSql = statementHandler.getBoundSql();
 21                 String sql = boundSql.getSql();
 22                 Object parameterObject = boundSql.getParameterObject();
 23                 List<ParameterMapping> parameterMappingList = boundSql.getParameterMappings();
 24                 
 25                 // 格式化Sql语句,去除换行符,替换参数
 26                 sql = formatSql(sql, parameterObject, parameterMappingList);
 27                 
 28                 System.out.println("SQL:[" + sql + "]执行耗时[" + sqlCost + "ms]");
 29             }
 30         }
 31         
 32         return invocation.proceed();
 33     }
 34 
 35     @Override
 36     public Object plugin(Object target) {
 37         return Plugin.wrap(target, this);
 38     }
 39 
 40     @Override
 41     public void setProperties(Properties properties) {
 42         
 43     }
 44     
 45     @SuppressWarnings("unchecked")
 46     private String formatSql(String sql, Object parameterObject, List<ParameterMapping> parameterMappingList) {
 47         sql = sql.replace("\n", "").replace("\t", "").replace("  ", " ").replace("( ", "(").replace(" )", ")");
 48         // 出异常时返回没有替换过占位符的sql
 49         String sqlWithoutReplacePlaceholder = sql;
 50         
 51         try {
 52             if (parameterMappingList != null) {
 53                 Class<?> parameterObjectClass = parameterObject.getClass();
 54 
 55                 if (isMap(parameterObjectClass)) {
 56                     Map<String, Object> paramMap = (Map<String, Object>) parameterObject;
 57                     for (ParameterMapping parameterMapping : parameterMappingList) {
 58                         String propertyName = parameterMapping.getProperty();
 59                         Object propertyValue = paramMap.get(propertyName);
 60                         if (propertyValue != null) {
 61                             if (propertyValue.getClass().isAssignableFrom(String.class)) {
 62                                 propertyValue = "\"" + propertyValue + "\"";
 63                             }
 64 
 65                             sql = sql.replaceFirst("\\?", propertyValue.toString());
 66                         }
 67                     }
 68                 } else {
 69                     for (ParameterMapping parameterMapping : parameterMappingList) {
 70                         String propertyName = parameterObjectClass.isPrimitive() ? "value"
 71                                 : parameterMapping.getProperty();
 72                         Field field = parameterObjectClass.getDeclaredField(propertyName);
 73                         field.setAccessible(true);
 74                         String propertyValue = String.valueOf(field.get(parameterObject));
 75                         if (propertyValue.getClass().isAssignableFrom(String.class)) {
 76                             propertyValue = "\"" + propertyValue + "\"";
 77                         }
 78 
 79                         sql = sql.replaceFirst("\\?", propertyValue);
 80                     }
 81                 }
 82             } 
 83         } catch (Exception e) {
 84             return sqlWithoutReplacePlaceholder;
 85         }
 86         
 87         return sql;
 88     }
 89     
 90     private boolean isMap(Class<?> parameterObjectClass) {
 91         Class<?>[] classes = parameterObjectClass.getInterfaces();
 92         for (Class<?> clazz : classes) {
 93             if (clazz.isAssignableFrom(Map.class)) {
 94                 return true;
 95             }
 96         }
 97         
 98         return false;
 99     }
100     
101 }

分析一下这段代码。

首先是注解@Intercepts与@Signature,这两个注解是必须的,因为Plugin的wrap方法会取这两个注解里面参数。@Intercepts中可以定义多个@Signature,一个@Signature表示符合如下条件的方法才会被拦截:

  • 接口必须是type定义的类型
  • 方法名必须和method一致
  • 方法形参的Class类型必须和args定义Class类型顺序一致

接着的一个问题是:有四个接口可以拦截,为什么使用StatementHandler去拦截?根据名字来看ParameterHandler和ResultSetHandler,前者处理参数,后者处理结果是不可能使用的,剩下的就是Executor和StatementHandler了。拦截StatementHandler的原因是而不是用Executor的原因是:

  • Executor的update与query方法可能用到MyBatis的一二级缓存从而导致统计的并不是真正的SQL执行时间
  • StatementHandler的update与query方法无论如何都会统计到PreparedStatement的execute方法执行时间,尽管也有一定误差(误差主要来自会将处理结果的时间也算上),但是相差不大

接着讲一下setProperties方法,可以将一些配置属性配置在<plugin></plugin>的子标签<property />中,所有的配置属性会在形参Properties中,setProperties方法可以拿到配置的属性进行需要的处理。

接着讲一下plugin方法,这里是为目标接口生成代理,不需要也没必要自己去写生成代理的方法,MyBatis的Plugin类已经为我们提供了wrap方法(当然如果自己有自己的逻辑也可以在Plugin.wrap方法前后加入,但是最终一定要使用Plugin.wrap方法生成代理),看一下该方法的实现:

 1 public static Object wrap(Object target, Interceptor interceptor) {
 2     Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
 3     Class<?> type = target.getClass();
 4     Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
 5     if (interfaces.length > 0) {
 6       return Proxy.newProxyInstance(
 7           type.getClassLoader(),
 8           interfaces,
 9           new Plugin(target, interceptor, signatureMap));
10     }
11     return target;
12 }

因为这里的target一定是一个接口,因此可以放心使用JDK本身提供的Proxy类,这里相当于就是如果该接口满足方法签名那么就为之生成一个代理。

最后就是intercept方法了,这里就是拦截器的核心代码了,方法的逻辑我就不解释了,可以自己看一下,唯一要注意的一点就是无论如何最终一定要返回invocation.proceed(),保证拦截器的层层调用。

 

xml文件配置即效果演示

写完了插件,只需要在config.xml文件中进行一次配置即可,非常简单:

 1 <plugins>
 2     <plugin interceptor="org.xrq.mybatis.plugin.SqlCostInterceptor" />
 3 </plugins>

这里每个<plugin>子标签代表一个插件,interceptor表示拦截器的完整路径,每个人的不同。

有了类和这段配置,就可以使用SqlCostInterceptor了,SqlCostInterceptor是通用的,但是每个人的CRUD是不同的,我打印一下我这里CRUD执行的结果:

SQL:[insert into mail(id, create_time, modify_time, web_id, mail, use_for) values(null, now(), now(), "1", "[email protected]", "个人使用");]执行耗时[1ms]
SQL:[insert into mail(id, create_time, modify_time, web_id, mail, use_for) values(null, now(), now(), "2", "[email protected]", "企业使用");]执行耗时[1ms]
SQL:[insert into mail(id, create_time, modify_time, web_id, mail, use_for) values(null, now(), now(), "3", "[email protected]", "注册账号使用");]执行耗时[0ms]

看到打印了完整的SQl语句以及SQL语句执行时间。

不过要说明一点,这个插件只是一个简单的Demo,我并没有完整测试过,应该是无法覆盖所有场景的,所以如果想用这段代码片段打印真正的SQL及其执行时间的朋友,还需要在这个基础上做修改,不过即使不改代码,这个插件起到美化SQL的作用,去除一些换行符还是没问题的。

至于MyBatis插件的实现原理,会在我【MyBatis源码分析】系列文章中详细解读。

以上是关于MyBatis7:MyBatis插件及示例----打印每条SQL语句及其执行时间的主要内容,如果未能解决你的问题,请参考以下文章

MyBatis插件及示例----打印每条SQL语句及其执行时间

MyBatis常用操作示例

Mybatis: 插件及分页

深入浅出MyBatis:MyBatis插件及开发过程

Mybatis插件:自定义插件分页及通用Mapper

mybatis插件机制原理