使用MyBatisMyBatis Plus实现SQL日记打印与执行监控

打印 上一主题 下一主题

主题 1816|帖子 1816|积分 5448

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

x
使用MyBatis/MyBatis Plus实现SQL日记打印与执行监控

一、配景与代价

在开发过程中,SQL日记的完整输出对于调试和性能优化至关紧张。MyBatis默认的日记输出仅显示带占位符的SQL语句,无法直接看到现实参数值,且缺乏执行时间统计。本文将介绍两种实现方案:

  • 原生配置方案:通过日记框架直接输出基础SQL日记
  • 增强方案:使用MyBatis拦截器实现完整SQL打印和执行监控
二、原生配置方案(快速上手)

1. 日记框架配置(以Logback为例)

  1. <!-- logback-spring.xml -->
  2. <configuration>
  3.     <logger name="com.zaxxer.hikari" level="INFO"/>
  4.     <logger name="java.sql.Connection" level="INFO"/>
  5.     <logger name="java.sql.Statement" level="DEBUG"/>
  6.     <logger name="java.sql.PreparedStatement" level="DEBUG"/>
  7. </configuration>
复制代码
2. 输出示例

  1. DEBUG [main] - ==>  Preparing: SELECT * FROM user WHERE name = ?
  2. DEBUG [main] - ==> Parameters: John(String)
复制代码
3. 局限性



  • 参数值单独显示,无法直接拼接完整SQL
  • 缺乏执行耗时统计
  • 动态SQL处理不敷直观
三、增强方案:自界说拦截器实现

1. SQL美化与参数更换

  1. public class MybatisPlusAllSqlLog implements InnerInterceptor {
  2.     public static final Logger log = LoggerFactory.getLogger("sys-sql");
  3.     @Override
  4.     public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
  5.         logInfo(boundSql, ms, parameter);
  6.     }
  7.     @Override
  8.     public void beforeUpdate(Executor executor, MappedStatement ms, Object parameter) throws SQLException {
  9.         BoundSql boundSql = ms.getBoundSql(parameter);
  10.         logInfo(boundSql, ms, parameter);
  11.     }
  12.     private static void logInfo(BoundSql boundSql, MappedStatement ms, Object parameter) {
  13.         try {
  14.             log.info("parameter = " + parameter);
  15.             // 获取到节点的id,即sql语句的id
  16.             String sqlId = ms.getId();
  17.             log.info("sqlId = " + sqlId);
  18.             // 获取节点的配置
  19.             Configuration configuration = ms.getConfiguration();
  20.             // 获取到最终的sql语句
  21.             String sql = getSql(configuration, boundSql, sqlId);
  22.             log.info("完整的sql:{}", sql);
  23.         } catch (Exception e) {
  24.             log.error("异常:{}", e.getLocalizedMessage(), e);
  25.         }
  26.     }
  27.     // 封装了一下sql语句,使得结果返回完整xml路径下的sql语句节点id + sql语句
  28.     public static String getSql(Configuration configuration, BoundSql boundSql, String sqlId) {
  29.         return sqlId + ":" + showSql(configuration, boundSql);
  30.     }
  31.     // 进行?的替换
  32.     public static String showSql(Configuration configuration, BoundSql boundSql) {
  33.         // 获取参数
  34.         Object parameterObject = boundSql.getParameterObject();
  35.         List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
  36.         // sql语句中多个空格都用一个空格代替
  37.         String sql = boundSql.getSql().replaceAll("[\\s]+", " ");
  38.         if (!CollectionUtils.isEmpty(parameterMappings) && parameterObject != null) {
  39.             // 获取类型处理器注册器,类型处理器的功能是进行java类型和数据库类型的转换
  40.             TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
  41.             // 如果根据parameterObject.getClass()可以找到对应的类型,则替换
  42.             if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
  43.                 sql = sql.replaceFirst("\\?",
  44.                         Matcher.quoteReplacement(getParameterValue(parameterObject)));
  45.             } else {
  46.                 // MetaObject主要是封装了originalObject对象,提供了get和set的方法用于获取和设置originalObject的属性值,主要支持对JavaBean、Collection、Map三种类型对象的操作
  47.                 MetaObject metaObject = configuration.newMetaObject(parameterObject);
  48.                 for (ParameterMapping parameterMapping : parameterMappings) {
  49.                     String propertyName = parameterMapping.getProperty();
  50.                     if (metaObject.hasGetter(propertyName)) {
  51.                         Object obj = metaObject.getValue(propertyName);
  52.                         sql = sql.replaceFirst("\\?",
  53.                                 Matcher.quoteReplacement(getParameterValue(obj)));
  54.                     } else if (boundSql.hasAdditionalParameter(propertyName)) {
  55.                         // 该分支是动态sql
  56.                         Object obj = boundSql.getAdditionalParameter(propertyName);
  57.                         sql = sql.replaceFirst("\\?",
  58.                                 Matcher.quoteReplacement(getParameterValue(obj)));
  59.                     } else {
  60.                         // 打印出缺失,提醒该参数缺失并防止错位
  61.                         sql = sql.replaceFirst("\\?", "缺失");
  62.                     }
  63.                 }
  64.             }
  65.         }
  66.         return sql;
  67.     }
  68.     // 如果参数是String,则添加单引号, 如果是日期,则转换为时间格式器并加单引号; 对参数是null和不是null的情况作了处理
  69.     private static String getParameterValue(Object obj) {
  70.         String value;
  71.         if (obj instanceof String) {
  72.             value = "'" + obj.toString() + "'";
  73.         } else if (obj instanceof Date) {
  74.             DateFormat formatter = DateFormat.getDateTimeInstance(DateFormat.DEFAULT,
  75.                     DateFormat.DEFAULT, Locale.CHINA);
  76.             value = "'" + formatter.format(new Date()) + "'";
  77.         } else {
  78.             if (obj != null) {
  79.                 value = obj.toString();
  80.             } else {
  81.                 value = "";
  82.             }
  83.         }
  84.         return value;
  85.     }
  86. }
复制代码
2. 执行耗时监控

  1. @Intercepts({
  2.         @Signature(type = Executor.class, method = "update", args = {MappedStatement.class,
  3.                 Object.class}),
  4.         @Signature(type = Executor.class, method = "query", args = {MappedStatement.class,
  5.                 Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class})})
  6. public class SqlStatementInterceptor implements Interceptor {
  7.     public static final Logger log = LoggerFactory.getLogger("sys-sql");
  8.     @Override
  9.     public Object intercept(Invocation invocation) throws Throwable {
  10.         long startTime = System.currentTimeMillis();
  11.         try {
  12.             return invocation.proceed();
  13.         } finally {
  14.             long timeConsuming = System.currentTimeMillis() - startTime;
  15.             log.info("执行SQL:{}ms", timeConsuming);
  16.             if (timeConsuming > 999 && timeConsuming < 5000) {
  17.                 log.info("执行SQL大于1s:{}ms", timeConsuming);
  18.             } else if (timeConsuming >= 5000 && timeConsuming < 10000) {
  19.                 log.info("执行SQL大于5s:{}ms", timeConsuming);
  20.             } else if (timeConsuming >= 10000) {
  21.                 log.info("执行SQL大于10s:{}ms", timeConsuming);
  22.             }
  23.         }
  24.     }
  25.     @Override
  26.     public Object plugin(Object target) {
  27.         return Plugin.wrap(target, this);
  28.     }
  29.     @Override
  30.     public void setProperties(Properties properties) {
  31.     }
  32. }
复制代码
自界说拦截器之后,请注意配置该拦截器
3.输出示例

  1. INFO  [http-nio-8080-exec-1] - SQLID: com.example.mapper.UserMapper.selectById
  2. INFO  [http-nio-8080-exec-1] - 完整SQL: SELECT id,name,age FROM user WHERE id=1
  3. INFO  [http-nio-8080-exec-1] - 执行耗时: 48ms
  4. WARN  [http-nio-8080-exec-1] - 慢SQL警告: 执行耗时1204ms
复制代码
四.总结

通过合理配置SQL日记输出,开发者可以:


  • 快速定位SQL执行题目
  • 直观分析现实执行的SQL语句
  • 有效识别性能瓶颈
  • 提拔动态SQL调试效率

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

伤心客

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表