MyBatis插件开发——解析和记录输出完整的SQL语句

news2024/11/15 13:51:10

实现功能

自定义MyBatis插件,该插件实现当MyBatis执行SQL发生异常时输出错误原因,SQL参数以及完整的SQL语句。在日常的开发中我们可以通过mybatis配置设置是否输出SQL,但是对于正常运行的SQL全部输出出来日志量过大,所以这里实现了仅针对于发生异常的时候输出执行的SQL语句。对于日志输出的SQL框架仅提供了带有占位符的SQL以及对应的参数,如果想要测试运行这个SQL还需要自己手动拼接下,所以为了提高效率,在插件中对于SQL进行解析和占位符的替换,最终输出的SQL语句是可以直接在数据库中运行的。

目前所实现的是针对于异常的情况,这个可以根据需要进行修改和扩展,比如对于所有的执行SQL都进行打印,或者在此基础上加上执行耗时的统计等等。

插件开发

添加依赖

  compileOnly 'org.projectlombok:lombok:1.18.26'
    compileOnly 'org.slf4j:slf4j-api:2.0.5'
    compileOnly 'org.slf4j:slf4j-simple:2.0.5'
    compileOnly 'org.mybatis:mybatis:3.2.3'
import java.lang.reflect.InvocationTargetException;
import java.text.DateFormat;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Properties;

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.Plugin;
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 org.apache.ibatis.type.TypeHandlerRegistry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


/**
 * 拦截执行SQL发生异常的场景,并将执行错误,执行SQL和参数打印出来
 * 拦截Executor里面的query和update方法
 *
 * @author zhangyu
 * @date 2023/6/19
 */
@Intercepts({@Signature(method = "query", type = Executor.class, args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
        @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})})
public class PrintExceptionSqlInterceptor implements Interceptor {

    private static final Logger LOGGER = LoggerFactory.getLogger(PrintExceptionSqlInterceptor.class);

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 获取执行方法的MappedStatement参数
        MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
        Object parameter = null;
        if (invocation.getArgs().length > 1) {
            parameter = invocation.getArgs()[1];
        }
        String sqlId = mappedStatement.getId();
        BoundSql boundSql = mappedStatement.getBoundSql(parameter);
        Configuration configuration = mappedStatement.getConfiguration();
        Object response;
        try {
            response = invocation.proceed();
        } catch (Exception e) {
            // 输出SQL异常信息
            LOGGER.error("SQL ErrorException:", e);
            LOGGER.info("SQL Parameters: {}", boundSql.getParameterObject());
            LOGGER.info("Execute SqlId: {}", sqlId);
            LOGGER.info("Completely Execute SQL: {}", getFullSql(configuration, boundSql));
            // 根据源异常类型进行返回
            if (e instanceof InvocationTargetException) {
                throw new InvocationTargetException(e);
            } else if (e instanceof IllegalAccessException) {
                throw new IllegalAccessException(e.getMessage());
            } else {
                throw new RuntimeException(e);
            }
        }
        return response;
    }

    /**
     * 通过该方法决定要返回的对象是目标对象还是对应的代理
     * 不要想的太复杂,一般就两种情况:
     * <p>
     * 1. return target;  直接返回目标对象,相当于当前Interceptor没起作用,不会调用上面的intercept()方法
     * 2. return Plugin.wrap(target, this);  返回代理对象,会调用上面的intercept()方法
     *
     * @param target 目标对象
     * @return 目标对象或者代理对象
     */
    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    /**
     * 用于获取在Configuration初始化当前的Interceptor时时候设置的一些参数
     *
     * @param properties Properties参数
     */
    @Override
    public void setProperties(Properties properties) {
    }


    /**
     * 转义正则特殊字符 ($()*+.[]?\^{}
     * \\需要第一个替换,否则replace方法替换时会有逻辑bug
     */
    private static String makeQueryStringAllRegExp(String str) {
        if (str != null && !"".equals(str)) {
            return str.replace("\\", "\\\\")
                    .replace("*", "\\*")
                    .replace("+", "\\+")
                    .replace("|", "\\|")
                    .replace("{", "\\{")
                    .replace("}", "\\}")
                    .replace("(", "\\(")
                    .replace(")", "\\)")
                    .replace("^", "\\^")
                    .replace("$", "\\$")
                    .replace("[", "\\[")
                    .replace("]", "\\]")
                    .replace("?", "\\?")
                    .replace(",", "\\,")
                    .replace(".", "\\.")
                    .replace("&", "\\&");
        }
        return str;
    }

    /**
     * 获取参数对应的string值
     *
     * @param obj 参数对应的值
     * @return string
     */
    private static String getParameterValue(Object obj) {
        String value;
        if (obj instanceof String) {
            value = "'" + obj + "'";
        } else if (obj instanceof Date) {
            DateFormat formatter =
                    DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.DEFAULT, Locale.CHINA);
            value = "'" + formatter.format(obj) + "'";
        } else {
            if (obj != null) {
                value = obj.toString();
            } else {
                value = "";
            }
        }
        // 对特殊字符进行转义,方便之后处理替换
        return value != null ? makeQueryStringAllRegExp(value) : "";
    }

    /**
     * 获取完整的执行SQL
     */
    public static String getFullSql(Configuration configuration, BoundSql boundSql) {
        try {
            return parseAndExtractFullSql(configuration, boundSql);
        } catch (Exception e) {
            // 如果解析失败返回原始SQL
            return boundSql.getSql();
        }
    }

    /**
     * 组装完整的sql语句并把把对应的参数都代入到sql语句里面
     *
     * @param configuration Configuration
     * @param boundSql      BoundSql
     * @return sql完整语句
     */
    private static String parseAndExtractFullSql(Configuration configuration, BoundSql boundSql) {
        // 获取mapper里面方法上的参数
        Object sqlParameter = boundSql.getParameterObject();
        // sql语句里面需要的参数
        List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
        // sql原始语句(?还没有替换成我们具体的参数)
        String sql = boundSql.getSql().replaceAll("[\\s]+", " ");
        if (!parameterMappings.isEmpty() && sqlParameter != null) {
            // sql语句里面的?替换成真实的参数
            TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
            if (typeHandlerRegistry.hasTypeHandler(sqlParameter.getClass())) {
                sql = sql.replaceFirst("\\?", getParameterValue(sqlParameter));
            } else {
                MetaObject metaObject = configuration.newMetaObject(sqlParameter);
                for (ParameterMapping parameterMapping : parameterMappings) {
                    // 按顺序把?替换成对应的值
                    String propertyName = parameterMapping.getProperty();
                    if (metaObject.hasGetter(propertyName)) {
                        Object obj = metaObject.getValue(propertyName);
                        sql = sql.replaceFirst("\\?", getParameterValue(obj));
                    } else if (boundSql.hasAdditionalParameter(propertyName)) {
                        Object obj = boundSql.getAdditionalParameter(propertyName);
                        sql = sql.replaceFirst("\\?", getParameterValue(obj));
                    }
                }
            }
        }
        return sql;
    }

}

输出JAR组件和使用

(1)方式1:手动注册

手动注册的方式需要在引入当前组件依赖后,再进行手动的注册,目前比较青睐于这种方式。一般来说我们可能将多个mybatis插件都放在一个组件JAR中,但是对于调用方而言不一定需要都应用到当前项目中,那么就可以根据需要自行选择配置注册。

@Configuration
public class MyBatisConfig  {

    @Autowired
    private List<SqlSessionFactory> sqlSessionFactoryList;

    @PostConstruct
    public void addMyInterceptor() {
        for (SqlSessionFactory sqlSessionFactory : sqlSessionFactoryList) {
            sqlSessionFactory.getConfiguration().addInterceptor(new PrintExceptionSqlInterceptor());
        }
    }
}

(2)方式2:自动配置

如果希望使用方引入当前组件的依赖后可以不做任何配置直接使用,可以使用自动配置的方式。
添加自动配置相关的依赖,设置依赖的作用域为compileOnly

 compileOnly (
            "org.springframework.boot:spring-boot-autoconfigure:2.0.5.RELEASE",
            "org.springframework.boot:spring-boot-configuration-processor:2.0.5.RELEASE"
    )

创建自动配置类

import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@ConditionalOnWebApplication
@ConditionalOnClass({PrintExceptionSqlInterceptor.class})
@ConditionalOnProperty(prefix = "mybatis-plugin", name = "enabled", havingValue = "true", matchIfMissing = true)
public class MyBatisPluginAutoConfiguration {

    @Bean
    public PrintExceptionSqlInterceptor yourInterceptor() {
        return new PrintExceptionSqlInterceptor();
    }
}

创建META-INF/spring.factories文件
org.springframework.boot.autoconfigure.EnableAutoConfiguration=自定义的配置类路径

测试

在这里插入图片描述

 SQL Parameters: QuestionUser(id=1, infoId=111111111, sort=1, useable=1, status=1, writeId=1, createUser=1, createTime=Sun Jun 18 14:15:51 CST 2023, updateUser=1, updateTime=Sun Jun 18 14:15:51 CST 2023, isDeleted=0)
 Execute SqlId: com.example.demo2.QuestionUserMapper.insert
 Completely Execute SQL: insert into question_user (id, info_id, sort, useable, status, write_id, create_user, create_time, update_user, update_time, is_deleted) values (1, 111111111, 1, 1, 1, 1, 1, '2023-6-18 14:15:51', 1, '2023-6-18 14:15:51', 0)

目前测试场景包括:单表的增删改查,多表参数操作,批量SQL操作,Map参数,实体类参数等等。

代码

https://github.com/zwzhangyu/ZyCodeHub/tree/main/database-orm/mybatis
在这里插入图片描述

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

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

相关文章

使用VScode + clangd 阅读 c/c++ 源码环境搭建

使用Vscode clangd 阅读c/c源码 一、需求 在嵌入式软件开发的工作中&#xff0c;我们常常需要分析C/C代码&#xff0c;比如linux kernel 的代码&#xff0c;而公司的代码一般都会存放在服务器中&#xff0c;服务器一般是linux&#xff0c;且无法联网&#xff0c;我们只能通过…

C++智能指针-保姆级讲解带你一文搞懂智能指针(附核心代码实现+讲解)

C智能指针 1.引言1.1 为什么会出现智能指针1.2内存泄漏1.2.1 什么是内存泄漏&#xff0c;内存泄漏的危害1.2.2 内存泄漏分类1.2.3如何检测内存泄漏1.2.4如何避免内存泄漏 2. 智能指针的使用及原理3.常见智能指针3.1std::auto_ptr3.2std::unique_ptr3.3std::share_ptr 1.引言 1…

【雕爷学编程】Arduino动手做(116)---五向导航按键模块

37款传感器与执行器的提法&#xff0c;在网络上广泛流传&#xff0c;其实Arduino能够兼容的传感器模块肯定是不止这37种的。鉴于本人手头积累了一些传感器和执行器模块&#xff0c;依照实践出真知&#xff08;一定要动手做&#xff09;的理念&#xff0c;以学习和交流为目的&am…

Axure教程—环形进度条

本文将教大家如何用AXURE制作环形进度条 一、效果 预览地址&#xff1a;https://mmfwgo.axshare.com 二、功能 &#xff08;1&#xff09;、点击“开始”按钮&#xff0c;环形进度开始执行&#xff0c;“开始”按钮转换为“暂停”按钮 &#xff08;2&#xff09;点击“重置”按…

delphi的ARM架构支持与System.Win.WinRT库

delphi的ARM架构支持与System.Win.WinRT库 目录 delphi的ARM架构支持与System.Win.WinRT库 一、WinRT 二、delphi的System.Win.WinRT库 2.1、支持ARM芯片指令 2.2、基于WinRT技术的特点 2.3、所以使用默认库而未经转化的服务端应用并不支持ARM架构服务器 2.4、对默认库…

自学网安遇到问题了该怎么解决

自学网络安全很容易学着学着就迷茫了&#xff0c;找到源头问题&#xff0c;解决它就可以了&#xff0c;所以首先咱们聊聊&#xff0c;学习网络安全方向通常会有哪些问题&#xff0c;看到后面有惊喜哦 1、打基础时间太长 学基础花费很长时间&#xff0c;光语言都有几门&#xf…

计算机程序设计的艺术--第一卷--第一章(1)

1 BASIC CONCEPT 1.1 algorithm algorithm 是所有计算机编程的本质&#xff0c;我们要仔仔细细的追溯一下这个概念是怎么来的。 algorithm 这个词&#xff0c;非常有意思&#xff0c;乍一看&#xff0c;好像是要写 logarithm&#xff0c;写错了&#xff0c;写成了 algorithm…

QTYX量化系统实战案例分享|均线多头排列惯性突破前高-202306第五弹

前言 “实战案例分享系列”是和大家分享一些股票量化分析工具QTYX在实战中的应用案例&#xff08;包括失败的案例&#xff09;&#xff0c;这样能够帮助大家更好地去理解QTYX中的功能设计&#xff0c;也能更好地帮助大家搭建出属于自己的量化交易系统。 关于QTYX的使用攻略可以…

Java Spark 操作 Apache Kudu

一、Apache Kudu Apache Kudu是一种列式分布式存储引擎&#xff0c;它的设计目标是支持快速分析和高吞吐量的数据访问&#xff0c;同时也能够支持低延迟、实时查询和更新操作。它被称为Hadoop生态系统的新一代存储层&#xff0c;能够与Apache Spark、Apache Impala、Apache Hiv…

Python3 字典与集合 | 菜鸟教程(七)

目录 一、Python3 字典 &#xff08;一&#xff09;字典是另一种可变容器模型&#xff0c;且可存储任意类型对象。 &#xff08;二&#xff09;字典的每个键值 key>value 对用冒号 : 分割&#xff0c;每个对之间用逗号(,)分割&#xff0c;整个字典包括在花括号 {} 中 &am…

西南交通大学智能监测 培训课程练习5

2023.06.17培训 linux的简单实用 打包、部署后端jar服务 目录 一、连接远程服务器 二、maven项目打包 2.1添加build依赖 2.2使用maven打包 三、Linux基础操作 3.1利用Xftp上传文件 3.1.1返回上一层目录 3.1.2查看文件 3.1.3进入文件 3.1.4创建文件夹 3.1.5上传文件 …

安装Apache mysql php

一.Apache网站服务 Apache起源 源于 APatchy Server&#xff0c;著名的开源Web服务软件 1995年时&#xff0c;发布Apache服务程序的1.0版本 由Apache软件基金会 (ASF) 负责维护 最新的名称为“Apache HTTP Server 安装Apache----下面两个插件是httpd2.4以后的版…

SpringBoot整合activiti7实现简单的员工请假流程

Activiti 是一个开源架构的工作流引擎&#xff0c;基于bpmn2.0 标准进行流程定义。其前身是JBPM&#xff0c;Activiti 通过嵌入到业务系统开发中进行使用。 整合springboot 引入相关依赖 <!-- 引入Activiti7 --><dependency><groupId>org.activiti</gro…

什么是2.5G和5G多千兆端口?

概要 在当前数字化时代&#xff0c;对于高速数据传输和网络连接的需求不断增长。为了满足这种需求&#xff0c;网络技术也在不断发展和进步。2.5G和5G多千兆端口是一种新型的网络连接技术&#xff0c;提供了比传统千兆以太网更高的传输速率和带宽。本文将详细介绍 的定义、工作…

[元带你学: eMMC协议详解 15] 写保护(Write Protect)详解

依JEDEC eMMC 5.1及经验辛苦整理&#xff0c;付费内容&#xff0c;禁止转载。 所在专栏 《元带你学: eMMC协议详解》 内容摘要 全文 1300字&#xff0c; 主要讲述写保护的用法&#xff0c; 写保护的类型。 Write Protect Management 为了允许主机保护数据不被擦除或覆盖写入&…

vue-server-renderer实现服务端渲染

vue-server-renderer实现服务端渲染 简单认识vue-server-renderer&#xff1a; 是 Vue.js 官方提供的一个库&#xff0c;用于将 Vue 组件渲染成 HTML 字符串或流&#xff0c;通常用于服务端渲染。 具体的咱们vue-server-renderer如何实现 1、预编译组件&#xff1a;根据 Vue …

云安全技术(一)之什么是云计算

对于在云环境中工作的安全专家而言&#xff0c;从传统数据中心模型获得的许多知识和最佳实践仍然适用于云计算环境&#xff0c;但安全专家对云计算概念、不同类型的云模型和云服务的深入理解对于成功实施和监督(Overseeing)安全策略和合规性至关重要。 什么是云计算 1.1 云计…

扫码枪(扫描枪)扫码在vue中的使用教学

1.扫描枪使用原理浅析。 扫描枪的使用原理其实很简单&#xff1a;就是把光信号转换成电信号&#xff0c;再将电信号通过模拟数字转换器转化为数字信号传输到计算机中处理。其实可以简单理解为&#xff1a;二维码/条形码 转换成 字符串。 2.扫描枪功能开发前准备。 正所谓“工…

关于【C语言】中scanf与getchar的用法和常见错误详解

写这篇博客的起因是最近博主自己学习中总是遇到类似的错误&#xff0c;并曾百思不得其解。 今天分享出来是希望帮助大家在写代码时避免这些错误。话不多说&#xff0c;我们直接开始吧&#xff01; 君兮_的个人主页 勤时当勉励 岁月不待人 C/C 游戏开发 输入函数scanf与getcha…

[架构之路-213]- 架构 - 架构设计过程快速概览与在线画图工具

目录 第一步&#xff1a;业务系统 &#xff08;1&#xff09;收集目标系统的用户需求 &#xff08;2&#xff09;定义用例图 第二步 领域建模 &#xff08;1&#xff09;业务流程定义 &#xff08;2&#xff09;业务功能分解 &#xff08;3&#xff09;非功能性架构&…