Mybatis输出可执行的SQL

MyBatis在原则上无法直接记录完整的SQL查询。为了记录查询,所有参数都需要被序列化并表示为字符串。对于简单的数据类型(如String或Integer),这并不成问题,但对于更复杂的数据类型(如Timestamp或Blob),其表示方式可能依赖于具体的数据库。 当查询执行时,并不需要将参数转换为字符串,因为JDBC驱动程序会以更高效(且数据库依赖的)格式将它们传递给数据库。然而,出于日志记录的目的,MyBatis只能获取到Java对象,而MyBatis并不知道如何将它们表示为数据库特定的字符串字面量。 因此,你能实现的最佳方案(这也是MyBatis所支持的)是记录带有占位符的查询,并单独记录使用的参数,这也是我们常见到的。

vbnet 复制代码
==>  Preparing: SELECT * FROM event WHERE 1=1 and `enabled` = ?
==> Parameters: true(Boolean)
<==      Total: 8

但是,很多情况下,我们的SQL查询当中并不会使用到很复杂的数据类型,如果能将拼接好的SQL直接输出到控制台的话,这将极大的提高我们日常开发/调试时的效率。

接下来就是我实现这一想法的关键步骤:

  • 首先,我增强了Mybatis的MappedStatement类,通过在项目启动时自动挂载上一个agent实现的,它的作用是拦截到SQL和参数,然后将他们拼接成完整可执行的SQL,然后通过日志系统输出到控制台。
ini 复制代码
package com.tiktok.core.transformer;

import net.bytebuddy.agent.builder.AgentBuilder;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.matcher.ElementMatchers;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.type.TypeHandlerRegistry;

import java.text.DateFormat;
import java.tiktok.spy.SpyAPI;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.regex.Matcher;

public class MybatisGetBoundSqlTransformer implements AgentBuilderVisitor {

    private final static String clazz = "org.apache.ibatis.mapping.MappedStatement";

    @Override
    public String getClazz() {
        return clazz;
    }

    @Override
    public AgentBuilder build(AgentBuilder agentBuilder) {
        return agentBuilder
                // 作用于类,指定拦截
                .type(ElementMatchers.named(getClazz()))
                // 作用于方法,匹配并修改
                .transform((builder, typeDescription, classLoader, javaModule, protectionDomain) -> builder
                        .visit(
                                Advice.to(MybatisGetBoundSqlTransformer.MybatisGetBoundSqlInterceptor.class)
                                        .on(ElementMatchers.named("getBoundSql")
                                                .and(ElementMatchers.takesArguments(Object.class))
                                                .and(ElementMatchers.returns(BoundSql.class))
                                        )
                        )
                );
    }

    public static class MybatisGetBoundSqlInterceptor {
        @Advice.OnMethodExit
        public static void exit(@Advice.This MappedStatement mappedStatement, @Advice.Return BoundSql returnObject) {
            try {
                if (SpyAPI.checkEntrypoint()) {
                    Object parameterObject = returnObject.getParameterObject();
                    List<ParameterMapping> parameterMappings = returnObject.getParameterMappings();
                    final String sql = returnObject.getSql().replaceAll("\s+", " ");
                    final String mapperId = mappedStatement.getId();

                    if (parameterObject == null || parameterMappings == null || parameterMappings.isEmpty()) {
                        SpyAPI.atExit("mybatis", mapperId, sql);
                        return;
                    }

                    Configuration configuration = mappedStatement.getConfiguration();
                    TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
                    String newSql = String.copyValueOf(sql.toCharArray());

                    if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
                        String value;
                        if (parameterObject instanceof String) {
                            value = "'" + parameterObject + "'";
                        } else if (parameterObject instanceof Date) {
                            DateFormat formatter = DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.DEFAULT, Locale.getDefault());
                            value = "'" + formatter.format(parameterObject) + "'";
                        } else {
                            value = parameterObject.toString();
                        }
                        newSql = newSql.replaceFirst("\?", Matcher.quoteReplacement(value));
                    } else {
                        MetaObject metaObject = configuration.newMetaObject(parameterObject);
                        for (ParameterMapping parameterMapping : parameterMappings) {
                            String propertyName = parameterMapping.getProperty();
                            Object parameter = null;
                            if (metaObject.hasGetter(propertyName)) {
                                parameter = metaObject.getValue(propertyName);
                            } else if (returnObject.hasAdditionalParameter(propertyName)) {
                                parameter = returnObject.getAdditionalParameter(propertyName);
                            }
                            String value;
                            if (parameter instanceof String) {
                                value = "'" + parameter + "'";
                            } else if (parameter instanceof Date) {
                                DateFormat formatter = DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.DEFAULT, Locale.getDefault());
                                value = "'" + formatter.format(parameter) + "'";
                            } else {
                                if (null != parameter) {
                                    value = parameter.toString();
                                } else {
                                    value = "缺失";
                                }
                            }
                            newSql = newSql.replaceFirst("\?", Matcher.quoteReplacement(value));
                        }
                    }

                    SpyAPI.atExit("mybatis", mapperId, newSql);
                }
            } catch (Throwable throwable) {
                throwable.printStackTrace();
            }
        }
    }

}

SpyAPI.atExit("mybatis", mapperId, newSql)是借鉴了arthas中间谍类的设计,将用户应用中获取到的SQL转交给agent进行处理。

  • 然后,通过获取了被@RestContronller注解的类,拦截了所有的接口请求,这样做的目的是希望只有当接口被请求时,才会在控制台中打印请求接口内执行的SQL,不必被持续不断滚动的SQL所干扰。
typescript 复制代码
package com.tiktok.core.transformer;

import net.bytebuddy.agent.builder.AgentBuilder;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.matcher.ElementMatchers;
import org.springframework.context.support.AbstractApplicationContext;
import org.springframework.web.bind.annotation.RestController;

import java.tiktok.spy.SpyAPI;
import java.util.Map;

public class AbstractApplicationContextTransformer implements AgentBuilderVisitor {

    private final static String clazz = "org.springframework.context.support.AbstractApplicationContext";

    @Override
    public String getClazz() {
        return clazz;
    }

    @Override
    public AgentBuilder build(AgentBuilder agentBuilder) {
        return agentBuilder
                // 作用于类,指定拦截
                .type(ElementMatchers.named(getClazz()))
                // 作用于方法,匹配并修改
                .transform((builder, typeDescription, classLoader, javaModule, protectionDomain) -> builder
                        .visit(
                                Advice.to(AbstractApplicationContextTransformer.AbstractApplicationContextInterceptor.class)
                                        .on(ElementMatchers.named("finishRefresh")
                                                .and(ElementMatchers.isProtected())
                                                .and(ElementMatchers.takesNoArguments())
                                        )
                        )
                );
    }

    public static class AbstractApplicationContextInterceptor {
        @Advice.OnMethodExit
        public static void exit(@Advice.This AbstractApplicationContext abstractApplicationContext) {
            try {
                Map<String, Object> restControllerMap = abstractApplicationContext.getBeansWithAnnotation(RestController.class);
                SpyAPI.setRestControllerMap(restControllerMap);
            } catch (Throwable throwable) {
                // ignore
            }
        }
    }

}

最后,效果时这样的:

  • 请求哪个接口,就只会打印哪个接口内执行的SQL语句
  • SQL已经被拼接完成,可以直接被执行
  • 为了方便复制,右键选择"Copy Mybatis Log"即可自动复制、格式化当前行的SQL语句
  • 还有一个功能是,普通方法也可以右键选择"Printf Mybatis Log"进行动态增强

更详细的演示:studio.youtube.com/video/kWzav...

插件地址:Mybatis Log Ultra - IntelliJ IDEs Plugin | Marketplace

相关推荐
williamyi7412 小时前
mybatis报错org/apache/commons/lang3/tuple/Pair] with root cause
apache·mybatis
bing_15816 小时前
MyBatis Mapper 接口的作用,以及如何将 Mapper 接口与 SQL 映射文件关联起来
数据库·sql·mybatis
就叫飞六吧1 天前
WangEditor快速实现版
node.js·mybatis
自在如风。1 天前
MyBatis-Plus 使用技巧
java·mybatis·mybatis-plus
鱼骨不是鱼翅2 天前
Mybatis操作数据库----小白基础入门
数据库·mybatis
钢板兽2 天前
Java后端高频面经——Spring、SpringBoot、MyBatis
java·开发语言·spring boot·spring·面试·mybatis
嘵奇2 天前
MyBatis-Plus 注解大全
java·mybatis
曹天骄2 天前
使用 MyBatis XML 和 QueryWrapper 实现动态查询
xml·mybatis