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

相关推荐
loser.loser4 小时前
QQ邮箱发送验证码(Springboot)
java·spring boot·mybatis
毅航6 小时前
Trae复刻Mybatis之旅(一):创建SqlSession会话,构建代理
后端·mybatis·trae
潮流coder8 小时前
mybatis的if判断==‘1‘不生效,改成‘1‘.toString()才生效的原因
java·tomcat·mybatis
BillKu10 小时前
Java + Spring Boot + Mybatis 实现批量插入
java·spring boot·mybatis
dog shit11 小时前
web第十次课后作业--Mybatis的增删改查
android·前端·mybatis
emo了小猫11 小时前
Mybatis #{} 和 ${}区别,使用场景,LIKE模糊查询避免SQL注入
数据库·sql·mysql·mybatis
yuren_xia19 小时前
Spring Boot + MyBatis 集成支付宝支付流程
spring boot·tomcat·mybatis
神仙别闹1 天前
基于Java(SpringBoot、Mybatis、SpringMvc)+MySQL实现(Web)小二结账系统
java·spring boot·mybatis
crud1 天前
Spring Boot 整合 MyBatis-Plus:从入门到精通,一文搞定高效持久层开发!
java·spring boot·mybatis
爱上语文1 天前
MyBatisPlus(3):常用配置
java·后端·mybatis