配置 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 的介紹就到這裏.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章