在SpringBoot中使用P6Spy攔截SQL語(yǔ)句的完整指南
引言
P6Spy 是一個(gè)用于攔截和記錄應(yīng)用程序和數(shù)據(jù)庫(kù)之間的所有 JDBC 操作的開(kāi)源 Java 庫(kù)。P6Spy 是無(wú)代碼侵入的,也就是說(shuō),我們無(wú)需修改應(yīng)用程序代碼,只需做一些簡(jiǎn)單的配置,即可使用 P6Spy 這個(gè)橫在應(yīng)用程序和 JDBC 驅(qū)動(dòng)之間的「間諜」來(lái)捕獲所有使用的 SQL 語(yǔ)句和其執(zhí)行細(xì)節(jié)。
因 P6Spy 是在 JDBC 層工作,所以不論我們使用的是市面上哪一款基于 JDBC 之上的 ORM 框架(如:Hibernate、MyBatis),P6Spy 都能很好的捕捉到最終生成的 SQL。
有了 P6Spy 這個(gè)工具,我們就可以很好的找出應(yīng)用程序中的慢查詢 SQL 語(yǔ)句、高頻 SQL 語(yǔ)句,從而為數(shù)據(jù)庫(kù)優(yōu)化提供數(shù)據(jù)支持。此外,P6Spy 還適用于安全審計(jì)或數(shù)據(jù)變更追蹤等場(chǎng)景。
本文先簡(jiǎn)單介紹一下 P6Spy 的工作原理,然后以在 Spring Boot 中使用 P6Spy 攔截 SQL 語(yǔ)句為例來(lái)演示 P6Spy 的基本功能和使用方式。
1 P6Spy 工作原理
正如本文開(kāi)頭所提到的,P6Spy 在 JDBC 層工作,像一個(gè)間諜一樣「劫持」了 DriverManager,將真實(shí)的 JDBC 連接(如:jdbc:mysql//localhost/db)替換為了 P6Spy 的連接(如:jdbc:p6spy:mysql//localhost/db);并將真實(shí)的驅(qū)動(dòng)類(如:com.mysql.cj.jdbc.Driver)替換為了 P6Spy 的驅(qū)動(dòng)類(com.p6spy.engine.spy.P6SpyDriver)。這樣,應(yīng)用程序針對(duì)數(shù)據(jù)庫(kù)的所有調(diào)用,都會(huì)被 P6Spy 所攔截和處理,所以 P6Spy 可以很容易的捕獲到執(zhí)行的 SQL、耗時(shí)情況等信息。
2 在 Spring Boot 中使用 P6Spy
下面,嘗試在 Spring Boot 中使用 P6Spy,本文示例工程使用的 ORM 框架是 JPA,使用的數(shù)據(jù)庫(kù)為 MySQL。
寫作本文時(shí),使用的 Java、Spring Boot、P6Spy 的版本為:
Java: 17 Spring Boot: 3.5.5 P6Spy: 3.9.1
2.1 引入 Maven 依賴
在 Spring Boot 中使用 P6Spy 非常的簡(jiǎn)單,只需引入一個(gè) p6spy-spring-boot-starter 依賴即可。下面即是我們的示例工程用到的主要依賴,有 JPA、Lombok、MySQL Connector 和 P6Spy Starter。
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.38</version>
<scope>provided</scope>
</dependency>
<!-- driver -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>9.4.0</version>
<scope>runtime</scope>
</dependency>
<!-- p6spy -->
<dependency>
<groupId>com.github.gavlyukovskiy</groupId>
<artifactId>p6spy-spring-boot-starter</artifactId>
<version>1.12.0</version>
</dependency>
</dependencies>
2.2 建表、建 Entity、建 Repository
本示例工程使用的數(shù)據(jù)庫(kù)為 MySQL,我們希望 P6Spy 幫助捕獲 User 的增刪改查操作,所以需要建一個(gè) user 表:
DROP TABLE IF EXISTS user;
CREATE TABLE user (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(20) NOT NULL,
email VARCHAR(20) NOT NULL,
year_of_birth INT NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT '2025-01-01 00:00:00',
updated_at TIMESTAMP NOT NULL DEFAULT '2025-01-01 00:00:00'
);
對(duì)應(yīng) user 表的 Java Entity 為 User,其源碼如下:
package com.example.demo.model;
@Data
@Entity
@Table(name = "user")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String email;
private Integer yearOfBirth;
private Date createdAt;
private Date updatedAt;
}
我們使用的 ORM 框架為 JPA,為了支撐 User 的增刪改查操作,需要編寫一個(gè) UserRepository。
可以看到,我們?cè)谌缦?UserRepository 編寫了四個(gè)方法,前兩個(gè)為查詢,后兩個(gè)為更新和刪除(分別使用 HQL 和原生 SQL)。
package com.example.demo.repository;
public interface UserRepository extends CrudRepository<User, Long> {
boolean existsByNameAndEmail(String name, String email);
List<User> findByNameIgnoreCase(String name);
@Transactional
@Modifying
@Query("UPDATE User u SET u.name = :name WHERE u.email = :email")
void updateNameByEmail(@Param("name") String name, @Param("email") String email);
@Transactional
@Modifying
@Query(value = "DELETE FROM user WHERE email = :email", nativeQuery = true)
void deleteByEmail(@Param("email") String email);
}
2.3 添加配置
下面在 application.yaml 添加數(shù)據(jù)庫(kù)連接配置。
可以看到,我們并未手動(dòng)將數(shù)據(jù)庫(kù) URL 更改為 jdbc:p6spy:mysql://xxx,也未手動(dòng)將 Driver Class 更改為 com.p6spy.engine.spy.P6SpyDriver,完全使用的是實(shí)際的數(shù)據(jù)庫(kù) URL 和驅(qū)動(dòng)類。這是因?yàn)?,我們引入的不是原始?P6Spy 依賴包,而是 P6Spy Spring Boot Starter,該 Starter 內(nèi)部會(huì)使用裝飾器幫我們修改數(shù)據(jù)源,從而省去了手動(dòng)的修改。
# application.yaml
spring:
datasource:
url: jdbc:mysql://localhost:3306/test?autoReconnect=true&useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT%2B8
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
show-sql: false
2.4 P6Spy 初步使用
接下來(lái)就可以對(duì) P6Spy 進(jìn)行使用了。下面我們編寫一個(gè)單元測(cè)試類 UserRepositoryTest 來(lái)對(duì)上述 UserRepository 進(jìn)行測(cè)試。
package com.example.demo.repository;
@SpringBootTest
public class UserRepositoryTest {
@Autowired
private UserRepository userRepository;
@Test
@Order(1)
public void testSave() {
User user = new User();
user.setName("Larry");
user.setEmail("larry@larry.com");
user.setYearOfBirth(2000);
user.setCreatedAt(new Date());
user.setUpdatedAt(new Date());
userRepository.save(user);
}
// ...
}
調(diào)用后,發(fā)現(xiàn)應(yīng)用程序日志里多了如下一行輸出??梢钥吹?,P6Spy 起作用了,其捕獲了最終的 SQL 語(yǔ)句,并打印了該語(yǔ)句執(zhí)行所花費(fèi)的時(shí)間。
#1756628295717 | took 29ms | statement | connection 2| url jdbc:mysql://localhost:3306/test?autoReconnect=true&useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT%2B8
insert into user (created_at,email,name,updated_at,year_of_birth) values (?,?,?,?,?)
insert into user (created_at,email,name,updated_at,year_of_birth) values ('2025-08-31T16:18:15.557+0800','larry@larry.com','Larry','2025-08-31T16:18:15.557+0800',2000);
至此,我們可以看到,在 Spring Boot 中使用 P6Spy 非常的簡(jiǎn)單,只需引入一個(gè) P6Spy Starter 即可,無(wú)需做額外配置,P6Spy 即可幫我們捕捉 SQL 并統(tǒng)計(jì)其耗時(shí)信息。
2.5 P6Spy 進(jìn)階使用
自定義日志輸出格式
從上面的輸出可以看到,P6Spy 默認(rèn)的輸出除了打印最終 SQL 外,還打印了 Prepared Statement、數(shù)據(jù)庫(kù) URL 等其它信息。如果我們不想要 P6Spy 打印這些額外的信息,可以手動(dòng)指定需要打印的信息。
要想自定義一些 P6Spy 的參數(shù),就需要在工程 resources 目錄下新建一個(gè) spy.properties。
下面的配置在 spy.properties 中指定了自定義的日志輸出格式。
# spy.properties logMessageFormat=com.p6spy.engine.spy.appender.CustomLineFormat customLogMessageFormat=%(currentTime)|%(executionTime)ms|%(category)|%(sqlSingleLine)
再次運(yùn)行 UserRepositoryTest 測(cè)試類,發(fā)現(xiàn) P6Spy 的打印信息變了。
1756628485593|31ms|statement|insert into user (created_at,email,name,updated_at,year_of_birth) values ('2025-08-31T16:21:25.354+0800','larry@larry.com','Larry','2025-08-31T16:21:25.354+0800',2000)
自定義 P6Spy 打印器
再高級(jí)一點(diǎn)的用法是自己編寫 P6Spy 的「打印器」,下面嘗試編寫一個(gè) P6Spy 的「打印器」,除了將時(shí)間戳顯示更直觀以外,還嘗試獲取并輸出 SQL 操作的調(diào)用者(Caller)。
下面的配置即在 spy.properties 指定了自己編寫的「打印器」。
# spy.properties logMessageFormat=com.example.demo.formatter.CustomP6SpyMessageFormatter
該「打印器」的源碼如下:
package com.example.demo.formatter;
public class CustomP6SpyMessageFormatter implements MessageFormattingStrategy {
@Override
public String formatMessage(int connId, String now, long elapsed, String category, String prepared, String sql, String url) {
if (StringUtils.isBlank(sql)) {
return "";
}
String caller = Arrays.stream(Thread.currentThread().getStackTrace())
.filter(e -> e.getClassName().startsWith("com.example.demo"))
.filter(e -> !e.getClassName().equals(CustomP6SpyMessageFormatter.class.getName()))
.findFirst()
.map(e -> String.format("%s#%s:%d", e.getClassName(), e.getMethodName(), e.getLineNumber()))
.orElse("Unknown");
String nowFormatted = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS")
.format(new Date(Long.parseLong(now)));
return String.format("%s - [Time elapsed: %d ms] [Caller: %s] [SQL: %s]", nowFormatted, elapsed, caller, sql.trim());
}
}
可以看到,該自定義「打印器」實(shí)現(xiàn)了 P6Spy 的 MessageFormattingStrategy 接口并重寫了其 formatMessage() 方法。然后,在該方法內(nèi),按照我們的想法,找出了調(diào)用者并格式化了 SQL 抓取時(shí)間。
重新運(yùn)行 UserRepositoryTest 測(cè)試類,發(fā)現(xiàn) P6Spy 打印的日志信息變?yōu)榱宋覀兯远x的。
2025-08-31 16:25:09.685 - [Time elapsed: 31 ms] [Caller: com.example.demo.repository.UserRepositoryTest#testSave:27] [SQL: insert into user (created_at,email,name,updated_at,year_of_birth) values ('2025-08-31T16:25:09.483+0800','larry@larry.com','Larry','2025-08-31T16:25:09.483+0800',2000)]
將 P6Spy 日志與應(yīng)用程序日志分離
P6Spy 默認(rèn)使用的 Log Appender 是 com.p6spy.engine.spy.appender.Slf4JLogger。所以,P6Spy 的日志默認(rèn)是輸出到我們應(yīng)用程序的總?cè)罩局械摹?/p>
實(shí)際項(xiàng)目中,使用 P6Spy 時(shí)為了更好的觀察 SQL 的運(yùn)行情況,通常需要將 P6Spy 的日志單獨(dú)打印到一個(gè)文件,并進(jìn)行單獨(dú)管理。
下面即嘗試修改我們示例工程的日志管理文件 logback.xml,將 P6Spy 的日志單獨(dú)輸出到一個(gè)文件 p6spy.log,并進(jìn)行存檔和定期清理。
<!-- logback.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
...
<appender name="P6SPY_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_HOME}/p6spy.log</file>
<encoder>
<pattern>%msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${LOG_HOME}/p6spy.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
<maxFileSize>100MB</maxFileSize>
<maxHistory>7</maxHistory>
<totalSizeCap>2GB</totalSizeCap>
</rollingPolicy>
</appender>
<logger name="p6spy" level="INFO" additivity="false">
<appender-ref ref="P6SPY_FILE"/>
<appender-ref ref="CONSOLE"/>
</logger>
...
</configuration>
這樣,P6Spy 的日志即會(huì)與應(yīng)用程序總?cè)罩痉蛛x,而單獨(dú)輸出到 p6spy.log,而且歷史日志也會(huì)按要求清理或存檔。
p6spy.log p6spy.2025-08-30.0.log.gz
3 小結(jié)
綜上,我們首先介紹了 P6Spy 的功能、使用場(chǎng)景和工作原理,然后在 Spring Boot 示例工程中引入了 P6Spy Starter,對(duì) P6Spy 基礎(chǔ)功能和進(jìn)階功能的使用進(jìn)行了演示。
以上就是在SpringBoot中使用P6Spy攔截SQL語(yǔ)句的完整指南的詳細(xì)內(nèi)容,更多關(guān)于SpringBoot P6Spy攔截SQL語(yǔ)句的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
IDEA配置Vue項(xiàng)目啟動(dòng)器的過(guò)程
文章介紹了兩種啟動(dòng)方式一是通過(guò)控制臺(tái)手動(dòng)輸入指令并用Ctrl+C終止;二是配置npm啟動(dòng)器以簡(jiǎn)化測(cè)試流程,提升效率2025-07-07
java.lang.NullPointerException異常問(wèn)題解決方案
這篇文章主要介紹了java.lang.NullPointerException異常問(wèn)題解決方案,本篇文章通過(guò)簡(jiǎn)要的案例,講解了該項(xiàng)技術(shù)的了解與使用,以下就是詳細(xì)內(nèi)容,需要的朋友可以參考下2021-08-08
Java類的序列化版本唯一標(biāo)識(shí)符serialVersionUID使用
serialVersionUID是一個(gè)類的序列化版本唯一標(biāo)識(shí)符,用于確保在反序列化過(guò)程中類的實(shí)例與序列化文件中的類版本相匹配,它在版本兼容性和安全性方面起著關(guān)鍵作用2025-01-01
SpringBoot搭建Dubbo項(xiàng)目實(shí)現(xiàn)斐波那契第n項(xiàng)詳解
這篇文章主要講解了“SpringBoot+Dubbo怎么實(shí)現(xiàn)斐波那契第N項(xiàng)”,文中的講解內(nèi)容簡(jiǎn)單清晰,易于學(xué)習(xí)與理解,下面請(qǐng)大家跟著小編的思路慢慢深入,一起來(lái)研究和學(xué)習(xí)吧2022-06-06
Java8需要知道的4個(gè)函數(shù)式接口簡(jiǎn)單教程
這篇文章主要介紹了Java?8中引入的函數(shù)式接口,包括Consumer、Supplier、Predicate和Function,以及它們的用法和特點(diǎn),文中通過(guò)代碼介紹的非常詳細(xì),需要的朋友可以參考下2025-03-03
Spring中的@ConditionalOnProperty作用和用法詳解
這篇文章主要介紹了Spring中的@ConditionalOnProperty作用和用法詳解,在spring boot中有時(shí)候需要控制配置類是否生效,可以使用@ConditionalOnProperty注解來(lái)控制@Configuration是否生效,需要的朋友可以參考下2023-11-11

