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

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
的方案。此方案需要借助配置中心apollo
、nacos
等,动态更新日志的级别,已达到按需打印SQL
的目的。
05 Mybaits拦截器
Mybaits
拦截器,之前的文章里介绍过,就不再赘述。我们需要拦截的是org.apache.ibatis.executor.statement.StatementHandler
的prepare()
方法。
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
。