告别SQL盲猜!6种方案带你玩转SQL打印

关注我的公众号:【编程朝花夕拾】,可获取首发内容。

01 引言

在我们日常开发中,SQL的打印我们不太关注。因为线下的调试,我们通过断点,日志等方式可以快速定位问题。

但是提测之后或者线上,发现数据与预期的数据不符,需要对比SQL的条件。线上的不能随意发布代码,导致排查问题变的困难。

本期,我们从线下到线上整理一下几种好用的打印SQL的方案:

  • ORM内置日志功能
  • P6Spy框架
  • 日志框架
  • Mybaits拦截器
  • AOP切面
  • Arthas打印SQL

02 ORM内置日志功能

Mybaits是一款非常优秀的ORM框架,DAO层的设计它占了半壁江山。而其内部配置本身支持SQL的打印,线下测试的时候,我们经常会将次配置打开,以便随时了解SQL的执行情况。

properties配置

properties 复制代码
# 控制台输出SQL
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl

xml配置

mybatis-configuration.xml

xml 复制代码
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
	<settings>
		<setting name="logImpl" value="STDOUT_LOGGING"/>
	</settings>
</configuration>

properties配置和xml配置,任意配置一项即可。

结果

打印的参数和结果都有了,这样的SQL已经足够我们排查问题。但是不太方便复制值别人查看,需要替换参数才行。Idea的插件Mybatis log free可以帮我们自动填充参数,用起来简直不要太爽。

03 P6Spy 实现

GitHub地址:github.com/p6spy/p6spy

这是一款第三方的数据库日志框架,里面提供了很多配置,可以随意定制日志格式。配置详见:

p6spy.readthedocs.io/en/latest/c...

3.1 Maven依赖

xml 复制代码
<dependency>
    <groupId>p6spy</groupId>
    <artifactId>p6spy</artifactId>
    <version>${latest.version}</version>
</dependency>

3.2 配置修改

properties 复制代码
# 数据源
spring.datasource.driver-class-name=com.p6spy.engine.spy.P6SpyDriver
#spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:p6spy:mysql://127.0.0.1:3306/test
#spring.datasource.url=jdbc:mysql://127.0.0.1:3306/test

需要修改源数据源的参数,具有代码侵入性。

3.3 创建 spy.properties

properties 复制代码
# 部分配置
module.log=com.p6spy.engine.logging.P6LogFactory
appender=com.p6spy.engine.spy.appender.StdoutLogger
logMessageFormat=com.p6spy.engine.spy.appender.CustomLineFormat
customLogMessageFormat=%(executionTime)ms | SQL: %(sqlSingleLine)

3.4 结果

结果可以看到数据库连接的信息,预执行的SQL以及执行的SQL

04 日志框架

日志框架的方式,是可以指定任意包的。

配置

properties 复制代码
# logging.level.xxxx=debug
logging.level.com.simonking.boot.mybaits.mapper.UserInfoNativeMapper=debug

这里指定我们需要打印的Mapper

结果

这种方式也是我们生产上经常使用的动态监控某个包下SQL的方案。此方案需要借助配置中心apollonacos等,动态更新日志的级别,已达到按需打印SQL的目的。

05 Mybaits拦截器

Mybaits拦截器,之前的文章里介绍过,就不再赘述。我们需要拦截的是org.apache.ibatis.executor.statement.StatementHandlerprepare()方法。

java 复制代码
@Intercepts({@Signature(
        type = StatementHandler.class,
        method = "prepare",
        args = {Connection.class, Integer.class})
})
@Component
public class SqlPrintInterceptor implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        StatementHandler handler = (StatementHandler) invocation.getTarget();
        BoundSql boundSql = handler.getBoundSql();
        // 获取原始SQL
        String sql = boundSql.getSql();

        Object param = boundSql.getParameterObject();

        // 美化SQL输出
        System.out.println("\n=============== SQL日志 ===============");
        System.out.println("原始SQL:" + sql);
        System.out.println("SQL: " + sql.replaceAll("\\s+", " ").trim());
        System.out.println("参数: " + JSON.toJSONString(param));
        System.out.println("=====================================");

        return invocation.proceed();
    }
}

结果

06 AOP动态拦截

AOP的做法,需要熟悉调用SQL的方法。实现起来门槛较高,需要了解具体的参数。

我们这里拦截Mapper,和日志框架类似。

java 复制代码
@Aspect
@Component
public class SqlLoggerAspect {

    @Autowired
    private SqlSessionFactory sqlSessionFactory;

    @Around("execution(* com.simonking.boot.mybaits.mapper.UserInfoNativeMapper.*(..))")
    public Object logSql(ProceedingJoinPoint pjp) throws Throwable {
        MethodSignature signature = (MethodSignature)pjp.getSignature();
        Method method = signature.getMethod();
        String namespace = method.getDeclaringClass().getName();
        String methodName = method.getName();

        Configuration configuration = sqlSessionFactory.getConfiguration();
        MappedStatement mappedStatement = configuration.getMappedStatement(namespace + "." + methodName);

        Map<String, Object> map = new HashMap<>();
        Object obj = pjp.getArgs()[0];
        Class<?> clazz = obj.getClass();
        for (Field field : clazz.getDeclaredFields()) {
            field.setAccessible(true);
            String fieldName = field.getName();
            Object value = field.get(obj);
            map.put(fieldName, value);
        }

        BoundSql boundSql = mappedStatement.getBoundSql(map);

        System.out.println("[AOP SQL] " + boundSql.getSql());
        System.out.println("[AOP SQL 参数]: " + JSON.toJSONString(map));
        return pjp.proceed();
    }
}

结果

07 Arthas打印SQL

Arthas是一款Java在线诊断工具,之前也介绍过。用它也可以打印SQL。

sh 复制代码
# 全部参数
watch org.apache.ibatis.executor.SimpleExecutor doQuery '{params[0].resource,params[0].id,params[4].sql,params[4].parameterObject,returnObj,throwExp}' 'params[0].id.contains("xxx.xxxMapper")' -x 3

# 只打印sql和参数
watch org.apache.ibatis.executor.SimpleExecutor doQuery '{params[4].sql,returnObj}' 'params[0].id.contains("xxx.xxxMapper")' -x 3

结果

这种方式在生产上应用也非常使用,根据需要抓取需要的SQL信息。

但是这样的方法是对SQL的注解脚本无效,如:@Select@Update

08 小结

线下的SQL打印可以随意玩,怎么方便怎么来。但是线上的打印SQL的方案就需要谨慎。如果开启打印SQL,那么频繁打印SQL不仅存在数据安全问题,还会引起CPU标高或者线程池变满(我已经被毒打过了,不要轻易去试)。

线上还是推荐按需收集SQL,推荐采用配置中心实时修改日志级别,或者Arthas按需采集SQL

相关推荐
一只叫煤球的猫3 小时前
写代码很6,面试秒变菜鸟?不卖课,面试官视角走心探讨
前端·后端·面试
bobz9654 小时前
tcp/ip 中的多路复用
后端
bobz9654 小时前
tls ingress 简单记录
后端
皮皮林5515 小时前
IDEA 源码阅读利器,你居然还不会?
java·intellij idea
你的人类朋友5 小时前
什么是OpenSSL
后端·安全·程序员
bobz9655 小时前
mcp 直接操作浏览器
后端
前端小张同学8 小时前
服务器部署 gitlab 占用空间太大怎么办,优化思路。
后端
databook8 小时前
Manim实现闪光轨迹特效
后端·python·动效
武子康9 小时前
大数据-98 Spark 从 DStream 到 Structured Streaming:Spark 实时计算的演进
大数据·后端·spark
该用户已不存在9 小时前
6个值得收藏的.NET ORM 框架
前端·后端·.net