第三章:常见框架拦截器对比——MyBatis、Hibernate、JDBC驱动
做SQL审计拦截,第一个问题就是:在哪儿拦?
你想想看,应用层、ORM框架层、驱动层,每层都能插一手。但不同层级的拦截器,能力天差地别。我这些年见过不少团队,选错了拦截点,导致要么漏审、要么性能炸了。
这一章,咱们就掰开揉碎了聊聊三大主流拦截器:MyBatis拦截器、Hibernate拦截器、JDBC驱动拦截器。我会把各自的优缺点、适用场景、以及我踩过的坑,都给你说明白。
3.1 为什么需要对比拦截器?
说白了,选拦截器跟选工具一样——没有最好的,只有最合适的。核心要考虑三点:
- 拦截粒度:能拦住原始SQL还是预编译参数?
- 性能影响:额外开销有多大?会不会拖垮QPS?
- 可扩展性:能不能灵活定制?是否支持复杂规则?
我之前遇到过一家金融公司,直接在DAO层加了个切面审计。结果呢?只拦到了JPA方法调用,真正的SQL和参数完全没抓到。这就像你装了门禁,却让人从窗户爬进来了。
3.2 MyBatis拦截器
先说MyBatis。这是国内最流行的ORM框架之一,它的拦截器机制基于插件(Plugin)体系。
MyBatis允许你拦截四大核心接口:Executor、StatementHandler、ParameterHandler、ResultSetHandler。对于SQL审计,最常用的是前两个。
来看一个最简单的拦截器实现。它拦截Executor接口的update和query方法,拿到完整的SQL和参数:
@Intercepts({
@Signature(type = Executor.class, method = "update",
args = {MappedStatement.class, Object.class}),
@Signature(type = Executor.class, method = "query",
args = {MappedStatement.class, Object.class,
RowBounds.class, ResultHandler.class})
})
public class SqlAuditInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object[] args = invocation.getArgs();
MappedStatement ms = (MappedStatement) args[0];
String sql = ms.getBoundSql(args[1]).getSql();
// 这里替换?为实际参数
String finalSql = buildFinalSql(sql, args[1]);
// 审计逻辑:记录、阻断或告警
if (!AuditEngine.check(finalSql)) {
throw new SecurityException("SQL审计未通过: " + finalSql);
}
return invocation.proceed();
}
private String buildFinalSql(String sql, Object param) {
// 参数绑定解析逻辑
// ...
return resolvedSql;
}
}
嗯,这里要注意,MyBatis拦截器的参数解析有点绕。如果参数是Map或者POJO,你得手动解析BoundSql里的参数映射。我之前在项目里就因为这个,差点漏掉了update语句的where条件。
StatementHandler的prepare方法,能直接拿到BoundSql对象。这样参数解析更稳定,不容易出错。
3.3 Hibernate拦截器
Hibernate走的是另一条路——事件监听+拦截器接口。它的核心是Interceptor接口和EventListeners
Hibernate拦截器有两个切入点:
- Session级拦截器:实现
Interceptor接口,重写onPrepareStatement - SessionFactory级监听器:注册
PreLoadEventListener等
看个例子,拦截Hibernate生成的SQL:
public class HibernateSqlAuditInterceptor implements Interceptor, Serializable {
@Override
public String onPrepareStatement(String sql) {
// 注意:这里的sql是原生SQL,不含参数占位符?
// 参数需要从别的地方获取
if (!AuditEngine.check(sql)) {
throw new RuntimeException("SQL审计拦截: " + sql);
}
return sql;
}
}
发现问题没?onPrepareStatement只给了你SQL模板,没给你参数值。这就尴尬了——你看到的可能是SELECT * FROM users WHERE id = ?,但你不知道?到底是多少。
PreparedStatement的代理。但这样做会导致代码变复杂,而且还没法拦截批量操作。
所以,Hibernate拦截器更适合做粗粒度审计——比如只记录操作了哪张表、什么类型的SQL。拿不到完整SQL?嗯,加个参数绑定工具能办到,但这不是Hibernate的长处。
3.4 JDBC驱动拦截器
最后看JDBC驱动级别的拦截器。这层最底层,但也最“狠”。说白了,它是直接拦截java.sql包下的Connection、PreparedStatement等接口。
常用方式有两种:
- 自定义P6Spy风格驱动:包装原驱动,拦截所有SQL
- JDBC代理驱动:实现
DataSource,返回代理的Connection
用P6Spy举个例子。它其实就是一个JDBC代理驱动:
// spy.properties 配置
driverlist=com.mysql.cj.jdbc.Driver
appender=com.p6spy.engine.spy.appender.CustomAppender
// 自定义审计Appender
public class AuditAppender implements SpyAppender {
@Override
public void log(String message) {
// message格式: "SELECT * FROM users WHERE id=1"
AuditEngine.check(message);
}
}
JDBC拦截器的优势在于对应用无侵入。你只需要改一个driver-class-name,所有ORM框架——MyBatis、Hibernate、甚至JOOQ——全都落在这张网里。我之前接手过一个遗留项目,DAO层混杂了三种ORM,最后就是用JDBC驱动拦截器一把抓,才把审计统一起来。
3.5 三大拦截器对比
我整理了一张对比表,这样看起来更直观:
| 对比维度 | MyBatis拦截器 | Hibernate拦截器 | JDBC驱动拦截器 |
|---|---|---|---|
| 拦截层级 | ORM核心执行层 | Session/事件层 | 原生JDBC驱动层 |
| 获取完整SQL | ✅ 能拿到(含参数) | ⚠️ 只能拿SQL模板 | ✅ 能拿到(含参数) |
| 对应用侵入性 | 中等(需改MyBatis配置) | 中等(需加拦截器bean) | 低(只改驱动配置) |
| 性能影响 | 低(仅在拦截点开销) | 较低(但参数绑定复杂) | 较高(每次请求代理一层) |
| ORM框架兼容性 | 仅MyBatis | 仅Hibernate | 所有ORM(通用) |
| 批量操作支持 | ✅ 支持 | ⚠️ 部分支持 | ✅ 支持 |
| 扩展性 | 高(可自定义拦截逻辑) | 中(事件监听有限) | 高(可完全重写驱动行为) |
选型时,我个人习惯这样判断:
- 如果项目只用MyBatis,优先选MyBatis拦截器。性能最好,控制最细
- 如果项目是Hibernate承载核心业务,用Hibernate拦截器凑合够用。但记得加参数补全
- 如果项目ORM框架混杂,或者对审计要求非常严格(比如金融监管),直接上JDBC驱动拦截器。虽然性能有损耗,但稳
3.6 一张图看懂选型逻辑
下面这张图,梳理了三个拦截器的技术边界和选型策略。画它费了点心思,但看完你就明白什么场景该选什么了。
3.7 避坑指南与个人忠告
这一路走下来,我总结了几条铁律:
- 别想着一个拦截器搞定所有场景。我见过有人试图在MyBatis拦截器里改Hibernate的SQL,这是不可能的。各框架有自己的生命周期,混用只会让代码变得乌烟瘴气。
- 性能不能忽视。尤其是JDBC驱动拦截器,如果审计逻辑里做IO操作(比如写日志、访问外部API),一定要异步化。否则线上一个慢SQL就能把链路拖垮。
- 参数还原永远是难点。不管是MyBatis还是JDBC代理,参数解析的代码一定要写好单元测试。我曾在生产环境遇见过因为参数类型不匹配,导致审计日志里全是
?=??的情况,排查起来让人崩溃。