Mybatis自定义日志打印
一,目标
- 替换?为具体的参数值
- 统计sql执行时间
- 记录执行时间过长的sql,并输出信息到文档(以天为单位进行存储)
平常打印出来的sql都是sql一行,参数一行。如图:

二,理论
这里我们主要通过Mybatis的Interceptor接口与Java自身的IO操作来实现。
Interceptor接口
MyBatis 的 Interceptor 是一个强大的功能,它允许开发者在执行数据库操作的过程中插入自定义逻辑。通过使用拦截器,我们可以在执行 SQL 语句之前或之后进行处理,记录日志、修改输入参数、管理事务、监控性能等功能。
@Intercepts注解
Mybatis提供的一个用于定义拦截器的注解。
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface Intercepts {Signature[] value(); //这里表示可以添加多个注解Signature作为value
}
@Intercepts注解
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({})
public @interface Signature {Class<?> type(); // 拦截哪个类String method(); // 类的哪个方法Class<?>[] args(); // 什么参数
}
这里需要注意一点
方法和参数与你当前项目中引入的版本有关。 写时请必须确认你写的类中确实有该方法,该参数。否则就会出现类似
### Error opening session. Cause: org.apache.ibatis.plugin.PluginException: Could not find method on interface org.apache.ibatis.executor.Executor named query. Cause: java.lang.NoSuchMethodException: org.apache.ibatis.executor.Executor.query(org.apache.ibatis.mapping.MappedStatement,java.lang.Object) ### Cause: org.apache.ibatis.plugin.PluginException: Could not find method on interface org.apache.ibatis.executor.Executor named query. Cause: java.lang.NoSuchMethodException: org.apache.ibatis.executor.Executor.query(org.apache.ibatis.mapping.MappedStatement,java.lang.Object)] with root cause java.lang.NoSuchMethodException: org.apache.ibatis.executor.Executor.query(org.apache.ibatis.mapping.MappedStatement,java.lang.Object)
的异常。
三,实操
1, 核心类--SqlPrintInterceptor
package com.luojie.config.myInterface.mybatisIntercept;import com.luojie.common.Conditions;
import com.luojie.util.TxtUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.ibatis.cache.CacheKey;
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.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Signature;
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 java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.regex.Matcher;/*** 自定义打印日志功能的拦截器*/
@Intercepts({// 拦截 Executor 接口的 query 方法,包含不同的参数组合@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),@Signature(type = Executor.class, method = "queryCursor", args = {MappedStatement.class, Object.class, RowBounds.class}),@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})
})
@Slf4j
public class SqlPrintInterceptor implements Interceptor {private static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");@Overridepublic Object intercept(Invocation invocation) throws Throwable {// 记录开始时间long startTime = System.currentTimeMillis();Object proceed = null;// 执行原始方法try {proceed = invocation.proceed();} catch (Throwable t) {log.error("Error during SQL execution", t);throw t; // 重新抛出异常}// 记录结束时间long endTime = System.currentTimeMillis();long executionTime = endTime - startTime; // 计算执行时间// 转换执行时间为 "XXs.XXms" 格式String formattedExecutionTime = formatExecutionTime(executionTime);// 生成打印的 SQL 语句String printSql = generateSql(invocation);// 输出 SQL 和执行时间System.out.println(Conditions.RED + "SQL: " + printSql);System.out.println("Execution time: " + formattedExecutionTime);System.out.print(Conditions.RESET);log.info("SQL: " + printSql);log.info("Execution time: " + formattedExecutionTime);// 记录慢sql(这里我为了方便观察,所以设置界限为0,各位可以根据实际情况设置)if ((executionTime / 1000) >= 0) {writeSlowSqlToLocation(printSql, formattedExecutionTime);}return proceed; // 返回原始方法的结果}// 记录慢sqlprivate void writeSlowSqlToLocation(String sql, String executeTime) {String formattedDate = dateFormat.format(new Date());String logs = formattedDate + " SQL: " + sql + " 执行耗时: " + executeTime;TxtUtil.writeLog(logs);}// 新增格式化执行时间的方法private String formatExecutionTime(long executionTime) {long seconds = executionTime / 1000; // 获取秒数long milliseconds = executionTime % 1000; // 获取剩余的毫秒数return String.format("%ds.%03dms", seconds, milliseconds); // 格式化为 "XXs.XXXms"}private String generateSql(Invocation invocation) {// 获取 MappedStatement 对象MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];Object parameter = null;// 获取参数对象if (invocation.getArgs().length > 1) {parameter = invocation.getArgs()[1];}// 获取 MyBatis 配置Configuration configuration = mappedStatement.getConfiguration();// 获取 BoundSql 对象BoundSql boundSql = mappedStatement.getBoundSql(parameter);// 获取参数对象Object parameterObject = boundSql.getParameterObject();// 获取参数映射列表List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();// 获取执行的 SQL 语句String sql = boundSql.getSql();// 替换 SQL 中多个空格为一个空格sql = sql.replaceAll("[\\s]+", " ");// 如果参数对象和参数映射不为空if (!ObjectUtils.isEmpty(parameterObject) && !ObjectUtils.isEmpty(parameterMappings)) {// 如果只有一个参数,直接替换if (parameterObject instanceof String && parameterMappings.size() == 1) {return sql.replaceFirst("\\?", String.valueOf(parameterObject)); // 处理缺少值的情况}// 遍历每个参数映射for (ParameterMapping parameterMapping : parameterMappings) {String propertyName = parameterMapping.getProperty(); // 获取属性名MetaObject metaObject = configuration.newMetaObject(parameterObject); // 创建 MetaObjectObject obj = null; // 初始化参数对象// 如果参数对象有对应的 getter 方法if (metaObject.hasGetter(propertyName)) {obj = metaObject.getValue(propertyName); // 获取参数值} else if (boundSql.hasAdditionalParameter(propertyName)) {obj = boundSql.getAdditionalParameter(propertyName); // 获取附加参数}// 替换 SQL 中的占位符if (obj != null) {sql = sql.replaceFirst("\\?", Matcher.quoteReplacement(getParameterValue(obj)));} else {sql = sql.replaceFirst("\\?", Matcher.quoteReplacement(propertyName)); // 处理缺少值的情况}}}return sql; // 返回生成的 SQL 语句}private String getParameterValue(Object parameterObject) {// 如果参数对象为空,返回 "null"if (parameterObject == null) {return "null";}// 返回参数对象的字符串表示return parameterObject.toString();}@Overridepublic Object plugin(Object target) {// 生成插件对象return Interceptor.super.plugin(target);}@Overridepublic void setProperties(Properties properties) {// 设置属性Interceptor.super.setProperties(properties);}
}
2, txt工具类--TxtUtil
package com.luojie.util;import lombok.extern.slf4j.Slf4j;import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.text.SimpleDateFormat;
import java.util.Date;/*** 简单的txt文件工具 类**/
@Slf4j
public class TxtUtil {private static final String LOG_DIRECTORY = "D:\\tmp\\log"; // 指定日志文件夹private static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");public static void writeLog(String message) {// 获取当前日期String currentDate = dateFormat.format(new Date());// 构建文件路径String filePath = LOG_DIRECTORY + "/" + currentDate + ".txt";// 创建目录如果不存在try {Files.createDirectories(Paths.get(LOG_DIRECTORY));} catch (IOException e) {log.error(e.getMessage());}// 追加写入文件try (BufferedWriter writer = new BufferedWriter(new FileWriter(filePath, true))) {writer.write(message);writer.newLine(); // 换行} catch (IOException e) {log.error(e.getMessage());}}
}
3, 常量类--Conditions
package com.luojie.common;/*** 常量类*/
public class Conditions {/*** 控制print的打印颜色*/public static final String RED = "\033[0;31m"; // 红色public static final String GREEN = "\033[0;32m"; // 绿色public static final String YELLOW = "\033[0;33m"; // 黄色public static final String BLUE = "\033[0;34m"; // 蓝色public static final String MAGENTA = "\033[0;35m"; // 品红public static final String CYAN = "\033[0;36m"; // 青色public static final String WHITE = "\033[0;37m"; // 白色/*** 重置print所有颜色*/public static final String RESET = "\033[0m"; // 重置
}
这里是为了控制打印到控制台的颜色,方便观察
4, 让mybatis使用上这个拦截器

package com.luojie.config;import com.luojie.config.myInterface.mybatisIntercept.SqlPrintInterceptor;
import com.zaxxer.hikari.HikariDataSource;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;import javax.sql.DataSource;@Configuration
@MapperScan(basePackages = "com.luojie.dao.mapper1", sqlSessionFactoryRef = "sqlSessionFactory1")
public class DataSource1Config {@Value("${datasource1.url}")private String url;@Value("${datasource1.username}")private String username;@Value("${datasource1.password}")private String password;@Bean(name = "dataSource1")public DataSource dataSource1() {return DataSourceBuilder.create().url(url).username(username).password(password)// 使用HikariCP数据连接池管理.type(HikariDataSource.class).build();}@Bean(name = "sqlSessionFactory1")public SqlSessionFactory sqlSessionFactory1(@Qualifier("dataSource1") DataSource dataSource) throws Exception {SqlSessionFactoryBean sessionFactoryBean = new SqlSessionFactoryBean();sessionFactoryBean.setDataSource(dataSource);sessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:rojerTestMapper/mapper1/*.xml"));// 增加自定义的sql日志打印器// 用new Interceptor[]{new SqlPrintInterceptor()}而不是直接new SqlPrintInterceptor()是为了后续方便扩展sessionFactoryBean.setPlugins(new Interceptor[]{new SqlPrintInterceptor()});return sessionFactoryBean.getObject();}
}
5, 测试结果
只有一个参数时

存在多个参数时

文件保存确认

四, 扩展
我这里简单将慢sql信息记录到本地,大家可以根据自己的项目需要,通过消息中间件实现和短信发送的方式,实现慢sql监控告警等功能。
以上。
祝各位大佬前途光明。
相关文章:
Mybatis自定义日志打印
一,目标 替换?为具体的参数值统计sql执行时间记录执行时间过长的sql,并输出信息到文档(以天为单位进行存储) 平常打印出来的sql都是sql一行,参数一行。如图: 二,理论 这里我们主要通过Mybatis…...
【在Linux世界中追寻伟大的One Piece】Socket编程TCP(续)
目录 1 -> V2 -Echo Server多进程版本 2 -> V3 -Echo Server多线程版本 3 -> V3-1 -多线程远程命令执行 4 -> V4 -Echo Server线程池版本 1 -> V2 -Echo Server多进程版本 通过每个请求,创建子进程的方式来支持多连接。 InetAddr.hpp #pragma…...
面试高频问题:C/C++编译时内存五个分区
在面试时,C/C++编译时内存五个分区是经常问到的问题,面试官通过这个问题来考察面试者对底层的理解。在平时开发时,懂编译时内存分区,也有助于自己更好管理内存。 目录 内存分区的定义 内存分区的重要性 代码区 数据区 BSS区 堆区 栈区 静态内存分配 动态内存分配…...
阅读博士论文《功率IGBT模块健康状态监测方法研究》
IGBT的失效可以分为芯片级失效和封装级失效。其中封装级失效是IGBT模块老化的主要原因,是多种因素共同作用的结果。在DBC的这种结构中,流过芯片的负载电流通过键合线传导到 DBC上层铜箔,再经过端子流出模块。DBC与芯片和提供机械支撑的基板之…...
Spring ApplicationContext接口
ApplicationContext接口是Spring框架中更高级的IoC容器接口,扩展了BeanFactory接口,提供了更多的企业级功能。ApplicationContext不仅具备BeanFactory的所有功能,还增加了事件发布、国际化、AOP、资源加载等功能。 ApplicationContext接口的…...
[perl] 数组与哈希
数组变量以 符号开始,元素放在括号内 简单举例如下 #!/usr/bin/perl names ("a1", "a2", "a3");print "\$names[0] $names[0]\n"; print "size: ",scalar names,"\n";$new_names shift(names); …...
电机学习-SPWM原理及其MATLAB模型
SPWM原理及其MATLAB模型 一、SPWM原理二、基于零序分量注入的SPWM三、MATLAB模型 一、SPWM原理 SPWM其实是相电压的控制方式,定义三相正弦相电压的表达式: { V a m V m sin ω t V b m V m sin ( ω t − 2 3 π ) V c m V m sin ( ω t 2…...
群控系统服务端开发模式-应用开发-腾讯云上传工厂及七牛云上传工厂开发
记住业务流程图,要不然不清楚自己封装的是什么东西。 一、腾讯云工厂开发 切记在根目录下要安装腾讯云OSS插件,具体代码如下: composer require qcloud/cos-sdk-v5 在根目录下extend文件夹下Upload文件夹下channel文件夹中,我们修…...
【深度学习滑坡制图|论文解读3】基于融合CNN-Transformer网络和深度迁移学习的遥感影像滑坡制图方法
【深度学习滑坡制图|论文解读3】基于融合CNN-Transformer网络和深度迁移学习的遥感影像滑坡制图方法 【深度学习滑坡制图|论文解读3】基于融合CNN-Transformer网络和深度迁移学习的遥感影像滑坡制图方法 文章目录 【深度学习滑坡制图|论文解读3】基于融合CNN-Transformer网络和…...
《计算机原理与系统结构》学习系列——处理器(下)
系列文章目录 目录 流水线冒险数据冒险数据相关与数据冒险寄存器先读后写旁路取数使用型冒险阻塞 控制冒险分支引发的控制冒险假设分支不发生动态分支预测双预测位动态分支预测缩短分支延迟带冒险控制的单周期流水线图 异常MIPS中的异常MIPS中的异常处理另一种异常处理机制非精…...
JDK新特性(8-21)数据类型-直接内存
目录 Jdk 新特性 JDK 8 特性 默认方法实现作用:可以使接口更加灵活,不破坏现有实现的情况下添加新的方法。 函数式接口 StreamAPI JDK 9 特性 JDK 10 特性 JDK 11 特性 JDK 14 特性 JDK 17 特性 JDK 21 特性 数据类型 基本数据类型和引用数据类型的区别…...
003-Kotlin界面开发之声明式编程范式
概念本源 在界面程序开发中,有两个非常典型的编程范式:命令式编程和声明式编程。命令式编程是指通过编写一系列命令来描述程序的运行逻辑,而声明式编程则是通过编写一系列声明来描述程序的状态。在命令式编程中,程序员需要关心程…...
QT pro项目工程的条件编译
QT pro项目工程的条件编译 前言 项目场景:项目中用到同一型号两个相机,同时导入两个版本有冲突,编译不通过, 故从编译就区分相机导入调用,使用宏区分 一、定义宏 在pro文件中定义宏: DEFINES USE_Cam…...
深度学习之经典网络-AlexNet详解
AlexNet 是一种经典的卷积神经网络(CNN)架构,在 2012 年的 ImageNet 大规模视觉识别挑战赛(ILSVRC)中表现优异,将 CNN 引入深度学习的新时代。AlexNet 的设计在多方面改进了卷积神经网络的架构,…...
部署Prometheus、Grafana、Zipkin、Kiali监控度量Istio
1. 模块简介 Prometheus 是一个开源的监控系统和时间序列数据库。Istio 使用 Prometheus 来记录指标,跟踪 Istio 和网格中的应用程序的健康状况。Grafana 是一个用于分析和监控的开放平台。Grafana 可以连接到各种数据源,并使用图形、表格、热图等将数据…...
结合 Spring Boot Native 和 Spring Boot 构建高性能服务器架构
随着云计算和微服务架构的普及,开发者们不断寻求提高应用性能和用户体验的解决方案。Spring Boot Native 的出现,利用 GraalVM 的原生映像特性,使得 Java 应用的启动速度和资源占用得到了显著改善。本文将深入探讨如何将前端应用使用 Spring …...
ArcGIS影像调色(三原色)三原色调整
本期主要介绍ArcGIS影像调色(三原色) ArcGIS影像调色(三原色),对比度、亮度、gamma。红绿蓝三原色调整。 视频学习 ArcGIS影像调色(三原色)...
SQLite从入门到精通面试题及参考答案
目录 SQLite 是什么? SQLite 的优点有哪些? 轻量级与易于部署 零配置和低维护成本 良好的兼容性和跨平台性 高性能和可靠性 SQLite 的局限性有哪些? 并发处理能力有限 缺乏用户管理和权限控制功能 有限的扩展性 有限的网络支持 SQLite 和其他数据库系统(如 MyS…...
【C/C++】字符/字符串函数(0)(补充)——由ctype.h提供
零.导言 除了字符分类函数,字符转换函数也是一类字符/字符串函数。 C语言提供了两种字符转换函数,分别是 toupper , tolower。 一.什么是字符转换函数? 顾名思义,即转换字符的函数,如大写字母转小写字母&am…...
Git 的特殊配置文件
文章目录 1.前言2.Git 标准配置文件.gitignore作用格式示例 .gitattributes作用格式示例 .gitmodules作用格式示例 .gitconfig作用格式示例 3.非 Git 标准约定文件.gitkeep简介示例 .gitacls作用格式示例 参考文献 1.前言 Git 是一个强大的版本控制系统,它使用多个…...
别再混淆了!JavaScript与Java的10个本质区别(附常见面试题解析)
别再混淆了!JavaScript与Java的10个本质区别(附常见面试题解析) 当面试官问"Java和JavaScript有什么区别"时,超过60%的初级开发者会给出"它们就像汽车和地毯的关系"这类玩笑式回答。但真正理解这两种语言的核…...
CDN 报错 403/502/504 怎么解决?源站与防护策略排查
网站接入CDN后,原本访问流畅,突然出现403、502、504报错,用户反馈无法访问,自己排查半天找不到头绪——其实这类报错大多和「源站状态」「防护策略」「CDN配置」三个环节相关,今天就结合实操经验,把这三种常…...
提升51%运行速度:Win11Debloat系统优化工具全方位应用指南
提升51%运行速度:Win11Debloat系统优化工具全方位应用指南 【免费下载链接】Win11Debloat 一个简单的PowerShell脚本,用于从Windows中移除预装的无用软件,禁用遥测,从Windows搜索中移除Bing,以及执行各种其他更改以简化…...
终极指南:ImagePicker资源解析机制如何高效处理图像资源
终极指南:ImagePicker资源解析机制如何高效处理图像资源 【免费下载链接】ImagePicker :camera: Reinventing the way ImagePicker works. 项目地址: https://gitcode.com/gh_mirrors/im/ImagePicker ImagePicker作为一款重新定义图片选择体验的工具…...
自动缝纫机SolidWorks
在自动缝纫机的设计过程中,往往需要处理大量精密零件的协同工作,从送布机构、针杆组件到旋梭系统,每个部件的尺寸精度和装配关系都直接影响设备的运行稳定性和缝纫效果。而SolidWorks作为三维设计工具,在这一过程中扮演着关键角色…...
BGP路由优化实战:加速收敛,提升网络稳定性
BGP路由优化实战:加速收敛,提升网络稳定性在复杂的网络环境中,尤其是在大规模数据中心或跨区域互联的网络中,BGP(Border Gateway Protocol)路由协议的性能直接影响着网络的可用性和用户体验。BGP 作为互联网…...
Crypto-JS实战指南:如何构建可靠的浏览器端加密验证体系
Crypto-JS实战指南:如何构建可靠的浏览器端加密验证体系 【免费下载链接】crypto-js JavaScript library of crypto standards. 项目地址: https://gitcode.com/gh_mirrors/cr/crypto-js 在Web应用开发中,加密功能的正确性直接关系到用户数据安全…...
保姆级教程:用STM32CubeMX配置TIM1的PA8和PA11输出PWM波(STM32F103C8T6)
STM32CubeMX实战:从零配置TIM1的PA8/PA11输出PWM驱动电机 当你第一次拿到STM32F103C8T6这块蓝色的小板子时,可能会被密密麻麻的引脚吓到——但别担心,今天我们要用STM32CubeMX这个神器,像搭积木一样轻松配置出精准的PWM波形。我清…...
igel高级功能解析:交叉验证与模型评估最佳实践
igel高级功能解析:交叉验证与模型评估最佳实践 【免费下载链接】igel a delightful machine learning tool that allows you to train, test, and use models without writing code 项目地址: https://gitcode.com/gh_mirrors/ig/igel igel是一个让机器学习变…...
鲁棒估计与5点算法求解本质矩阵
发散,无法保证找到全局正确的解。鉴于5点算法的代数复杂性和实现难度(涉及高次多项式求根、病态方程处理等),并且考虑到本系列文章的核心主题是数值优化而非代数几何,我们在此不展开其繁琐的数学推导和代码实现细节。感…...
