写一个MyBatis的plugins,记录完整SQL日志

概览

    MyBatis的SQL输出日志语句和参数是分开打印的,我们调试时需要将语句和参数再次拼接才能进行测试执行。当遇到长SQL和参数较多时,还原一个SQL往往需要十几分钟的时间。有很多优秀(懒)的程序员通过编写集成开发环境插件的方式来优化SQL还原拼接的过程,例如Idea平台下的: mybatis-log-plugin(收费)mybatis-log-free(免费)

    最近接到的项目有很多长SQL并且参数的数量也很多,在做SQL优化时需要花很多时间在SQL拼接上,想到前同事使用MyBatis框架插件机制来打印完整的SQL日志,尝试自己实现一下,同时也是为了加强对MyBatis框架的了解。

MyBatis Plugins

    MyBatis的插件机制更准确的说是拦截器,可以用来拦截执行SQL语句执行之前或之后的操作,从而实现对 SQL 执行过程的干预。例如:参数加解密、权限判断、统一字段处理、添加分页、记录SQL执行时间等功能。实现MyBatis插件非常简单,写一个实现Interceptor接口的类,并通过@Intercepts、@Signature注解描述自己拦截的行为即可。

示例-1

import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Signature;

// 拦截所有的 update 方法调用
@Intercepts(
    {@Signature(type= Executor.class, method = "update", args = {MappedStatement.class,Object.class})
})
public class ExamplePlugin implements Interceptor {
    private Properties properties = new Properties();

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // TODO 执行之前的业务处理(自定义)
        Object returnObject = invocation.proceed();
        // TODO 执行之后的业务处理(自定义)
        return returnObject;
    }

	/**
	 * 设置插件参数,内容可以在mybatis-config.xml中设置
	 */
    @Override
    public void setProperties(Properties properties) {
        this.properties = properties;
    }
}

Invocation

    Invocation象包含了被拦截方法的所有必要信息,Target(被调用的对象),Method(被调用的方法),Arguments(传递给方法的参数列表)。proceed 方法用于继续执行被拦截的方法。如果不调用 proceed 方法,被拦截的方法将不会被执行。

MappedStatement

    MappedStatement是MyBatis中的一个核心概念和内部类,它是MyBatis执行SQL语句的关键对象。每个MappedStatement对象代表了一个已经映射的SQL语句,包括了执行此SQL语句所需的所有信息,如SQL语句文本、参数映射、结果映射、缓存策略等
    MappedStatement对象由MyBatis的内部机制自动创建并管理的。当用户定义了一个Mapper接口中的方法或者XML映射文件中的SQL语句时,MyBatis会根据这些定义生成相应的MappedStatement对象,并将其存储在Configuration对象的内部映射表中。

Configuration

    Configuration创建和管理MappedStatement对象,是MyBatis的核心配置对象,包含了所有映射信息、类型别名、类型处理器、对象工厂等配置。

BoundSql

    BoundSql代表一个已绑定参数的SQL语句。由SqlSource创建,SqlSource是MyBatis中用于解析SQL语句并创建BoundSql的抽象类,当执行一个MappedStatement时,MyBatis会根据MappedStatement中的SqlSource创建一个BoundSql实例。BoundSql包含了SQL语句文本、参数列表以及参数映射等信息。

MetaObject

    MetaObject是一个重要工具类,它主要用于简化JavaBean的操作,提供了一种统一的方式来访问和操作对象的属性。

完整示例

import org.apache.commons.collections4.CollectionUtils;
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 org.apache.ibatis.type.TypeHandlerRegistry;

import java.text.DateFormat;
import java.util.*;
import java.util.regex.Matcher;

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

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        long start = System.currentTimeMillis();
        try {
            return invocation.proceed();
        } finally {
            long end = System.currentTimeMillis();
            printSql(invocation, start, end);
        }
    }

    @Override
    public Object plugin(Object target) {
        return Interceptor.super.plugin(target);
    }

    @Override
    public void setProperties(Properties properties) {
        Interceptor.super.setProperties(properties);
    }

    /**
     * 打印完整SQL和执行耗时
     *
     * @param invocation
     * @param start
     * @param end
     */
    private void printSql(Invocation invocation, long start, long end) {

        // 获取参数
        Object parameter = null;
        if (invocation.getArgs().length > 1) {
            parameter = invocation.getArgs()[1];
        }

        // 获取最终的SQL
        MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
        BoundSql boundSql = ms.getBoundSql(parameter);

        // 获取节点的配置
        Configuration configuration = ms.getConfiguration();

        // 替换SQL中的多余的空格
        String sql = boundSql.getSql().replaceAll("[\\s]+", " ");

        // 获取参数
        Object parameterObject = boundSql.getParameterObject();
        List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
        if (!CollectionUtils.isEmpty(parameterMappings) && parameterObject != null) {

            TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
            if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {

                // String、Date等基本类型的处理
                sql = sql.replaceFirst("\\?", Matcher.quoteReplacement(adornParamValue(parameterObject)));

            } else {

                // JavaBean、Collection、Map等复杂入参对象
                MetaObject metaObject = configuration.newMetaObject(parameterObject);
                for (ParameterMapping parameterMapping : parameterMappings) {
                    String propertyName = parameterMapping.getProperty();
                    if (metaObject.hasGetter(propertyName)) {
                        Object obj = metaObject.getValue(propertyName);
                        sql = sql.replaceFirst("\\?", Matcher.quoteReplacement(adornParamValue(obj)));
                    } else if (boundSql.hasAdditionalParameter(propertyName)) {
                        // 该分支是动态sql
                        Object obj = boundSql.getAdditionalParameter(propertyName);
                        sql = sql.replaceFirst("\\?", Matcher.quoteReplacement(adornParamValue(obj)));
                    } else {
                        //打印出缺失,提醒该参数缺失并防止错位
                        sql = sql.replaceFirst("\\?", "missing");
                    }
                }

            }
        }

        Object target = invocation.getTarget();
        System.out.println(ms.getId() + " -> 耗时:" + (end - start) + " -> 完整SQL:" + sql);
    }

    /**
     * 装饰参数值
     *
     * @param obj
     * @return
     */
    private static String adornParamValue(Object obj) {
        String value;
        if (obj instanceof String) {
            value = "'" + obj.toString() + "'";
        } else if (obj instanceof Date) {
            DateFormat formatter = DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.DEFAULT, Locale.CHINA);
            value = "'" + formatter.format(new Date()) + "'";
        } else {
            if (obj != null) {
                value = obj.toString();
            } else {
                value = "";
            }
        }
        return value;
    }

}

参考资料

  • 4
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值