环境搭建:数据库表与 Spring Boot 项目集成
好了,前面几章我们把 SQL 审计拦截的理论和核心接口聊得差不多了。这一章咱们直接动手,把基础环境搭起来。我个人习惯先从数据库表开始,这样底层的数据结构定了,上层代码写起来才有谱。
4.1 设计审计日志表
说白了,你需要一张表来记录每一次被拦截的 SQL。我最早做的项目里,这张表只记了 SQL 原文和执行时间,结果排查问题时发现参数的原始值丢了,特别痛苦。所以后来我把字段扩充了一下,分享给你们参考:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | BIGINT AUTO_INCREMENT | 主键 |
| sql_id | VARCHAR(64) | 唯一标识,用于去重和追踪 |
| sql_text | TEXT | 预编译后的 SQL(带问号) |
| params | TEXT | 参数列表的 JSON 字符串 |
| execute_time | BIGINT | 执行耗时,单位毫秒 |
| create_time | DATETIME | 记录创建时间 |
| user_id | VARCHAR(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) {
// 可读取自定义配置
}
}
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 图总结了本章搭建的核心链路,从左到右看:
4.4 避坑指南
环境搭建看起来简单,但有几个地方容易让人折腾半天。我来分享下我曾经历过的:
我曾经踩过的几个坑:
- 拦截器不生效:检查
@Intercepts的method属性。MyBatis 3.5+ 以后StatementHandler.prepare的方法签名变了,必须写{Connection.class, Integer.class},写错一个字都不调用。 - SQL 参数打印出来是 null:因为
getParameterObject()返回的是Object,如果传了多个参数会被封装成Map,需要自己解析。我在项目中写了一个工具类来统一处理。 - 日志把生产数据库打挂了:嗯……这个我真干过。测试环境忘了关日志输出,结果一次全表扫描的 SQL 日志把磁盘 IO 撑满了。所以 一定要加上开关,通过配置控制是否启用拦截器。
4.5 快速验收清单
写完代码后,按下面几步快速验证,心里就踏实了:
- 建表:执行 SQL,确认表已创建。
- 启动项目:无报错,MyBatis 正常加载。
- 执行一次 SQL:看控制台是否打印 SQL 日志。
- 检查表数据:手动插入一条测试记录到
sql_audit_log,验证表结构能用。
到这步,你的环境就已经跑通了。后面几章我们会基于这个基础,加入参数解析、性能监控、异步写入等进阶功能。别着急,先把这个地基夯实。