当前位置: 首页 > news >正文

Mybatis自定义日志打印

一,目标

  1.  替换?为具体的参数值
  2. 统计sql执行时间
  3. 记录执行时间过长的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自定义日志打印

一&#xff0c;目标 替换?为具体的参数值统计sql执行时间记录执行时间过长的sql&#xff0c;并输出信息到文档&#xff08;以天为单位进行存储&#xff09; 平常打印出来的sql都是sql一行&#xff0c;参数一行。如图&#xff1a; 二&#xff0c;理论 这里我们主要通过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多进程版本 通过每个请求&#xff0c;创建子进程的方式来支持多连接。 InetAddr.hpp #pragma…...

面试高频问题:C/C++编译时内存五个分区

在面试时,C/C++编译时内存五个分区是经常问到的问题,面试官通过这个问题来考察面试者对底层的理解。在平时开发时,懂编译时内存分区,也有助于自己更好管理内存。 目录 内存分区的定义 内存分区的重要性 代码区 数据区 BSS区 堆区 栈区 静态内存分配 动态内存分配…...

阅读博士论文《功率IGBT模块健康状态监测方法研究》

IGBT的失效可以分为芯片级失效和封装级失效。其中封装级失效是IGBT模块老化的主要原因&#xff0c;是多种因素共同作用的结果。在DBC的这种结构中&#xff0c;流过芯片的负载电流通过键合线传导到 DBC上层铜箔&#xff0c;再经过端子流出模块。DBC与芯片和提供机械支撑的基板之…...

Spring ApplicationContext接口

ApplicationContext接口是Spring框架中更高级的IoC容器接口&#xff0c;扩展了BeanFactory接口&#xff0c;提供了更多的企业级功能。ApplicationContext不仅具备BeanFactory的所有功能&#xff0c;还增加了事件发布、国际化、AOP、资源加载等功能。 ApplicationContext接口的…...

[perl] 数组与哈希

数组变量以 符号开始&#xff0c;元素放在括号内 简单举例如下 #!/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其实是相电压的控制方式&#xff0c;定义三相正弦相电压的表达式&#xff1a; { V a m V m sin ⁡ ω t V b m V m sin ⁡ ( ω t − 2 3 π ) V c m V m sin ⁡ ( ω t 2…...

群控系统服务端开发模式-应用开发-腾讯云上传工厂及七牛云上传工厂开发

记住业务流程图&#xff0c;要不然不清楚自己封装的是什么东西。 一、腾讯云工厂开发 切记在根目录下要安装腾讯云OSS插件&#xff0c;具体代码如下&#xff1a; composer require qcloud/cos-sdk-v5 在根目录下extend文件夹下Upload文件夹下channel文件夹中&#xff0c;我们修…...

【深度学习滑坡制图|论文解读3】基于融合CNN-Transformer网络和深度迁移学习的遥感影像滑坡制图方法

【深度学习滑坡制图|论文解读3】基于融合CNN-Transformer网络和深度迁移学习的遥感影像滑坡制图方法 【深度学习滑坡制图|论文解读3】基于融合CNN-Transformer网络和深度迁移学习的遥感影像滑坡制图方法 文章目录 【深度学习滑坡制图|论文解读3】基于融合CNN-Transformer网络和…...

《计算机原理与系统结构》学习系列——处理器(下)

系列文章目录 目录 流水线冒险数据冒险数据相关与数据冒险寄存器先读后写旁路取数使用型冒险阻塞 控制冒险分支引发的控制冒险假设分支不发生动态分支预测双预测位动态分支预测缩短分支延迟带冒险控制的单周期流水线图 异常MIPS中的异常MIPS中的异常处理另一种异常处理机制非精…...

JDK新特性(8-21)数据类型-直接内存

目录 Jdk 新特性 JDK 8 特性 默认方法实现作用:可以使接口更加灵活&#xff0c;不破坏现有实现的情况下添加新的方法。 函数式接口 StreamAPI JDK 9 特性 JDK 10 特性 JDK 11 特性 JDK 14 特性 JDK 17 特性 JDK 21 特性 数据类型 基本数据类型和引用数据类型的区别…...

003-Kotlin界面开发之声明式编程范式

概念本源 在界面程序开发中&#xff0c;有两个非常典型的编程范式&#xff1a;命令式编程和声明式编程。命令式编程是指通过编写一系列命令来描述程序的运行逻辑&#xff0c;而声明式编程则是通过编写一系列声明来描述程序的状态。在命令式编程中&#xff0c;程序员需要关心程…...

QT pro项目工程的条件编译

QT pro项目工程的条件编译 前言 项目场景&#xff1a;项目中用到同一型号两个相机&#xff0c;同时导入两个版本有冲突&#xff0c;编译不通过&#xff0c; 故从编译就区分相机导入调用&#xff0c;使用宏区分 一、定义宏 在pro文件中定义宏&#xff1a; DEFINES USE_Cam…...

深度学习之经典网络-AlexNet详解

AlexNet 是一种经典的卷积神经网络&#xff08;CNN&#xff09;架构&#xff0c;在 2012 年的 ImageNet 大规模视觉识别挑战赛&#xff08;ILSVRC&#xff09;中表现优异&#xff0c;将 CNN 引入深度学习的新时代。AlexNet 的设计在多方面改进了卷积神经网络的架构&#xff0c;…...

部署Prometheus、Grafana、Zipkin、Kiali监控度量Istio

1. 模块简介 Prometheus 是一个开源的监控系统和时间序列数据库。Istio 使用 Prometheus 来记录指标&#xff0c;跟踪 Istio 和网格中的应用程序的健康状况。Grafana 是一个用于分析和监控的开放平台。Grafana 可以连接到各种数据源&#xff0c;并使用图形、表格、热图等将数据…...

结合 Spring Boot Native 和 Spring Boot 构建高性能服务器架构

随着云计算和微服务架构的普及&#xff0c;开发者们不断寻求提高应用性能和用户体验的解决方案。Spring Boot Native 的出现&#xff0c;利用 GraalVM 的原生映像特性&#xff0c;使得 Java 应用的启动速度和资源占用得到了显著改善。本文将深入探讨如何将前端应用使用 Spring …...

ArcGIS影像调色(三原色)三原色调整

本期主要介绍ArcGIS影像调色&#xff08;三原色&#xff09; ArcGIS影像调色&#xff08;三原色&#xff09;&#xff0c;对比度、亮度、gamma。红绿蓝三原色调整。 视频学习 ArcGIS影像调色&#xff08;三原色&#xff09;...

SQLite从入门到精通面试题及参考答案

目录 SQLite 是什么? SQLite 的优点有哪些? 轻量级与易于部署 零配置和低维护成本 良好的兼容性和跨平台性 高性能和可靠性 SQLite 的局限性有哪些? 并发处理能力有限 缺乏用户管理和权限控制功能 有限的扩展性 有限的网络支持 SQLite 和其他数据库系统(如 MyS…...

【C/C++】字符/字符串函数(0)(补充)——由ctype.h提供

零.导言 除了字符分类函数&#xff0c;字符转换函数也是一类字符/字符串函数。 C语言提供了两种字符转换函数&#xff0c;分别是 toupper &#xff0c; tolower。 一.什么是字符转换函数&#xff1f; 顾名思义&#xff0c;即转换字符的函数&#xff0c;如大写字母转小写字母&am…...

Git 的特殊配置文件

文章目录 1.前言2.Git 标准配置文件.gitignore作用格式示例 .gitattributes作用格式示例 .gitmodules作用格式示例 .gitconfig作用格式示例 3.非 Git 标准约定文件.gitkeep简介示例 .gitacls作用格式示例 参考文献 1.前言 Git 是一个强大的版本控制系统&#xff0c;它使用多个…...

多模态2025:技术路线“神仙打架”,视频生成冲上云霄

文&#xff5c;魏琳华 编&#xff5c;王一粟 一场大会&#xff0c;聚集了中国多模态大模型的“半壁江山”。 智源大会2025为期两天的论坛中&#xff0c;汇集了学界、创业公司和大厂等三方的热门选手&#xff0c;关于多模态的集中讨论达到了前所未有的热度。其中&#xff0c;…...

Linux 文件类型,目录与路径,文件与目录管理

文件类型 后面的字符表示文件类型标志 普通文件&#xff1a;-&#xff08;纯文本文件&#xff0c;二进制文件&#xff0c;数据格式文件&#xff09; 如文本文件、图片、程序文件等。 目录文件&#xff1a;d&#xff08;directory&#xff09; 用来存放其他文件或子目录。 设备…...

遍历 Map 类型集合的方法汇总

1 方法一 先用方法 keySet() 获取集合中的所有键。再通过 gey(key) 方法用对应键获取值 import java.util.HashMap; import java.util.Set;public class Test {public static void main(String[] args) {HashMap hashMap new HashMap();hashMap.put("语文",99);has…...

理解 MCP 工作流:使用 Ollama 和 LangChain 构建本地 MCP 客户端

&#x1f31f; 什么是 MCP&#xff1f; 模型控制协议 (MCP) 是一种创新的协议&#xff0c;旨在无缝连接 AI 模型与应用程序。 MCP 是一个开源协议&#xff0c;它标准化了我们的 LLM 应用程序连接所需工具和数据源并与之协作的方式。 可以把它想象成你的 AI 模型 和想要使用它…...

《用户共鸣指数(E)驱动品牌大模型种草:如何抢占大模型搜索结果情感高地》

在注意力分散、内容高度同质化的时代&#xff0c;情感连接已成为品牌破圈的关键通道。我们在服务大量品牌客户的过程中发现&#xff0c;消费者对内容的“有感”程度&#xff0c;正日益成为影响品牌传播效率与转化率的核心变量。在生成式AI驱动的内容生成与推荐环境中&#xff0…...

Python实现prophet 理论及参数优化

文章目录 Prophet理论及模型参数介绍Python代码完整实现prophet 添加外部数据进行模型优化 之前初步学习prophet的时候&#xff0c;写过一篇简单实现&#xff0c;后期随着对该模型的深入研究&#xff0c;本次记录涉及到prophet 的公式以及参数调优&#xff0c;从公式可以更直观…...

python爬虫:Newspaper3k 的详细使用(好用的新闻网站文章抓取和解析的Python库)

更多内容请见: 爬虫和逆向教程-专栏介绍和目录 文章目录 一、Newspaper3k 概述1.1 Newspaper3k 介绍1.2 主要功能1.3 典型应用场景1.4 安装二、基本用法2.2 提取单篇文章的内容2.2 处理多篇文档三、高级选项3.1 自定义配置3.2 分析文章情感四、实战案例4.1 构建新闻摘要聚合器…...

TRS收益互换:跨境资本流动的金融创新工具与系统化解决方案

一、TRS收益互换的本质与业务逻辑 &#xff08;一&#xff09;概念解析 TRS&#xff08;Total Return Swap&#xff09;收益互换是一种金融衍生工具&#xff0c;指交易双方约定在未来一定期限内&#xff0c;基于特定资产或指数的表现进行现金流交换的协议。其核心特征包括&am…...

PL0语法,分析器实现!

简介 PL/0 是一种简单的编程语言,通常用于教学编译原理。它的语法结构清晰,功能包括常量定义、变量声明、过程(子程序)定义以及基本的控制结构(如条件语句和循环语句)。 PL/0 语法规范 PL/0 是一种教学用的小型编程语言,由 Niklaus Wirth 设计,用于展示编译原理的核…...

Spring Boot+Neo4j知识图谱实战:3步搭建智能关系网络!

一、引言 在数据驱动的背景下&#xff0c;知识图谱凭借其高效的信息组织能力&#xff0c;正逐步成为各行业应用的关键技术。本文聚焦 Spring Boot与Neo4j图数据库的技术结合&#xff0c;探讨知识图谱开发的实现细节&#xff0c;帮助读者掌握该技术栈在实际项目中的落地方法。 …...