在上一篇的 配置 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/p6spy 和 https://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
有兩點需要注意:
logMessageFormat
屬性使用com.p6spy.engine.spy.appender.CustomLineFormat
.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 的介紹就到這裏.