使用MyBatis拦截器实现SQL的完整打印

 更新时间:2024年07月09日 08:58:24   作者:毅航  
当我们使用Mybatis结合Mybatis-plus进行开发时,为了查看执行sql的信息通常我们可以通过属性配置的方式打印出执行的sql语句,但这样的打印出了sql语句常带有占位符信息,不利于排错,所以本文介绍了构建MyBatis拦截器,实现SQL的完整打印,需要的朋友可以参考下

当我们使用Mybatis结合Mybatis-plus进行开发时,为了查看执行sql的信息通常我们可以通过属性配置的方式打印出执行的sql语句,但这样的打印出了sql语句常带有占位符信息,不利于排错。

为了解决这一痛点问题,我们可以通过Mybatis提供的拦截器,来获取到真正执行的sql信息,从而避免我们手动替换占位符的额外操作。

前言

在日常使用Mybatis-plus开发时,为了能获取到执行的sql语句,通常可以在配置文件进入如下的配置:

mybatis-plus:
  configuration:
    map-underscore-to-camel-case: true
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  mapper-locations: classpath*:mappers/*.xml

通过配置MyBatis-plus中将log-impl的日志打印的实现为org.apache.ibatis.logging.stdout.StdOutImpl ,以实现sql语句在控制台的打印。

此时,当我们执行如下sql信息时:

<select id="selectByUserName" resultType="com.example.pojo.User">
    select user_name userName , age from t_user where user_name = #{name}
</select>

可以看到在控制台会打印出如下内容:

不难发现,我们打印出的sql信息其实是带有占位符的。如果我们想在sql工具中对sql进行执行,则需要我们手动对占位符进行替换,对于上述这样的sql来说这并不是一件难事。但当sql相关查询参数比较多的时,通过手动对sql占位符进行替换显然不是一件明智的举措了。

为了解决这一问题,我们其实可以借助Mybatis提供的拦截器来获取真正执行的sql信息,从而避免手动对占位符的替换!

Mybatis的拦截器

InterceptorMyBatis一个非常强大的特性,它允许你拦截执行的sql 语句,并在 sql执行前后进行自定义处理。从而实现诸如日志记录、参数修改、结果处理、分页等功能。

通常MyBatis内部允许对sql执行过程中Executor、ParameterHandler、ResultSetHandler 和 StatementHandler四个关键节进行拦截。众所周知,Executorsql执行过程的核心组件。Executor会调用 StatementHandlerParameterHandler 来完成sql的准备和执行。

因此,对于Executor拦截可以获取执行sql,并且对于sql 执行前后添加自定义逻辑,如缓存逻辑,在查询语句执行前后检查和添加缓存。

进一步来看,对于Execuotr而言,其还允许在数据库操作的不同阶段进行精确的干预和拦截。例如,如果对Executor中的update方法进行拦截,则其可以获取sql执行中 insert、update、delete三种类型的sql语句。而对Executor# query方法拦截器,其则可以获取 select 类型的 sql 语句。

知晓了MyBatisInterceptor对于Mybatis核心组件Executor的拦截逻辑后。接下来,我们将主要介绍如何在Mybatis中自定义一个自己的Interceptor

事实上,如果要在MyBatis 中编写一个拦截器,则首先需要实现 Interceptor 接口,该接口主要包含如下方法:

  • intercept 方法

intercept 方法接收一个 Invocation 对象,代表被拦截的方法调用。这个方法可以在方法调用前后执行自定义逻辑,并决定是否继续执行原方法。

@Override
public Object intercept(Invocation invocation) throws Throwable {
    // 在这里编写拦截逻辑
    return invocation.proceed(); // 继续执行原方法
}
  • plugin 方法

plugin 方法用于生成目标对象的代理。如果目标对象是需要拦截的类型,返回代理对象;否则直接返回目标对象。

@Override
public Object plugin(Object target) {
    return Plugin.wrap(target, this);
}
  • setProperties 方法

setProperties 方法用于接收在配置文件中定义的属性,这些属性可以用来配置拦截器的行为。

@Override
public void setProperties(Properties properties) {
    // 读取配置属性
}

如下是Interceptor的一个统计sql执行时长的示例代码:

package com.example.interceptor;

import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.plugin.*;
import java.sql.Connection;
import java.util.Properties;

@Intercepts({
    @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})
})
public class SqlPrintInterceptor implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
        String sql = statementHandler.getBoundSql().getSql();
        long startTime = System.currentTimeMillis();
        try {
            return invocation.proceed();
        } finally {
            long endTime = System.currentTimeMillis();
            System.out.println("SQL: " + sql);
            System.out.println("Execution Time: " + (endTime - startTime) + "ms");
        }
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

  
}

sql打印拦截器

经过上述分析,相信大家对Mybatis中的Interceptor已经有了比较整体的认识。接下来,我们便来分析该如何构建一个打印完整sql的拦截器。

在开始写代码时,首先来对我们的需求进行再次明确。我们的目标是期待通过MybatisInterceptor来实现完整sql的打印。 而如果要实现这一目标,对Executor进行拦截无疑来说是恰当的选择。因为ExecutorMybatis执行sql的一个媒介,其调用 StatementHandlerParameterHandler 来完成对sql的准备和执行。明确了拦截器的切入点后,我们再来看我们要对Executor中的那些方法进行拦截。

正如之前介绍的那样," 如果拦截 Executor 中的 update 方法,可以捕获执行 insertupdatedelete 三种类型的 SQL 语句。相反,拦截 Executorquery 方法将允许对 select 类型的 SQL 语句进行捕获。"

因此,在构建拦截器时我们的@Signature内容如下:

@Intercepts({
        @Signature(type = Executor.class, method = "query",
                args = {MappedStatement.class, Object.class,
                        RowBounds.class, ResultHandler.class}),
        @Signature(type = Executor.class, method = "query",
                args = {MappedStatement.class, Object.class,
                        RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
        @Signature(type = Executor.class, method = "update",
                args = {MappedStatement.class, Object.class})
})

我们对Executor中的queryupdate方法进行拦截,其中args表示的是方法入参信息。由于Executor中的query方法存在方法的重载,所以出现两次!

在此基础上,我们构建出的拦截器如下:

@Intercepts({
        @Signature(type = Executor.class, method = "query",
                args = {MappedStatement.class, Object.class,
                        RowBounds.class, ResultHandler.class}),
        @Signature(type = Executor.class, method = "query",
                args = {MappedStatement.class, Object.class,
                        RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
        @Signature(type = Executor.class, method = "update",
                args = {MappedStatement.class, Object.class})
})
@Slf4j
public class SqlInterceptor  implements Interceptor {

    /**
     * 默认替换字符
     */
    public static final String UNKNOWN = "UNKNOWN";
    /**
    * 替换sql中的?占位符
    */
    public static final String SQL_PLACEHOLDER = "#{%s}";



    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        String completeSql = "";
        try {
            completeSql = getCompleteSqlInfo(invocation);
        }catch (RuntimeException e) {
            log.error("获取sql信息出错,异常信息 ",e);
        }finally {
            log.info("sql执行信息:[{}] ",completeSql);
        }
        return invocation.proceed();
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    /**
     * 获取完整的sql信息
     * @param invocation
     * @return
     */
    private String getCompleteSqlInfo(Invocation invocation) {
        // invocation中的Args数组中第一个参数即为MappedStatement对象
        MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
        // invocation中的Args数组中第二个参数为sql语句所需要的参数
        Object parameter = null;
        if (invocation.getArgs().length > 1) {
            parameter = invocation.getArgs()[1];
        }
        return generateCompleteSql(mappedStatement, parameter);
    }

    private String generateCompleteSql(MappedStatement mappedStatement, Object parameter) {
        // 获取sql语句
        String mappedStatementId = mappedStatement.getId();
        // BoundSql就是封装myBatis最终产生的sql类
        BoundSql boundSql = mappedStatement.getBoundSql(parameter);
        // 格式化sql信息
        String sql =  SqlFormatter.format(boundSql.getSql());
        // 获取参数列表
        List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
        Object parameterObject = boundSql.getParameterObject();
        Configuration configuration = mappedStatement.getConfiguration();

        if (!CollUtil.isEmpty(parameterMappings) && parameterObject != null) {
            // 遍历参数完成对占位符的替换处理
            for (int i = 0 ; i < parameterMappings.size() ; i++) {
                String replacePlaceHolder = String.format(SQL_PLACEHOLDER,i);
                sql = sql.replaceFirst("\?",replacePlaceHolder);
            }
            // MetaObject主要是封装了originalObject对象,提供了get和set的方法用于获取和设置originalObject的属性值
            MetaObject metaObject = configuration.newMetaObject(parameterObject);
            for (int i = 0 ; i < parameterMappings.size() ; i ++) {
                ParameterMapping parameterMapping = parameterMappings.get(i);
                String replacePlaceHolder = String.format(SQL_PLACEHOLDER,i);
                String propertyName = parameterMapping.getProperty();
                if (metaObject.hasGetter(propertyName)) {
                        Object obj = metaObject.getValue(propertyName);
                        sql = sql.replaceFirst(Pattern.quote(replacePlaceHolder),
                                Matcher.quoteReplacement(getParameterValue(obj)));
                } else if (boundSql.hasAdditionalParameter(propertyName)) {
                    // 处理动态sql标签信息
                    Object obj = boundSql.getAdditionalParameter(propertyName);
                    sql = sql.replaceFirst(Pattern.quote(replacePlaceHolder),
                                Matcher.quoteReplacement(getParameterValue(obj)));
                } else {
                    // 未知参数,替换?为特定字符
                    sql = sql.replaceFirst(Pattern.quote(replacePlaceHolder), UNKNOWN);
                }
            }
        }

        StringBuilder formatSql = new StringBuilder()
                .append(" mappedStatementId - ID:").append(mappedStatementId)
                .append(StringPool.NEWLINE).append("Execute SQL:").append(sql);
        return formatSql.toString();
    }

    /**
     *
     * @author 毅航
     * @date 2024/7/7 9:14
     */
    private static String getParameterValue(Object obj) {
        // 直接返回空字符串将避免在 SQL 查询中加入不必要的单引号,从而保持查询的正确性。
        if (obj == null) {
            return "";
        }

        String stringValue = obj.toString();
        // 对于非空字符串,我们添加单引号以满足以满足参数优化的需求。
        return "'" + stringValue + "'";
    }

为了读者能快速理解上述拦截器的原理,小编在此上述代码中的generateCompleteSql的处理逻辑进行简单的分析。

首先,generateCompleteSql方法的主要目的是生成一个完整的、可读性高的Sql语句,其它接收两个参数:MappedStatement对象和parameter(参数对象)。其内部逻辑如下:

  • 获取SQL语句和基本信息:

    • mappedStatementId存储了MappedStatement的ID,这通常与MyBatis中的映射语句相关联。
    • 通过mappedStatement.getBoundSql(parameter)获取BoundSql对象,其中包含了未解析的SQL语句和参数映射信息。
  • 格式化SQL语句:

    • 调用SqlFormatter.format()方法来格式化SQL语句,增加可读性。
  • 准备参数信息:

    • BoundSql中提取参数映射列表parameterMappings和参数对象parameterObject
    • 检查参数映射列表是否非空且参数对象非空,这是进行参数替换的前提。
  • 参数替换:

    • 遍历参数映射列表,使用正则表达式和字符串操作,将SQL语句中的?占位符替换为特定的占位符(如#{param0})。
    • 利用configuration.newMetaObject(parameterObject)创建MetaObject,用于访问参数对象的属性。
    • 对于每个参数映射,尝试通过MetaObject获取属性值或通过BoundSql的附加参数信息获取值,然后将这些值转换为字符串形式,再替换到SQL语句中。
    • 如果属性值无法通过上述方式获取,则将占位符替换为预定义的未知标识符UNKNOWN
  • 构建并返回完整SQL语句:

    • 最后,构造一个字符串,包含mappedStatementId和最终的SQL语句,便于日志记录或调试。
    • 返回这个字符串作为函数的结果。

将上述拦截器注入Spring容器,

@Configuration
public class MybatisConfigBean {

    @Bean
    public SqlInterceptor addMybatisInterceptor() {
        return new SqlInterceptor();
    }
}

启动SpringBoot应用,然后执行相关sql时,可以看到控制台有如下输出:

2024-07-07 10:11:46.407  INFO 19076 --- [nio-8080-exec-9] com.example.Interceptor.SqlInterceptor   
: sql执行信息:[ mappedStatementId - ID:com.example.dao.UserMapper.selectByUserName
Execute SQL:select
        user_name userName ,
        age 
    from
        t_user 
    where
        user_name = 'zhangSan?' 
        and remark = 'test1'] 

至此,我们就利用MyBatis对外暴露出的Interceptor接口,手动实现一个能优雅地打印完整sql日志的拦截器!

总结

本文首先对Mybatis内置sql打印机制进行了分析,深入阐述了其所面临痛点,然后对Mybatis的拦截器机制进行了深入介绍,并借助拦截器截止,实现了一款可以完整打印sql的拦截器!

以上就是使用MyBatis拦截器实现SQL的完整打印的详细内容,更多关于MyBatis拦截器SQL打印的资料请关注脚本之家其它相关文章!

相关文章

  • 关于MVC与SpringMVC的介绍、区别、执行流程

    关于MVC与SpringMVC的介绍、区别、执行流程

    这篇文章主要介绍了关于MVC与SpringMVC的介绍、区别、执行流程,MVC框架的主要目标是将应用程序的业务逻辑(Model)与用户界面(View)分离开来,从而提高应用程序的可维护性和可扩展性,需要的朋友可以参考下
    2023-05-05
  • springBoot+dubbo+zookeeper实现分布式开发应用的项目实践

    springBoot+dubbo+zookeeper实现分布式开发应用的项目实践

    本文主要介绍了springBoot+dubbo+zookeeper实现分布式开发应用的项目实践,文中通过示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2022-03-03
  • java基于Apache FTP实现文件上传、下载、修改文件名、删除

    java基于Apache FTP实现文件上传、下载、修改文件名、删除

    本篇文章主要介绍了Apache FTP实现文件上传、下载、修改文件名、删除,实现了FTP文件上传(断点续传)、FTP文件下载、FTP文件重命名、FTP文件删除等功能,有需要的可以了解一下。
    2016-11-11
  • MyBatis-Plus逆向工程——Generator的使用

    MyBatis-Plus逆向工程——Generator的使用

    这篇文章主要介绍了MyBatis-Plus逆向工程——Generator的使用,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2021-01-01
  • JDK 14的新特性:文本块Text Blocks的使用

    JDK 14的新特性:文本块Text Blocks的使用

    这篇文章主要介绍了JDK 14的新特性:文本块Text Blocks的使用,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-05-05
  • IntelliJ IDEA失焦自动重启服务的解决方法

    IntelliJ IDEA失焦自动重启服务的解决方法

    在使用 IntelliJ IDEA运行 SpringBoot 项目时,你可能会遇到一个令人困扰的问题,一旦你的鼠标指针离开当前IDE窗口,点击其他位置时, IDE 窗口会失去焦点,你的 SpringBoot 服务就会自动重启,所以本文给大家介绍了IntelliJ IDEA失焦自动重启服务的解决方法
    2023-10-10
  • Spring IoC学习之ApplicationContext中refresh过程详解

    Spring IoC学习之ApplicationContext中refresh过程详解

    这篇文章主要给大家介绍了关于Spring IoC学习之ApplicationContext中refresh过程的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-09-09
  • 在eclipse中修改tomcat的部署路径操作

    在eclipse中修改tomcat的部署路径操作

    这篇文章主要介绍了在eclipse中修改tomcat的部署路径操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2021-01-01
  • Java MapStruct解了对象映射的毒

    Java MapStruct解了对象映射的毒

    这篇文章主要介绍了MapStruct解了对象映射的毒,对MapStruct感兴趣的同学,可以参考下
    2021-04-04
  • 通过JDK源码角度分析Long类详解

    通过JDK源码角度分析Long类详解

    这篇文章主要给大家介绍了关于通过JDK源码角度分析Long类的相关资料,文中通过示例代码介绍的非常详细,对大家学习或者使用long类具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧。
    2017-11-11

最新评论