SpringBoot基于Mybatis或Mybatis-Plus自定义实现完整SQL打印和执行耗时

news2024/11/15 12:03:34

注释相当完善了,不啰嗦。直接上代码:

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.reflect.FieldUtils;
import org.apache.ibatis.executor.parameter.ParameterHandler;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.mapping.ParameterMode;
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.type.TypeHandlerRegistry;
import org.springframework.stereotype.Component;

import java.sql.Statement;
import java.util.*;
import java.util.stream.Collectors;

/**
 * mybatis sql 日志拦截器, 用于打印SQL相关信息(例如:实际执行SQL语句,SQL执行时间,查询记录数等)
 * <p>
 * 关于 @Intercepts注解说明:
 * 为了让拦截器能够精确地拦截特定的方法,需要使用 @Intercepts 注解来声明拦截的方法和参数类型
 * 该注解包含一个参数,即一个 @Signature 类型的数组,用于指定要拦截的方法。每个 @Signature 注解表示一个要拦截的方法签名,其中包括以下属性:
 * <p>
 * type:被拦截的目标类或接口。在这里,StatementHandler.class 表示拦截 MyBatis 中的 StatementHandler 类。
 * method:被拦截的方法名。可以通过字符串指定方法名或使用方法引用。
 * args:被拦截方法的参数类型数组。用于指定被拦截方法的参数类型及顺序。
 * 在以下代码中的注解示例中,拦截器指定了对 StatementHandler 类中的三个方法进行拦截,分别是:
 * <p>
 * query(Statement.class, ResultHandler.class):拦截 StatementHandler 类中的 query 方法,该方法有两个参数,分别是 Statement 和 ResultHandler。
 * update(Statement.class):拦截 StatementHandler 类中的 update 方法,该方法有一个参数,即 Statement。
 * batch(Statement.class):拦截 StatementHandler 类中的 batch 方法,该方法也有一个参数,即 Statement。
 * 通过使用 @Intercepts 注解和 @Signature 注解,可以精确地指定要拦截的方法和参数类型,从而实现对特定方法的拦截和处理。
 *
 * @author zt
 */
@Slf4j
@Component
@Intercepts({@Signature(type = StatementHandler.class, method = "query", args = {Statement.class, ResultHandler.class}),
        @Signature(type = StatementHandler.class, method = "update", args = {Statement.class}),
        @Signature(type = StatementHandler.class, method = "batch", args = {Statement.class})})
public class MybatisLogSqlInterceptor implements Interceptor {

    /**
     * 定义一个包含需要添加单引号括起来的参数类型集合。
     */
    private static final Set<String> NEED_BRACKETS =
            Collections.unmodifiableSet(new HashSet<>(Arrays.asList("String", "Date", "Time", "LocalDate", "LocalTime", "LocalDateTime", "BigDecimal", "Timestamp")));

    /**
     * MyBatis的配置对象。
     */
    private Configuration configuration = null;

    /**
     * 拦截器的核心方法,用于拦截并处理SQL语句执行前后的逻辑。
     *
     * @param invocation Invocation 类是 MyBatis 框架提供的一个接口,定义了用于描述方法执行的信息和操作的方法。
     *                   在方法拦截时,MyBatis 将被拦截的方法封装成 Invocation 对象,并作为参数传递给拦截器。
     *                   <p>
     *                   Invocation 接口中定义了以下几个重要的方法:
     *                   <p>
     *                   Object getTarget():获取目标对象,即被拦截的对象。
     *                   Method getMethod():获取被拦截的方法对象。
     *                   Object[] getArgs():获取被拦截方法的参数列表。
     *                   Object proceed() throws Throwable:继续执行被拦截的方法。
     *                   Object proceed(Object[] args) throws Throwable:继续执行被拦截的方法,并使用指定的参数列表。
     *                   Object getThis():获取代理对象,即拦截器生成的代理对象。
     *                   通过 Invocation 对象,我们可以获取被拦截方法的相关信息,如目标对象、方法名称、参数列表等。拦截器可以根据这些信息对方法进行额外的处理,比如记录日志、性能监控、权限验证等。最后,通过调用 proceed() 方法,可以继续执行被拦截的方法。
     *                   <p>
     *                   在代码中,Invocation invocation 参数被用于执行被拦截方法,并在方法执行前后进行一些额外的操作。
     * @return
     * @throws Throwable
     */
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Object target = invocation.getTarget();
        long startTime = System.currentTimeMillis();
        // 初始化行数为 1
        int lines = 1;
        // 默认状态为 "失败"
        String status = "failed";
        try {
            // 执行原始方法,并获取返回结果
            Object proceed = invocation.proceed();
            // 如果返回结果为集合,则统计行数
            if (proceed instanceof Collection<?>) {
                lines = ((List<?>) proceed).size();
            }
            // 执行成功,将状态设置为 "成功"
            status = "success";
            return proceed;
        } finally {
            // 计算 SQL 执行耗时
            long sqlCost = System.currentTimeMillis() - startTime;
            // 获取 SQL 语句
            String sql = this.getSql(target);
            // 打印日志
            log.info("\u001B[38;5;220mSQL 执行结果:{}. SQL 信息:{}\u001B[0m", status, sql);
            log.info("\u001B[38;5;220m耗时:{} mm. Total:{}\u001B[0m", sqlCost, lines);
        }
    }

    /**
     * 获取 SQL 语句
     *
     * @param target 获取目标对象,即被拦截的对象。
     * @return 实际执行SQL语句
     */
    private String getSql(Object target) {
        try {
            // 获取 StatementHandler 对象
            StatementHandler statementHandler = (StatementHandler) target;
            // 获取 BoundSql 对象
            BoundSql boundSql = statementHandler.getBoundSql();
            if (configuration == null) {
                // 通过反射获取 Configuration 对象
                final ParameterHandler parameterHandler = statementHandler.getParameterHandler();
                this.configuration = (Configuration) FieldUtils.readField(parameterHandler, "configuration", true);
            }
            // 格式化 SQL 语句并返回
            return formatSql(boundSql, configuration);
        } catch (Exception e) {
            // 异常处理,打印警告日志
            log.warn("获取 SQL 语句失败:{}", target, e);
            return "无法解析的 SQL 语句";
        }
    }

    /**
     * 格式化 SQL 语句
     *
     * @param boundSql      绑定的 SQL 对象,包含 SQL 语句和参数信息
     * @param configuration MyBatis 的配置信息对象,用于获取配置信息
     * @return 格式化后的 SQL 字符串
     */
    private String formatSql(BoundSql boundSql, Configuration configuration) {
        // 获取原始 SQL 语句
        String sql = boundSql.getSql();
        // 获取参数映射列表
        List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
        // 获取参数对象
        Object parameterObject = boundSql.getParameterObject();
        // 判断是否为空
        if (StringUtils.isEmpty(sql) || Objects.isNull(configuration)) {
            return "";
        }

        // 获取 TypeHandlerRegistry 对象
        TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();

        // 移除 SQL 字符串中的空格、换行符等
        sql = sql.replaceAll("[\n\r ]+", " ");

        // 过滤掉输出参数的参数映射
        if (parameterMappings == null) {
            return sql;
        }
        parameterMappings = parameterMappings.stream()
                .filter(it -> it.getMode() != ParameterMode.OUT)
                .collect(Collectors.toList());

        // 使用 StringBuilder 保存格式化后的 SQL
        final StringBuilder result = new StringBuilder(sql);

        // 解析问号并替换参数
        for (int i = result.length(); i > 0; i--) {
            if (result.charAt(i - 1) != '?') {
                continue;
            }
            ParameterMapping parameterMapping = parameterMappings.get(parameterMappings.size() - 1);
            Object value;
            String propertyName = parameterMapping.getProperty();
            // 判断绑定的附加参数中是否有对应的属性名
            if (boundSql.hasAdditionalParameter(propertyName)) {
                value = boundSql.getAdditionalParameter(propertyName);
            } else if (parameterObject == null) {
                value = null;
            } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
                value = parameterObject;
            } else {
                // 使用 MetaObject 获取属性值
                MetaObject metaObject = configuration.newMetaObject(parameterObject);
                value = metaObject.getValue(propertyName);
            }
            if (value != null) {
                // 判断参数类型,如果是需要添加括号的类型,则添加单引号
                String type = value.getClass().getSimpleName();
                if (NEED_BRACKETS.contains(type)) {
                    result.replace(i - 1, i, "'" + value + "'");
                } else {
                    result.replace(i - 1, i, value.toString());
                }
            } else {
                // 参数值为空时,替换为 "null"
                result.replace(i - 1, i, "null");
            }
            // 移除已处理的参数映射
            parameterMappings.remove(parameterMappings.size() - 1);
        }
        return result.toString();
    }
}

执行结果(我给日志上了颜色,方便独立区分):

实际使用中,也可以根据自己的需要,在代码内自定义扩展自己的需求。

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

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

相关文章

移远通信率先完成5G RedCap运营商实网测试,为商用部署奠定良好基础

近日&#xff0c;移远通信Rx255C 5G RedCap系列模组在上海率先完成了运营商RedCap实网环境下的测试&#xff0c;并成功验证了RedCap网络接入等一系列能力&#xff0c;为加速RedCap在中高速物联网领域的商用部署奠定了良好的基础。 位于上海市嘉定区的RedCap实网测试现场 本次外…

​价值驱动-数据分析价值逻辑与实践思考

月説小飞象交流会 未来是一片迷雾&#xff0c;令人胆怯&#xff0c;但不妨走下去&#xff0c;看看命运给我们准备了什么。 内部交流│25期 价值驱动 数据分析价值逻辑与实践思考 data analysis ●●●● 分享人&#xff1a;黄小伟 当今的企业&#xff0c;随着数字化技术日新月异…

机器学习——深度学习

1 感知机 y f ( ∑ i 1 n w i x i − b ) yf(\sum\limits_{i1}^{n}w_ix_i-b) yf(i1∑n​wi​xi​−b) 其中&#xff0c; f f f 常常取阶跃函数或 Sigmoid 函数。 学习规则&#xff1a; Δ w i η ( y − y ^ ) x i w i ← w i Δ w i \Delta w_i\eta(y-\hat{y})x_i\\ w_i…

C高级重点

1、请简要描述一下Linux文件系统的层级结构&#xff0c;包括不同目录的作用和功能。 Linux的文件系统结构是一个倒插树结构&#xff0c;所有的文件都从根目录出发。 2、find指令的用途 find 查找的路径 -name 文件名 ----->在指定路径下&#xff0c;以文件名为条件查找文…

windows gcc、g++和cmake安装

1、gcc gwindows版本工具mingw下载安装 参考&#xff1a;https://blog.csdn.net/didi_ya/article/details/111240502 https://blog.csdn.net/weixin_46416035/article/details/127387170 ##看这个 下载&#xff1a; https://sourceforge.net/projects/mingw-w64/files/mingw…

图书搜索领域重大突破!用Apache SeaTunnel、Milvus和OpenAI提高书名相似度搜索精准度和效率

作者 | 刘广东&#xff0c;Apache SeaTunnel Committer 背景 目前&#xff0c;现有的图书搜索解决方案&#xff08;例如公共图书馆使用的解决方案&#xff09;十分依赖于关键词匹配&#xff0c;而不是对书名实际内容的语义理解。因此会导致搜索结果并不能很好地满足我们的需…

nodejs安装记录

1.更改安装目录 D:\env\nodejs 2.命令行输入 node -v 查看nodejs的版本号 3.命令行输入 npm -v查看npm的版本号 4.修改模块安装和缓存路径 之前的设置&#xff1a; 在nodejs安装目录下新建一个文件夹&#xff0c;命名为 node_cache 使用命令修改config配置&#xff0c;首先…

【超简单】Windows 使用 mstsc 远程连接 Ubuntu桌面

登录网站下载 《xrdp-installer-1.4.7.zip》 xRDP Installation Script (free)&#xff1a;https://c-nergy.be/repository.html 在Ubuntu 下&#xff0c;解压&#xff0c;添加x 权限后执行安装 chmod 777 xrdp-installer-1.4.7.sh ./xrdp-installer-1.4.7.sh安装完毕后&…

【嵌入式模型转换】2. 算能盒子SE5 芯片板子BM1684 sophon-pipeline

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言1. 开始安装 前言 文章1&#xff0c;我们在SE5上实现了&#xff0c;SOC模式下的 C 和 python-sail的模型转换&#xff0c;文章连接&#xff1a; 【嵌入式模型转…

VUE L ∠脚手架新生命周期过度动画 ⑩⑦

目录 文章有误请指正&#xff0c;如果觉得对你有用&#xff0c;请点三连一波&#xff0c;蟹蟹支持✨ V u e j s Vuejs Vuejs初识 V u e C L I VueCLI VueCLI C L I CLI CLI n e x t T i c k nextTick nextTick C L I CLI CLI V u e Vue Vue封装的过度与动画 C L I CLI …

第十五章 Swin-Transformer网络详解

系列文章目录 第一章 AlexNet网络详解 第二章 VGG网络详解 第三章 GoogLeNet网络详解 第四章 ResNet网络详解 第五章 ResNeXt网络详解 第六章 MobileNetv1网络详解 第七章 MobileNetv2网络详解 第八章 MobileNetv3网络详解 第九章 ShuffleNetv1网络详解 第十章…

Selenium教程__获取界面handle、title和url(7)

本文将介绍如何使用Selenium来获取界面的句柄、标题和URL&#xff0c;并展示一些实际应用场景。 学习本文内容将能够轻松地获取并利用界面的句柄、标题和URL&#xff0c;从而更好地跟踪和管理UI的状态和行为。 from selenium import webdriverdriver webdriver.Chrome() dr…

dlib 人脸识别

其实很不明白&#xff0c;这个库存在这么久了&#xff0c;但csdn有关其的资料那么少&#xff0c;这里写点抛砖引玉。 代码思路&#xff1a; 获取人脸人脸对齐对齐后的人脸转128维向量人脸识别&#xff08;计算向量的距离&#xff09; 效果&#xff1a; dlib检测人脸确实有点慢…

VLAN间通信之VLANIF虚接口

VLAN间通信之VLANIF虚接口 说明&#xff1a;想要实现VLAN间互访有很多解决方案&#xff1a; 1&#xff09;VLAN终结--也称单臂路由 2&#xff09;VLANIF虚接口---最受欢迎的解决方案 1、VLANIF虚接口 1&#xff09;VLANIF接口是一种三层虚拟接口&#xff0c;可以实现VLAN间的…

浏览器F12开发者工具

浏览器F12开发者工具 1.介绍2.工具附录 1.介绍 F12常用于网站界面测试、调试&#xff0c;分析问题&#xff0c;查看HTML元素、查看响应事件和数据等&#xff0c;还可帮助测试工程师定位前后端Bug&#xff1b; 其中使用最多的功能页面是&#xff1a;元素&#xff08;Elements&…

数据结构--顺序表的定义

数据结构–顺序表的定义 知识总览 顺序表 \color{red}顺序表 顺序表―一用 顺序存储 \color{red}顺序存储 顺序存储的方式实现线性表 顺序存储。把 逻辑上相邻 \color{red}逻辑上相邻 逻辑上相邻的元素存储在 物理位置上也相邻 \color{red}物理位置上也相邻 物理位置上也相邻的…

设计模式第16讲——迭代器模式(Iterator)

目录 一、什么是迭代器模式 二、角色组成 三、 优缺点 四、应用场景 4.1 生活场景 4.2 java场景 五、代码实现 5.0 代码结构 5.1 Student——学生实体类 5.2 StudentIterator——抽象迭代器&#xff08;Iterator&#xff09; 5.3 StudentListIterator——具体迭代器…

threejs官方demo学习:模型加载

前言 案例太多了&#xff0c;考虑了一下&#xff0c;实际项目中有可能用的情况一般就是加载模型&#xff0c;然后对模型进行一些操作。因此打算好好看一下关于模型加载的案例&#xff0c;其他案例就不看了。 模型加载并改变材质 <script lang"ts" setup> im…

基于 FPGA 的单脉冲技术:算法设计(附源码)

一、前言 本例显示了开发单脉冲技术的工作流程的前半部分&#xff0c;其中信号使用数字下变频&#xff08;DDC&#xff09;进行下变频。本例中的模型适合在FPGA上实现。本示例重点介绍单脉冲技术的设计&#xff0c;以估计物体的方位角和仰角。 示例的第二部分是基于FPGA的单脉冲…

【C++】hash:unordered_map和unordered_set的底层结构

hash 哈希概念哈希冲突哈希函数哈希冲突的两种解决方法之闭散列哈希冲突的两种解决方法之开散列开散列和闭散列的比较 哈希概念 在c98中还并没有提出哈希这样的结构&#xff0c;只有以红黑树为底层结构的map&#xff0c;set系列&#xff0c;这样使得查询时的效率 l o g 2 N lo…