告别SQL盲猜!5种打印SQL语句的实战方案

01 引言

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

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

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

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

02 ORM内置日志功能

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

properties配置

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

xml配置

mybatis-configuration.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地址:https://github.com/p6spy/p6spy

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

https://p6spy.readthedocs.io/en/latest/configandusage.html

3.1 Maven依赖

<dependency>
    <groupId>p6spy</groupId>
    <artifactId>p6spy</artifactId>
    <version>${latest.version}</version>
</dependency>

3.2 配置修改

# 数据源
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

# 部分配置
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 日志框架

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

配置

# 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()方法。

@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,和日志框架类似。

@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。

# 全部参数
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

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

智_永无止境

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值