告别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

相关推荐
Mintopia7 分钟前
🎬《Next 全栈 CRUD 的百老汇》
前端·后端·next.js
hqxstudying20 分钟前
MyBatis 和 MyBatis-Plus对比
java·数据库·mysql·mybatis
源码哥_博纳软云28 分钟前
JAVA国际版多商户运营版商城系统源码多商户社交电商系统源码支持Android+IOS+H5
android·java·ios·微信·微信小程序·小程序·uni-app
CF14年老兵38 分钟前
深入浅出 Python 一等函数:一份友好的全面解析
后端·python·trae
猿java40 分钟前
为什么复杂的架构一定要做分层设计?
java·面试·架构
whitepure41 分钟前
万字详解常用数据结构(Java版)
java·数据结构·后端
天天摸鱼的java工程师43 分钟前
你们公司的 QPS 是怎么统计出来的?这 5 种常见方法我踩过一半的坑
java·后端·面试
guojl44 分钟前
Gateway使用手册
后端·微服务
BingoGo1 小时前
PHP 内存管理 深入理解 PHP 的引用和垃圾回收
后端·php
whitepure1 小时前
万字详解常用算法(Java版)
java·后端·算法