使用MybatisPlus实现sql日志打印优化

背景:

在排查无忧行后台服务日志时,一个请求可能会包含多个执行的sql,经常会遇到SQL语句与对应参数不连续显示,或者参数较多需要逐个匹配的情况。这种情况下,如果需要还原完整SQL语句就会比较耗时。因此,我希望能优化这一流程。

正文:

在平时排查sql执行情况时,对于sql查询参数比较多或者多个sql在同一个请求下执行的时候会比较麻烦,需要查看每个参数的值一一对应检查是否有异常,有时候还需要将查询sql的问号还原去库里面查询结果,就类似下面的日志:
在这里插入图片描述

一个一个参数还原比较麻烦,因此针对这种问题,我找到了一个解决办法,重写MybatisPlus的InnerInterceptor,将sql日志重新组装,那么在查看sql日志时候就可以直接查看赋值好参数的sql了,将上面截图的sql整合之后可以转化为下面的sql:
在这里插入图片描述

具体的思路以及实现方案,以无忧行项目为例

实现思路,利用mybatisplus提供的接口InnerInterceptor,重写sql日志,并将重写之后的类注入到sqlSessionFactory中,在执行sql之前捕获到并打印。
解释一下InnerInterceptor类:InnerInterceptor 是 MyBatis-Plus 框架中的一个核心拦截器接口,它是 MyBatis 拦截器机制的扩展,用于在 SQL 执行过程中进行拦截和增强。
InnerInterceptor工作原理:InnerInterceptor 通过 MyBatis 的插件机制工作,在以下时机进行拦截:

- beforeQuery:查询操作执行前
- beforeUpdate:更新操作执行前
- beforePrepare:SQL 准备阶段

针对查询sql执行的sql重整,使用的是beforeQuery,针对更新sql执行的重整,使用的是beforeUpdate,以下是具体实现:

1.首先,添加依赖,

maven依赖:

<dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.4.0</version>
</dependency>

或者gradle依赖:

compile("com.baomidou:mybatis-plus-generator:3.4.0")

2.重写InnerInterceptor

package com.cmi.jego.micro.flight.admin.service.config;import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.apache.ibatis.type.TypeHandlerRegistry;import java.sql.SQLException;
import java.text.DateFormat;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.regex.Matcher;/*** sql日志重写** @author zhouxy* @date 2025年05月08日 11:27*/
@Slf4j
@org.springframework.context.annotation.Configuration
public class MybatisPlusLogRewrite implements InnerInterceptor {@Overridepublic void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {log.info("beforeQuery");logInfo(boundSql, ms, parameter);}@Overridepublic void beforeUpdate(Executor executor, MappedStatement ms, Object parameter) throws SQLException {log.info("beforeUpdate");BoundSql boundSql = ms.getBoundSql(parameter);logInfo(boundSql, ms, parameter);}private static void logInfo(BoundSql boundSql, MappedStatement ms, Object parameter) {try {// 获取到节点的id,即sql语句的idString sqlId = ms.getId();// 获取节点的配置Configuration configuration = ms.getConfiguration();// 获取到最终的sql语句String sql = getSql(configuration, boundSql, sqlId);log.info("完整的sql:{}", sql);} catch (Exception e) {log.error("异常:{}", e.getLocalizedMessage(), e);}}// 封装了一下sql语句,使得结果返回完整xml路径下的sql语句节点id + sql语句public static String getSql(Configuration configuration, BoundSql boundSql, String sqlId) {return sqlId + ":" + showSql(configuration, boundSql);}// 进行?的替换public static String showSql(Configuration configuration, BoundSql boundSql) {// 获取参数Object parameterObject = boundSql.getParameterObject();List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
//        log.info("boundSql:{}",boundSql.getSql());// sql语句中多个空格都用一个空格代替String sql = boundSql.getSql().replaceAll("[\\s]+", " ");if (CollectionUtils.isNotEmpty(parameterMappings) && parameterObject != null) {// 获取类型处理器注册器,类型处理器的功能是进行java类型和数据库类型的转换TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();// 如果根据parameterObject.getClass()可以找到对应的类型,则替换if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {sql = sql.replaceFirst("\\?",Matcher.quoteReplacement(getParameterValue(parameterObject)));} else {// MetaObject主要是封装了originalObject对象,提供了get和set的方法用于获取和设置originalObject的属性值,主要支持对JavaBean、Collection、Map三种类型对象的操作MetaObject metaObject = configuration.newMetaObject(parameterObject);for (ParameterMapping parameterMapping : parameterMappings) {String propertyName = parameterMapping.getProperty();if (metaObject.hasGetter(propertyName)) {Object obj = metaObject.getValue(propertyName);sql = sql.replaceFirst("\\?",Matcher.quoteReplacement(getParameterValue(obj)));} else if (boundSql.hasAdditionalParameter(propertyName)) {// 该分支是动态sqlObject obj = boundSql.getAdditionalParameter(propertyName);sql = sql.replaceFirst("\\?",Matcher.quoteReplacement(getParameterValue(obj)));} else {// 打印出缺失,提醒该参数缺失并防止错位sql = sql.replaceFirst("\\?", "缺失");}}}}return sql;}// 如果参数是String,则添加单引号, 如果是日期,则转换为时间格式器并加单引号; 对参数是null和不是null的情况作了处理private static String getParameterValue(Object obj) {String value;if (obj instanceof String) {value = "'" + obj.toString() + "'";} else if (obj instanceof Date) {DateFormat formatter = DateFormat.getDateTimeInstance(DateFormat.DEFAULT,DateFormat.DEFAULT, Locale.CHINA);value = "'" + formatter.format(new Date()) + "'";} else {if (obj != null) {value = obj.toString();} else {value = "";}}return value;}
}

3.将当前的重写类MybatisPlusLogRewrite注入到sqlSessionFactory中。

package com.cmi.jego.micro.flight.admin.service.config;import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import org.apache.ibatis.session.SqlSessionFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;import javax.annotation.PostConstruct;
import java.util.List;/*** @author zhouxy* @date 2025年05月09日 15:58*/
@Configuration
public class MyBatisPlusConfig {@Autowiredprivate List<SqlSessionFactory> sqlSessionFactoryList;@Autowiredprivate MybatisPlusLogRewrite myInnerInterceptor;/*** 添加Mybatis拦截器** @author zhouxy* @date 2025/5/15 16:29*/@PostConstructpublic void addMybatisInterceptor() {for (SqlSessionFactory sqlSessionFactory : sqlSessionFactoryList) {org.apache.ibatis.session.Configuration configuration = sqlSessionFactory.getConfiguration();//将sql拦截器添加到MybatisPlusInterceptor拦截器链MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();mybatisPlusInterceptor.addInnerInterceptor(myInnerInterceptor);configuration.addInterceptor(mybatisPlusInterceptor);}}
}

关闭其他sql打印日志,执行查询sql,只打印当前重写之后的sql日志如下:

2025-05-14 18:03:01.353  INFO 34718 --- [           main] c.c.j.m.f.a.service.handler.LogHandler   : function:[OrderRefundServiceImpl.queryOrderListStatusCount], request: {"lang":"zh_CN","pageNum":1,"pageSize":10}
2025-05-14 18:03:01.385  INFO 34718 --- [           main] c.c.j.m.f.a.s.c.MybatisPlusLogRewrite    : beforeQuery
2025-05-14 18:03:01.387  INFO 34718 --- [           main] c.c.j.m.f.a.s.c.MybatisPlusLogRewrite    : 完整的sql:com.cmi.jego.micro.flight.admin.service.mapper.jegotrip.TicketRefundMapper.queryListStatusCount:select distinct(tr.id) as refundId, tr.status from ticket_refund tr left join ticket_order td on tr.order_id = td.order_id left join ticket_passenger tp on tr.order_id = tp.order_id left join ticket_order_detail tod on tr.order_id = tod.order_id WHERE ( td.search_channel in ( 'hbgj-api-domestic-custom' ) or (td.order_source is null and td.search_channel is null) or td.order_source = '0' )
2025-05-14 18:03:01.395  WARN 34718 --- [           main] c.a.druid.pool.DruidAbstractDataSource   : discard long time none received connection. , jdbcUrl : jdbc:mysql://localhost:3306/ticket_jegotrip?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&serverTimezone=GMT%2B8, version : 1.2.5, lastPacketReceivedIdleMillis : 263261
2025-05-14 18:03:01.395  WARN 34718 --- [           main] c.a.druid.pool.DruidAbstractDataSource   : discard long time none received connection. , jdbcUrl : jdbc:mysql://localhost:3306/ticket_jegotrip?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&serverTimezone=GMT%2B8, version : 1.2.5, lastPacketReceivedIdleMillis : 263912
2025-05-14 18:03:01.396  WARN 34718 --- [           main] c.a.druid.pool.DruidAbstractDataSource   : discard long time none received connection. , jdbcUrl : jdbc:mysql://localhost:3306/ticket_jegotrip?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&serverTimezone=GMT%2B8, version : 1.2.5, lastPacketReceivedIdleMillis : 263933
2025-05-14 18:03:01.396  WARN 34718 --- [           main] c.a.druid.pool.DruidAbstractDataSource   : discard long time none received connection. , jdbcUrl : jdbc:mysql://localhost:3306/ticket_jegotrip?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&serverTimezone=GMT%2B8, version : 1.2.5, lastPacketReceivedIdleMillis : 263958
2025-05-14 18:03:01.396  WARN 34718 --- [           main] c.a.druid.pool.DruidAbstractDataSource   : discard long time none received connection. , jdbcUrl : jdbc:mysql://localhost:3306/ticket_jegotrip?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&serverTimezone=GMT%2B8, version : 1.2.5, lastPacketReceivedIdleMillis : 263986
2025-05-14 18:03:01.851  INFO 34718 --- [           main] c.c.j.m.f.a.s.c.MybatisPlusLogRewrite    : beforeQuery
2025-05-14 18:03:01.851  INFO 34718 --- [           main] c.c.j.m.f.a.s.c.MybatisPlusLogRewrite    : 完整的sql:com.cmi.jego.micro.flight.admin.service.mapper.jegotrip.TicketRefundMoneyMapper.queryFailedByRefundIds:SELECT id , order_id, user_id, refund_id, pay_order_id, supplier_order_id, type, price, refund_price, status, refund_time, limit_time, refund_no, create_time, update_time, remark FROM ticket_refund_money WHERE refund_id IN ( 1295 , 1296 , 1423 , 1424 ) AND status = 4
2025-05-14 18:03:01.871  INFO 34718 --- [           main] c.c.j.m.f.a.service.handler.LogHandler   : class:[OrderRefundServiceImpl.queryOrderListStatusCount] response: {"bizCode":0,"bizMsg":"SUCCESS","data":{"statusCounts":[{"count":1,"status":-1,"statusDesc":"已取消"},{"count":2,"status":3,"statusDesc":"已退票退款"},{"count":1,"status":4,"statusDesc":"退票异常"}]},"rpcCode":0,"rpcMsg":"SUCCESS"}, cost: 521,
2025-05-14 18:03:01.876  INFO 34718 --- [           main] c.c.j.m.f.a.s.s.OrderRefundServiceTest   : {"bizCode":0,"bizMsg":"SUCCESS","data":{"statusCounts":[{"count":1,"status":-1,"statusDesc":"已取消"},{"count":2,"status":3,"statusDesc":"已退票退款"},{"count":1,"status":4,"statusDesc":"退票异常"}]},"rpcCode":0,"rpcMsg":"SUCCESS"}

性能评估方面

重写InnerInterceptor类其实是在SqlSessionFactory中添加了拦截器,那么性能如何呢?下面是我这边实际测试的时长对比

不添加sql重写:

在这里插入图片描述

添加sql重写:

在这里插入图片描述

根据测试时间可以看出,加了sql重写会比未加的时候的时间长了11ms。
因为涉及到拦截以及反射,性能比不加的时候会稍微耗时一些,
如果是访问量不高的情况下,用户对于加没加sql重写的主观感应时长其实差不多,因此这种sql重写方案适合后台访问并发量不高的情况使用。
如果是并发量较高且比较注重性能的情况下,不建议加。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.pswp.cn/news/906854.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

go多线程压测监控

实现了 go多协程压力测试实现了Monitor&#xff0c;异步统计qps、时延、cpu(client端)等指标&#xff0c;周期printStat。只需要把单条执行func传给Monitor即可命令行传参ctrlc之后正常退出(mock cpu 占用) 代码见 https://gitee.com/bbjg001/golearning/tree/master/others/…

安卓无障碍脚本开发全教程

文章目录 第一部分&#xff1a;无障碍服务基础1.1 无障碍服务概述核心功能&#xff1a; 1.2 基本原理与架构1.3 开发环境配置所需工具&#xff1a;关键依赖&#xff1a; 第二部分&#xff1a;创建基础无障碍服务2.1 服务声明配置2.2 服务配置文件关键属性说明&#xff1a; 2.3 …

闲时处理技术---CAD C#二次开发

在CAD C#二次开发中&#xff0c;使用闲时处理技术可以提高程序的响应性能和资源利用率。以下是一般的实现步骤&#xff1a; 1. 了解CAD的事件机制 CAD提供了一些事件&#xff0c;如 Idle 事件&#xff0c;当CAD应用程序处于空闲状态时会触发该事件。你可以订阅这个事件来执行闲…

Git研究

以下命令在CentOS系统下执行 创建Git仓库 git init git-example 监控.git目录的变化情况&#xff1a; watch -n .5 tree .git 写入文件内容&#xff0c;并把文件添加到Stage暂存区 echo 1 > t.txtgit add 1.txt 观察结果如下&#xff1a;objects下多出了一个d00491fd…

野火鲁班猫(arrch64架构debian)从零实现用MobileFaceNet算法进行实时人脸识别(四)安装RKNN Toolkit Lite2

RKNN Toolkit Lite2 是瑞芯微专为RK系列芯片开发的NPU加速推理API。若不使用该工具&#xff0c;计算任务将仅依赖CPU处理&#xff0c;无法充分发挥芯片高达6TOPS的NPU算力优势。 按照官方文档先拉一下官方代码库&#xff0c;然后通过whl文件安装&#xff0c;因为我是python3.1…

Vue3集成Element Plus完整指南:从安装到主题定制下-实现后台管理系统框架搭建

本文将详细介绍如何使用 Vue 3 构建一个综合管理系统&#xff0c;包括路由配置、页面布局以及常用组件集成。 一、路由配置 首先&#xff0c;我们来看系统的路由配置&#xff0c;这是整个应用的基础架构&#xff1a; import {createRouter, createWebHistory} from vue-rout…

【Oracle】创建公共数据连接

需求描述 两个oracle数据库&#xff0c;想从B数据库创建视图脚本访问A数据库相关表的数据&#xff0c;该怎么访问呢&#xff1f; 解决方法 在Oracle数据库中&#xff0c;创建公共数据库链接&#xff08;Public Database Link&#xff09;可以允许数据库中的任何用户访问远程…

时序数据库IoTDB的分片与负载均衡策略深入解析

一、引言 随着数据库服务的业务负载增加&#xff0c;扩展服务资源成为必然需求。扩展方式主要分为纵向扩展和横向扩展。纵向扩展通过增加单台机器的能力&#xff08;如内存、硬盘、处理器&#xff09;来实现&#xff0c;但受限于单台机器的硬件能力。而横向扩展则通过增加更多…

计算机网络期末复习资料

我用夸克网盘分享了「计算机网络」&#xff0c; 链接&#xff1a;https://pan.quark.cn/s/8aac2f0b840e 计算机网络试题库 1单项选择题 1.1以下属于物理层的设备是 ( A) A. 中继器 B.以太网交换机 C. 桥 D. 网关 1.2在以太网中&#xff0c;是根据 (B) 地址来区分…

【IEEE 2025】低光增强KANT(使用KAN代替MLP)----论文详解与代码解析

【IEEE 2025】本文参考论文Enhancing Low-Light Images with Kolmogorov–Arnold Networks in Transformer Attention 虽然不是顶刊&#xff0c;但是有值得学习的地方 论文地址&#xff1a;arxiv 源码地址&#xff1a;github 文章目录 Part1 --- 论文精读Part2 --- 代码详解形状…

naivechain:简易区块链实现

naivechain&#xff1a;简易区块链实现 naivechain A naive and simple implementation of blockchains. 项目地址: https://gitcode.com/gh_mirrors/nai/naivechain 项目介绍 naivechain 是一个简单且易于理解的区块链实现项目。它使用 Go 语言编写&#xff0c;以极简…

Zabbix开源监控的全面详解!

一、zabbix的基本概述 zabbix&#xff0c;这款企业级监控软件&#xff0c;能全方位监控各类网络参数&#xff0c;确保企业服务架构的安全稳定运行。它提供了灵活多样的告警机制&#xff0c;帮助运维人员迅速发现并解决问题。此外&#xff0c;zabbix还具备分布式监控功能&#…

软考软件评测师——软件工程之开发模型与方法

目录 一、核心概念 二、主流模型详解 &#xff08;一&#xff09;经典瀑布模型 &#xff08;二&#xff09;螺旋演进模型 &#xff08;三&#xff09;增量交付模型 &#xff08;四&#xff09;原型验证模型 &#xff08;五&#xff09;敏捷开发实践 三、模型选择指南 四…

50天50个小项目 (Vue3 + Tailwindcss V4) ✨ | Blurry Loading (毛玻璃加载)

&#x1f4c5; 我们继续 50 个小项目挑战&#xff01;—— Blurry Loading 组件 仓库地址&#xff1a;https://github.com/SunACong/50-vue-projects 项目预览地址&#xff1a;https://50-vue-projects.vercel.app/ ✨ 组件目标 实现一个加载进度条&#xff0c;随着加载进度的…

WPF性能优化之延迟加载(解决页面卡顿问题)

文章目录 前言一. 基础知识回顾二. 问题分析三. 解决方案1. 新建一个名为DeferredContentHost的控件。2. 在DeferredContentHost控件中定义一个名为Content的object类型的依赖属性&#xff0c;用于承载要加载的子控件。3. 在DeferredContentHost控件中定义一个名为Skeleton的ob…

VLM-MPC:自动驾驶中模型预测控制器增强视觉-语言模型

《VLM-MPC: Model Predictive Controller Augmented Vision Language Model for Autonomous Driving》2024年8月发表&#xff0c;来自威斯康星大学的论文。 受视觉语言模型&#xff08;VLM&#xff09;的紧急推理能力及其提高自动驾驶系统可理解性的潜力的启发&#xff0c;本文…

推荐系统里真的存在“反馈循环”吗?

推荐系统里真的存在“反馈循环”吗&#xff1f; 许多人说&#xff0c;推荐算法不过是把用户早已存在的兴趣挖掘出来&#xff0c;你本来就爱听流行歌、买潮牌玩具&#xff0c;系统只是在合适的时间把它们端到你面前&#xff0c;再怎么迭代&#xff0c;算法也改变不了人的天性&a…

代码混淆技术的还原案例

案例一 eval 混淆 特征 &#xff1a; 反常的 eval 连接了一堆数据 练习网站 https://scrape.center/ spa9 这个案例 基本的还原方法 但是这个代码还是非常的模糊不好看 优化一下 &#xff1a; 当然还有更快捷的方法 &#xff1a; 好用的 js混淆还原的 web &#xf…

鸿蒙Flutter实战:22-混合开发详解-2-Har包模式引入

以 Har 包的方式加载到 HarmonyOS 工程 创建工作 创建一个根目录 mkdir ohos_flutter_module_demo这个目录用于存放 flutter 项目和鸿蒙项目。 创建 Flutter 模块 首先创建一个 Flutter 模块&#xff0c;我们选择与 ohos_app 项目同级目录 flutter create --templatemodu…

Go核心特性与并发编程

Go核心特性与并发编程 1. 结构体与方法&#xff08;扩展&#xff09; 高级结构体特性 // 嵌套结构体与匿名字段 type Employee struct {Person // 匿名嵌入Department stringsalary float64 // 私有字段 }// 构造函数模式 func NewPerson(name string, age int) *Pe…