配置 p6spy log 输出应用最终执行的 sql 语句

介绍了如何使用 p6spy log 输出应用最终执行的 sql 语句, 以方便调试

目录
[隐藏]

在上一篇的 配置 mybatis 打印出执行的 sql 及返回的结果集 中, 说到了在 mybatis 中如何打印出执行的 sql, 但是还是遗留了一个问题, 也即是它的输出的 sql 并不是最终可执行的, 而是类似于 jdbc 那种 PrepareStatement 的形式, 参数的值是用问号代替的, 如下:

select * from user where username = ? and password = ?

虽然其参数值通常也会一起输出, 但如果我们对查询的结果有疑问, 想去数据库里自己执行看看, 就不得不自己去拼凑那些最终的 sql:

select * from user where username = 'admin' and password = '123456';

如果参数特别多的查询, 这会成为一个麻烦. 那么, 是否有方式可以直接输出最终的 sql 呢? 一种方式就是下面将要介绍的 p6spy log.

p6spy log 输出效果

先看其输出的效果:

26:33 #1607390793732 | took 7ms | statement | connection 0| url jdbc:p6spy:mysql://localhost:3306/code_sample?serverTimezone=GMT%2B8
select * from user where username = ? and password = ?
select * from user where username = 'admin' and password = '123456';
26:33 list size: 1

可以看到, 除了那种 PrepareStatement 的形式, 还有最终的 sql. 那么, 要如何去实现这样的效果呢?

另注: 这里的日志布局我启用了一种极简的风格, 只有"分钟:秒数", 具体见 配置简化开发阶段日志输出布局 的介绍.

p6spy log 配置

首先需要引入其依赖, 在 maven 的 pom.xml 文件中加入其依赖:

<dependency>
    <groupId>p6spy</groupId>
    <artifactId>p6spy</artifactId>
    <version>3.9.1</version>
</dependency>

如果你使用其它方式, 如 gradle, 或直接引入 jar 包的方式, 请参考其官网的说明: https://p6spy.readthedocs.io/en/latest/install.html

基本配置

然后是数据库驱动需要换成 p6spy 的驱动及 url, 这里以 mysql 为例, 给出在 application-dev.properties 中的配置:

# p6spy log datasource driver
#spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.driver-class-name=com.p6spy.engine.spy.P6SpyDriver

# p6spy log datasource url
#spring.datasource.url=jdbc:mysql://localhost:3306/code_sample?serverTimezone=GMT%2B8
spring.datasource.url=jdbc:p6spy:mysql://localhost:3306/code_sample?serverTimezone=GMT%2B8

作为对比, 注释的配置为原始的配置.

注: 其它的数据库请参考其 github 及官网的说明: https://github.com/p6spy/p6spyhttps://p6spy.readthedocs.io/en/latest/index.html

另注: 上述配置建议放在你的本地开发环境配置文件中, 通常为 application-dev.properties, 关于 spring-boot 的 分环境配置profile 机制, 如不熟悉请自行查阅网络了解.

警告: 输出 sql 特性通常只是在本地开发调试阶段启动, 如果在生产环境也延续上述配置, 可能导致严重的性能问题!

最后是 p6spy log 本身的一些配置, 在一个叫 spy.properties 的文件中, java 工程里将其放置在 src/resources 目录根下即可, 一些关键的配置及说明如下:

#################################################################
# P6Spy Options File                                            #
# See documentation for detailed instructions                   #
# https://p6spy.readthedocs.io/en/latest/configandusage.html    #
#################################################################

# 根据你实际使用的调整
# 输出到 slf4j 
appender=com.p6spy.engine.spy.appender.Slf4JLogger

# 输出到 stdout
#appender=com.p6spy.engine.spy.appender.StdoutLogger

# 输出到 file
#appender=com.p6spy.engine.spy.appender.FileLogger
# 如果不配置, 默认会在当前工程下生成一个叫 spy.log 的文件
#logfile=C:/log/p6spy/spy.log
# 追加模式, 设置 false 则每次清空文件
#append=true

# 使用多行格式化. 如果使用默认的, 所有的信息会挤在一行, 比较难看
logMessageFormat=com.p6spy.engine.spy.appender.MultiLineFormat

#logMessageFormat=P6SpyLogger

默认的格式化配置

默认的格式化输出内容很多, 而且挤在一行, 其具体格式内容如下:

current time|execution time|category|connection id|datasource url|statement SQL String|effective SQL string

包括当前时间, 执行时间, 类别, 连接 id, 数据源 url, statement sql 和 最终有效的 sql. 输出很长, 不好看, 而且最关键的最终的 sql 语句被挤到最右边去了, 需要拖动才能看到:

26:33 #1607390793732 | took 7ms | statement | connection 0| url jdbc:p6spy:mysql://localhost:3306/code_sample?serverTimezone=GMT%2B8 | select * from user where username = ? and password = ? | select * from user where username = 'admin' and password = '123456';
26:33 list size: 1

多行输出格式化配置

如果想让最终输出的 sql 比较清晰易读, 可以使用一个预定义的多行输出格式化配置.

具体方式是在 spy.properties 文件中定义 logMessageFormat 属性的值为 com.p6spy.engine.spy.appender.MultiLineFormat, 如下所示:

# ... 其余配置略, 见前述内容

# 使用多行格式化. 如果使用默认的, 所有的信息会挤在一行, 比较难看
logMessageFormat=com.p6spy.engine.spy.appender.MultiLineFormat

之后, 输出即可达到文章开头处的换行输出效果:

26:33 #1607390793732 | took 7ms | statement | connection 0| url jdbc:p6spy:mysql://localhost:3306/code_sample?serverTimezone=GMT%2B8
select * from user where username = ? and password = ?
select * from user where username = 'admin' and password = '123456';
26:33 list size: 1

自定义格式化配置

最后, 如果觉得多行的输出还是比较啰嗦, 很多信息你不想看到, 你也可以自定义格式化输出, 在 spy.properties 的文件中, 使用以下配置:

# ... 其余配置略, 见前述内容

# 使用多行格式化. 如果使用默认的, 所有的信息会挤在一行, 比较难看
#logMessageFormat=com.p6spy.engine.spy.appender.MultiLineFormat

# 自定义输出
logMessageFormat=com.p6spy.engine.spy.appender.CustomLineFormat
customLogMessageFormat=\n\nstatement sql:\t%(effectiveSql)\nfinal sql:\t\t%(sql)\n

有两点需要注意:

  1. logMessageFormat 属性使用 com.p6spy.engine.spy.appender.CustomLineFormat.
  2. customLogMessageFormat 属性定义需要的格式.

customLogMessageFormat 中, 除直接量被直接支持外(如"statement sql", "final sql"), 还支持一些特殊的空白符如换行符(\n), 制表符(\t), 以及 p6spy 提供的变量占位符(以 %(XXX) 的形式).

在上述格式化配置中, 只输出了 %(effectiveSql) 以及 %(sql) 这两个比较有价值的信息, 其余则忽略了.

其它一些变量占位符还有如: %(connectionId), %(currentTime), %(executionTime), %(category), %(effectiveSql), %(effectiveSqlSingleLine), %(sql), %(sqlSingleLine) 等.

其中诸如 %(sqlSingleLine) 之类的会把多余的换行符去掉, 最终格式化为一行. 如果是 xml 映射的那种多行 sql, 最终会格式化为一行去显示

更多占位符的详细含义参考其官方文档: https://p6spy.readthedocs.io/en/latest/configandusage.html#customlogmessageformat

最终的输出效果如下:

30:48 HikariPool-1 - Starting...
30:48 HikariPool-1 - Start completed.
30:48 Began transaction (1) for test context [DefaultTestContext@34c01041 testClass = UserDaoTest, testInstance = net.xiaogd.sample.boot.p6spylog.dao.user.UserDaoTest@53941c2f, testMethod = testP6spyLog@UserDaoTest, testException = [null], mergedContextConfiguration = [MergedContextConfiguration@76f4b65 testClass = UserDaoTest, locations = '{}', classes = '{class net.xiaogd.sample.boot.p6spylog.AppApplication}', contextInitializerClasses = '[]', activeProfiles = '{}', propertySourceLocations = '{}', propertySourceProperties = '{org.mybatis.spring.boot.test.autoconfigure.MybatisTestContextBootstrapper=true}', contextCustomizers = set[[ImportsContextCustomizer@c94fd30 key = [org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration, org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration, org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration, org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration, org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration, org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration, org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration, org.mybatis.spring.boot.autoconfigure.MybatisLanguageDriverAutoConfiguration, org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration, org.springframework.boot.test.autoconfigure.jdbc.TestDatabaseAutoConfiguration]], org.springframework.boot.test.context.filter.ExcludeFilterContextCustomizer@dc9876b, org.springframework.boot.test.json.DuplicateJsonObjectContextCustomizerFactory$DuplicateJsonObjectContextCustomizer@a1f72f5, org.springframework.boot.test.mock.mockito.MockitoContextCustomizer@0, org.springframework.boot.test.autoconfigure.OverrideAutoConfigurationContextCustomizerFactory$DisableAutoConfigurationContextCustomizer@2bec854f, org.springframework.boot.test.autoconfigure.actuate.metrics.MetricsExportContextCustomizerFactory$DisableMetricExportContextCustomizer@205d38da, org.springframework.boot.test.autoconfigure.filter.TypeExcludeFiltersContextCustomizer@351584c0, org.springframework.boot.test.autoconfigure.properties.PropertyMappingContextCustomizer@ed3940d1, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverContextCustomizerFactory$Customizer@4e928fbf, org.springframework.boot.test.context.SpringBootTestArgs@1, org.springframework.boot.test.context.SpringBootTestWebEnvironment@0], contextLoader = 'org.springframework.boot.test.context.SpringBootContextLoader', parent = [null]], attributes = map[[empty]]]; transaction manager [org.springframework.jdbc.support.JdbcTransactionManager@73041b7d]; rollback [true]
30:49 

statement sql:  select * from user where username = ? and password = ?
final sql:      select * from user where username = 'admin' and password = '123456'

30:49 list size: 1

以上格式化输出, 为突出 sql 及为了对齐等考虑, 使用了较多的换行及制表符, 读者可根据自身需要取舍.

更多的配置参考其官网的说明: https://p6spy.readthedocs.io/en/latest/configandusage.html

在以上配置完成后, 执行 sql 查询相关操作时, 就能打印出前述的输出效果了.

其它测试相关的代码

测试类代码如下,

package net.xiaogd.sample.boot.p6spylog.dao.user;

import lombok.extern.slf4j.Slf4j;
import net.xiaogd.sample.boot.p6spylog.entity.User;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mybatis.spring.boot.test.autoconfigure.MybatisTest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.test.context.junit.jupiter.SpringExtension;

import java.util.List;

@Slf4j
@ExtendWith(SpringExtension.class)
@MybatisTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
public class UserDaoTest {

    @Autowired
    private UserDao userDao;

    @Test
    public void testP6spyLog() throws Exception {
        List<User> list = userDao.findUserByUsernameAndPassword("admin", "123456");
        log.info("list size: {}", list.size());
    }
}

为加速测试启动, 避免加载与 Dao 无关的 bean, 这里使用了 @MybatisTest 注解. 其相关模块 mybatis-spring-boot-starter-test 的 maven 依赖如下:

<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter-test</artifactId>
    <scope>test</scope>
    <version>2.1.4</version>
</dependency>

另注: 这里使用了最新的 Junit 5, 而不是传统的 Junit 4 的方式.

关于两者的区别, 请参考 Junit 官网的迁移指南: https://junit.org/junit5/docs/current/user-guide/#migrating-from-junit4

在上述代码中, 一个主要区别是 @ExtendWith(SpringExtension.class) 取代了 junit 4 的 @RunWith(SpringRunner.class)

Dao 类如下, 这里为方便, 简单使用了注解方式:

package net.xiaogd.sample.boot.p6spylog.dao.user;

import net.xiaogd.sample.boot.p6spylog.entity.User;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import org.springframework.stereotype.Repository;

import java.util.List;

@Mapper
@Repository
public interface UserDao {
    @Select("select * from user where username = #{username} and password = #{password}")
    List<User> findUserByUsernameAndPassword(String username, String password);
}

注: 如上 @Repository 注解不是必要的, 但 Idea IDE 如果没有相关插件支持可能会导致一些警告或报错信息, 加上该注解可以避免报错.

输出最终的 sql 还有其它的方式, 比如如果你使用了 druid 连接池, 它也有相关的配置能输出最终的 sql; 另外还有如果你使用的是 log4j, 还有 log4jdbc spy log 的方式也能输出最终的 sql.

由于篇幅关系及与本篇主题不符, 这里不一一介绍. 如果你有相关需求, 欢迎留言, 关于使用 p6spy log 输出最终 sql 的介绍就到这里.