引入P6Spy是为了在不改动业务代码的前提下,增强我们对数据库交互的可观测性。帮助我们主动发现性能问题(如:慢查询),在开发阶段提供便利的调试信息,并通过自定义日志处理提供了一种基础的数据安全保护手段。这对于提升应用质量、保障系统稳定性和安全性具有重要意义!

为什么推荐使用P6Spy?

  • P6Spy通过JDBC驱动代理的方式工作,对应用程序代码完全透明。我们无需修改任何业务逻辑或数据访问代码,只需简单配置数据源 URL 和驱动类即可启用。

  • 能捕获并记录应用程序发送到数据库的每一条 SQL 语句及其执行时间。这对于理解应用的实际数据库行为、排查问题和性能分析非常有价值。

  • P6Spy 允许我们设置慢查询阈值,当 SQL 执行时间超过这个阈值时,P6Spy 会特别标记这些查询。这使我们能够快速识别出性能瓶颈,优先优化那些对系统响应时间影响最大的 SQL 语句,而不是盲目地优化。

  • 在开发和测试环境中,开启 P6Spy 可以让开发者实时看到执行的 SQL,这对于调试 ORM (如 JPA/Hibernate) 映射问题、验证查询逻辑是否正确非常有帮助。它能清晰地展示框架实际生成的 SQL,避免“黑盒”操作带来的困惑。

  • 通过实现自定义的 MessageFormattingStrategy,我们可以在 SQL 语句被记录到日志之前,对其进行处理。例如:本例子代码中,我们利用正则表达式识别并替换了身份证号,实现了基础的日志脱敏功能。这在记录生产环境日志时,能有效防止敏感信息(如身份证号、手机号等)泄露,满足基本的安全和合规要求。

代码实操

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.33</version>
        </dependency>

        <!-- P6Spy dependency -->
        <dependency>
            <groupId>p6spy</groupId>
            <artifactId>p6spy</artifactId>
            <version>3.9.1</version>
        </dependency>

spy.properties

src/main/resources/spy.properties

# 指定应用的日志系统 (使用SLF4J,与Spring Boot默认日志一致)
appender=com.p6spy.engine.spy.appender.Slf4JLogger

# 自定义日志消息格式类
logMessageFormat=com.example.demo.config.P6SpyLogger

# 设置要记录的SQL类别
filter=true
includeCategories=statement,prepared

# 启用慢查询检测 (可选,但日志格式化器中已手动实现)
# outageDetection=true
# outageDetectionInterval=1

# 排除某些表或SQL语句(可选)
# exclude=select 1

# 自定义驱动列表 (可选,但如果遇到驱动加载问题可尝试添加)
# driverlist=com.mysql.cj.jdbc.Driver

application.properties

请注意看清楚是使用 jdbc:p6spy:mysql:// 作为前缀

# MySQL Database configuration
# 注意使用 jdbc:p6spy:mysql:// 作为前缀
spring.datasource.url=jdbc:p6spy:mysql://localhost:3306/mydbtest_spy?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true
spring.datasource.driver-class-name=com.p6spy.engine.spy.P6SpyDriver
spring.datasource.username=root
spring.datasource.password=12345678

# JPA/Hibernate properties
spring.jpa.database-platform=org.hibernate.dialect.MySQL8Dialect
# 在生产环境中应设置为 validate 或 none
spring.jpa.hibernate.ddl-auto=create-drop
# 禁用Hibernate自带的SQL显示,使用P6Spy
spring.jpa.show-sql=false
spring.jpa.properties.hibernate.format_sql=true

# 日志级别设置,方便查看P6Spy输出
logging.level.com.example.demo=DEBUG
logging.level.p6spy=DEBUG

P6SpyLogger

package com.example.demo.config;

import com.p6spy.engine.spy.appender.MessageFormattingStrategy;
import org.springframework.stereotype.Component;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * 自定义 P6Spy 日志格式化策略
 * 实现慢查询日志记录和身份证字段过滤
 */
@Component
publicclass P6SpyLogger implements MessageFormattingStrategy {

    // 中国大陆18位身份证号正则表达式
    privatestaticfinal String ID_CARD_PATTERN = "\\d{17}[\\dXx]";
    // 用于替换身份证号的掩码
    privatestaticfinal String MASK = "****MASKED****";

    /**
     * 格式化P6Spy输出的消息
     * @param connectionId 数据库连接ID
     * @param now 当前时间字符串 (已弃用)
     * @param elapsed 执行耗时(毫秒)
     * @param category SQL类别 (statement, prepared, resultset等)
     * @param prepared 预编译SQL (如果可用)
     * @param sql 原始SQL
     * @param url 数据库URL
     * @return 格式化后的日志消息
     */
    @Override
    public String formatMessage(int connectionId, String now, long elapsed, String category, String prepared, String sql, String url) {
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");

        // 优先使用预编译SQL,因为它通常包含参数占位符,更清晰
        String effectiveSql = (prepared != null && !prepared.isEmpty()) ? prepared : sql;

        // 对SQL进行脱敏处理
        String maskedSql = maskIdCard(effectiveSql);

        // 构建日志消息
        StringBuilder logEntry = new StringBuilder();
        logEntry.append(dateFormat.format(new Date()))
                .append(" | took ")
                .append(elapsed)
                .append("ms");

        // 标记慢查询 (假设阈值为1000ms)
        if (elapsed > 1000) {
             logEntry.append(" [SLOW QUERY!]");
        }

        logEntry.append(" | ")
                .append(category)
                .append(" | connection ")
                .append(connectionId)
                .append(" | url ")
                .append(url)
                .append(System.lineSeparator())
                .append(maskedSql)
                .append(";");

        return logEntry.toString();
    }

    /**
     * 使用正则表达式查找并替换SQL中的身份证号码
     * @param sql 原始SQL字符串
     * @return 脱敏后的SQL字符串
     */
    private String maskIdCard(String sql) {
        if (sql == null || sql.isEmpty()) {
            return sql;
        }
        Pattern pattern = Pattern.compile(ID_CARD_PATTERN);
        Matcher matcher = pattern.matcher(sql);
        return matcher.replaceAll(MASK);
    }
}

Entity

package com.example.demo.entity;

import javax.persistence.*;

@Entity
@Table(name = "users")
publicclass User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "name")
    private String name;

    @Column(name = "id_card")
    private String idCard; // 身份证字段

    // Getters and Setters
    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getIdCard() {
        return idCard;
    }

    public void setIdCard(String idCard) {
        this.idCard = idCard;
    }
}

Repository

package com.example.demo.repository;

import com.example.demo.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;

public interface UserRepository extends JpaRepository<User, Long> {
}

Service

package com.example.demo.service;

import com.example.demo.entity.User;
import com.example.demo.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
publicclass UserService {

    @Autowired
    private UserRepository userRepository;

    public List<User> findAllUsers() {
        // 模拟慢查询,休眠1.5秒
        try {
            Thread.sleep(1500);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        return userRepository.findAll();
    }

    public User saveUser(User user) {
        return userRepository.save(user);
    }
}

Controller

package com.example.demo.controller;

import com.example.demo.entity.User;
import com.example.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/users")
publicclass UserController {

    @Autowired
    private UserService userService;

    @GetMapping
    public List<User> getAllUsers() {
        return userService.findAllUsers();
    }

    @PostMapping
    public User createUser(@RequestBody User user) {
        return userService.saveUser(user);
    }
}
package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }

}

关注我,送Java福利

/**
 * 这段代码只有Java开发者才能看得懂!
 * 关注我微信公众号之后,
 * 发送:"666",
 * 即可获得一本由Java大神一手面试经验诚意出品
 * 《Java开发者面试百宝书》Pdf电子书
 * 福利截止日期为2025年02月28日止
 * 手快有手慢没!!!
*/
System.out.println("请关注我的微信公众号:");
System.out.println("Java知识日历");
Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐