在SpringBoot中使用P6Spy攔截SQL語句的完整指南
引言
P6Spy 是一個用于攔截和記錄應(yīng)用程序和數(shù)據(jù)庫之間的所有 JDBC 操作的開源 Java 庫。P6Spy 是無代碼侵入的,也就是說,我們無需修改應(yīng)用程序代碼,只需做一些簡單的配置,即可使用 P6Spy 這個橫在應(yīng)用程序和 JDBC 驅(qū)動之間的「間諜」來捕獲所有使用的 SQL 語句和其執(zhí)行細節(jié)。
因 P6Spy 是在 JDBC 層工作,所以不論我們使用的是市面上哪一款基于 JDBC 之上的 ORM 框架(如:Hibernate、MyBatis),P6Spy 都能很好的捕捉到最終生成的 SQL。
有了 P6Spy 這個工具,我們就可以很好的找出應(yīng)用程序中的慢查詢 SQL 語句、高頻 SQL 語句,從而為數(shù)據(jù)庫優(yōu)化提供數(shù)據(jù)支持。此外,P6Spy 還適用于安全審計或數(shù)據(jù)變更追蹤等場景。
本文先簡單介紹一下 P6Spy 的工作原理,然后以在 Spring Boot 中使用 P6Spy 攔截 SQL 語句為例來演示 P6Spy 的基本功能和使用方式。
1 P6Spy 工作原理
正如本文開頭所提到的,P6Spy 在 JDBC 層工作,像一個間諜一樣「劫持」了 DriverManager,將真實的 JDBC 連接(如:jdbc:mysql//localhost/db)替換為了 P6Spy 的連接(如:jdbc:p6spy:mysql//localhost/db);并將真實的驅(qū)動類(如:com.mysql.cj.jdbc.Driver)替換為了 P6Spy 的驅(qū)動類(com.p6spy.engine.spy.P6SpyDriver)。這樣,應(yīng)用程序針對數(shù)據(jù)庫的所有調(diào)用,都會被 P6Spy 所攔截和處理,所以 P6Spy 可以很容易的捕獲到執(zhí)行的 SQL、耗時情況等信息。
2 在 Spring Boot 中使用 P6Spy
下面,嘗試在 Spring Boot 中使用 P6Spy,本文示例工程使用的 ORM 框架是 JPA,使用的數(shù)據(jù)庫為 MySQL。
寫作本文時,使用的 Java、Spring Boot、P6Spy 的版本為:
Java: 17 Spring Boot: 3.5.5 P6Spy: 3.9.1
2.1 引入 Maven 依賴
在 Spring Boot 中使用 P6Spy 非常的簡單,只需引入一個 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ù)庫為 MySQL,我們希望 P6Spy 幫助捕獲 User 的增刪改查操作,所以需要建一個 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'
);
對應(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 的增刪改查操作,需要編寫一個 UserRepository。
可以看到,我們在如下 UserRepository 編寫了四個方法,前兩個為查詢,后兩個為更新和刪除(分別使用 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ù)庫連接配置。
可以看到,我們并未手動將數(shù)據(jù)庫 URL 更改為 jdbc:p6spy:mysql://xxx,也未手動將 Driver Class 更改為 com.p6spy.engine.spy.P6SpyDriver,完全使用的是實際的數(shù)據(jù)庫 URL 和驅(qū)動類。這是因為,我們引入的不是原始的 P6Spy 依賴包,而是 P6Spy Spring Boot Starter,該 Starter 內(nèi)部會使用裝飾器幫我們修改數(shù)據(jù)源,從而省去了手動的修改。
# 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 初步使用
接下來就可以對 P6Spy 進行使用了。下面我們編寫一個單元測試類 UserRepositoryTest 來對上述 UserRepository 進行測試。
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 語句,并打印了該語句執(zhí)行所花費的時間。
#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 非常的簡單,只需引入一個 P6Spy Starter 即可,無需做額外配置,P6Spy 即可幫我們捕捉 SQL 并統(tǒng)計其耗時信息。
2.5 P6Spy 進階使用
自定義日志輸出格式
從上面的輸出可以看到,P6Spy 默認的輸出除了打印最終 SQL 外,還打印了 Prepared Statement、數(shù)據(jù)庫 URL 等其它信息。如果我們不想要 P6Spy 打印這些額外的信息,可以手動指定需要打印的信息。
要想自定義一些 P6Spy 的參數(shù),就需要在工程 resources 目錄下新建一個 spy.properties。
下面的配置在 spy.properties 中指定了自定義的日志輸出格式。
# spy.properties logMessageFormat=com.p6spy.engine.spy.appender.CustomLineFormat customLogMessageFormat=%(currentTime)|%(executionTime)ms|%(category)|%(sqlSingleLine)
再次運行 UserRepositoryTest 測試類,發(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 打印器
再高級一點的用法是自己編寫 P6Spy 的「打印器」,下面嘗試編寫一個 P6Spy 的「打印器」,除了將時間戳顯示更直觀以外,還嘗試獲取并輸出 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());
}
}
可以看到,該自定義「打印器」實現(xiàn)了 P6Spy 的 MessageFormattingStrategy 接口并重寫了其 formatMessage() 方法。然后,在該方法內(nèi),按照我們的想法,找出了調(diào)用者并格式化了 SQL 抓取時間。
重新運行 UserRepositoryTest 測試類,發(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 默認使用的 Log Appender 是 com.p6spy.engine.spy.appender.Slf4JLogger。所以,P6Spy 的日志默認是輸出到我們應(yīng)用程序的總?cè)罩局械摹?/p>
實際項目中,使用 P6Spy 時為了更好的觀察 SQL 的運行情況,通常需要將 P6Spy 的日志單獨打印到一個文件,并進行單獨管理。
下面即嘗試修改我們示例工程的日志管理文件 logback.xml,將 P6Spy 的日志單獨輸出到一個文件 p6spy.log,并進行存檔和定期清理。
<!-- 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 的日志即會與應(yīng)用程序總?cè)罩痉蛛x,而單獨輸出到 p6spy.log,而且歷史日志也會按要求清理或存檔。
p6spy.log p6spy.2025-08-30.0.log.gz
3 小結(jié)
綜上,我們首先介紹了 P6Spy 的功能、使用場景和工作原理,然后在 Spring Boot 示例工程中引入了 P6Spy Starter,對 P6Spy 基礎(chǔ)功能和進階功能的使用進行了演示。
以上就是在SpringBoot中使用P6Spy攔截SQL語句的完整指南的詳細內(nèi)容,更多關(guān)于SpringBoot P6Spy攔截SQL語句的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Spring Security攔截器引起Java CORS跨域失敗的問題及解決
這篇文章主要介紹了Spring Security攔截器引起Java CORS跨域失敗的問題及解決,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-07-07
SpringMVC響應(yīng)視圖和結(jié)果視圖詳解
這篇文章主要介紹了SpringMVC響應(yīng)視圖和結(jié)果視圖,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-09-09
java DataInputStream和DataOutputStream詳解及實例代碼
這篇文章主要介紹了java DataInputStream和DataOutputStream詳解及實例代碼的相關(guān)資料,需要的朋友可以參考下2017-01-01
Spring Boot 通過注解實現(xiàn)數(shù)據(jù)校驗的方法
這篇文章主要介紹了Spring Boot 通過注解實現(xiàn)數(shù)據(jù)校驗的方法,本文通過實例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-09-09
Java實現(xiàn)WGS84/GCJ02/BD09的坐標互轉(zhuǎn)終極方案
這篇文章主要為大家詳細介紹了Java如何利用Geotools實現(xiàn)WGS84/GCJ02/BD09的坐標互轉(zhuǎn),文中的示例代碼講解詳細,需要的小伙伴可以了解下2025-09-09

