环境搭建:数据库表与 Spring Boot 项目集成

好了,前面几章我们把 SQL 审计拦截的理论和核心接口聊得差不多了。这一章咱们直接动手,把基础环境搭起来。我个人习惯先从数据库表开始,这样底层的数据结构定了,上层代码写起来才有谱。

4.1 设计审计日志表

说白了,你需要一张表来记录每一次被拦截的 SQL。我最早做的项目里,这张表只记了 SQL 原文和执行时间,结果排查问题时发现参数的原始值丢了,特别痛苦。所以后来我把字段扩充了一下,分享给你们参考:

字段名 类型 说明
idBIGINT AUTO_INCREMENT主键
sql_idVARCHAR(64)唯一标识,用于去重和追踪
sql_textTEXT预编译后的 SQL(带问号)
paramsTEXT参数列表的 JSON 字符串
execute_timeBIGINT执行耗时,单位毫秒
create_timeDATETIME记录创建时间
user_idVARCHAR(64)可选的:谁执行的
CREATE TABLE `sql_audit_log` (
    `id` BIGINT NOT NULL AUTO_INCREMENT,
    `sql_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT 'sql指纹id',
    `sql_text` TEXT NOT NULL COMMENT '预编译sql',
    `params` TEXT COMMENT '参数json',
    `execute_time` BIGINT NOT NULL DEFAULT 0 COMMENT '执行耗时(ms)',
    `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    `user_id` VARCHAR(64) DEFAULT NULL COMMENT '操作用户',
    PRIMARY KEY (`id`),
    KEY `idx_create_time` (`create_time`),
    KEY `idx_sql_id` (`sql_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
建议:我自己会把 sql_text 和 params 分开存,而不是拼成完整 SQL。为什么呢?因为后续要做 SQL 指纹统计(去掉参数值),单独存参数更方便。sql_id 字段是我后来加的,用 MD5(sql_text) 生成,用来做同类型 SQL 的聚合分析。

4.2 Spring Boot 项目集成

表建好了,现在把 Spring Boot 项目搭起来。假设你已经有一个基本的 Spring Boot 应用,我只说重点部分。

4.2.1 添加依赖

这块我习惯用 MyBatis 和 Druid 连接池,它们配合拦截器非常顺手。当然你也可以用 Hibernate 或 JPA,但拦截机制大同小异。

<!-- pom.xml 核心依赖 -->
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.3.0</version>
</dependency>
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.2.18</version>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.33</version>
    <scope>runtime</scope>
</dependency>

4.2.2 编写拦截器

MyBatis 提供了一个叫 Interceptor 的接口,我们实现它就能拦截 SQL 执行。我记得刚接触时踩过一个坑:必须给类加上 @Intercepts 注解,否则 MyBatis 根本不会调用它。

import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.plugin.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.sql.Connection;
import java.util.Properties;

@Intercepts({
    @Signature(
        type = StatementHandler.class,
        method = "prepare",
        args = {Connection.class, Integer.class}
    )
})
public class SqlAuditInterceptor implements Interceptor {

    private static final Logger log = LoggerFactory.getLogger(SqlAuditInterceptor.class);

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 获取目标 StatementHandler
        StatementHandler handler = (StatementHandler) invocation.getTarget();
        // 拿到 SQL 和参数
        String sql = handler.getBoundSql().getSql();
        Object params = handler.getBoundSql().getParameterObject();

        long start = System.currentTimeMillis();
        try {
            return invocation.proceed();
        } finally {
            long cost = System.currentTimeMillis() - start;
            // 这里只是简单打印,实际可以异步写入数据库
            log.info("SQL: {} | params: {} | cost: {}ms", sql, params, cost);
        }
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {
        // 可读取自定义配置
    }
}
注意:上面例子只是演示。生产环境里,你不能再同步写日志表,否则每次 SQL 执行都会多一次数据库写入,性能直接雪崩。正确做法是把日志扔到内存队列或者 MQ 里异步消费。我后面会专门讲异步处理。

4.2.3 注册拦截器

有两种方式让 MyBatis 知道你的拦截器。

方式一:Spring Boot 自动配置(推荐)

把拦截器声明为一个 Bean,MyBatis 会自动识别。

@Configuration
public class MyBatisConfig {

    @Bean
    public SqlAuditInterceptor sqlAuditInterceptor() {
        return new SqlAuditInterceptor();
    }
}

方式二:XML 配置(如果你还留着 MyBatis XML 配置文件)

<configuration>
    <plugins>
        <plugin interceptor="com.example.SqlAuditInterceptor">
            <property name="someProp" value="100"/>
        </plugin>
    </plugins>
</configuration>

我个人习惯用 Java Config,因为可以更方便地注入其他 Bean(比如日志写入的 Service)。

4.2.4 验证拦截是否生效

启动你的 Spring Boot 应用,随便调用一个 Mapper 的方法。看控制台,如果有类似这样的输出,就说明拦截成功了:

2025-01-15 10:23:45.678  INFO 12345 --- [nio-8080-exec-1] c.e.SqlAuditInterceptor : SQL: select * from user where id = ? | params: 100 | cost: 5ms

如果没出来,检查两点:

  • @Intercepts 注解的 type 写对了吗?以前我写错过 Executor 的类型名……
  • 排查一下你的 Mapper 是不是真的走了 MyBatis,而不是 JPA 或者其他 ORM。

4.3 整体流程一览

下面这张 SVG 图总结了本章搭建的核心链路,从左到右看:

SQL 审计拦截器环境搭建流程 创建审计日志表 添加 Maven 依赖 编写拦截器类 注册到配置 👉 关键要点 1. 表结构包含 sql_text、params、execute_time 三字段 2. 拦截器必须加 @Intercepts 注解指向 StatementHandler.prepare 3. 注册时声明为 @Bean 即可,MyBatis 自动加载 4. 先验证控制台日志再考虑写库

4.4 避坑指南

环境搭建看起来简单,但有几个地方容易让人折腾半天。我来分享下我曾经历过的:

我曾经踩过的几个坑:

  • 拦截器不生效:检查 @Intercepts 的 method 属性。MyBatis 3.5+ 以后 StatementHandler.prepare 的方法签名变了,必须写 {Connection.class, Integer.class},写错一个字都不调用。
  • SQL 参数打印出来是 null:因为 getParameterObject() 返回的是 Object,如果传了多个参数会被封装成 Map,需要自己解析。我在项目中写了一个工具类来统一处理。
  • 日志把生产数据库打挂了:嗯……这个我真干过。测试环境忘了关日志输出,结果一次全表扫描的 SQL 日志把磁盘 IO 撑满了。所以 一定要加上开关,通过配置控制是否启用拦截器。

4.5 快速验收清单

写完代码后,按下面几步快速验证,心里就踏实了:

  1. 建表:执行 SQL,确认表已创建。
  2. 启动项目:无报错,MyBatis 正常加载。
  3. 执行一次 SQL:看控制台是否打印 SQL 日志。
  4. 检查表数据:手动插入一条测试记录到 sql_audit_log,验证表结构能用。

到这步,你的环境就已经跑通了。后面几章我们会基于这个基础,加入参数解析、性能监控、异步写入等进阶功能。别着急,先把这个地基夯实。