第三章:常见框架拦截器对比——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审计,最常用的是前两个。

核心原理:MyBatis通过动态代理 + 责任链模式,在执行前/后注入拦截逻辑。

来看一个最简单的拦截器实现。它拦截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 = ?,但你不知道?到底是多少。

我曾经在项目中踩过这个坑:Hibernate的拦截器,想拿到完整参数需要额外绑定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驱动拦截器一把抓,才把审计统一起来。

核心差异:JDBC驱动拦截器能看到最原始的SQL和参数,这是它能做到“通吃”的底气。但代价是性能开销更大,因为它在每次JDBC调用时都要额外走一层代理。

3.5 三大拦截器对比

我整理了一张对比表,这样看起来更直观:

对比维度 MyBatis拦截器 Hibernate拦截器 JDBC驱动拦截器
拦截层级 ORM核心执行层 Session/事件层 原生JDBC驱动层
获取完整SQL ✅ 能拿到(含参数) ⚠️ 只能拿SQL模板 ✅ 能拿到(含参数)
对应用侵入性 中等(需改MyBatis配置) 中等(需加拦截器bean) 低(只改驱动配置)
性能影响 低(仅在拦截点开销) 较低(但参数绑定复杂) 较高(每次请求代理一层)
ORM框架兼容性 仅MyBatis 仅Hibernate 所有ORM(通用)
批量操作支持 ✅ 支持 ⚠️ 部分支持 ✅ 支持
扩展性 高(可自定义拦截逻辑) 中(事件监听有限) 高(可完全重写驱动行为)

选型时,我个人习惯这样判断:

  • 如果项目只用MyBatis,优先选MyBatis拦截器。性能最好,控制最细
  • 如果项目是Hibernate承载核心业务,用Hibernate拦截器凑合够用。但记得加参数补全
  • 如果项目ORM框架混杂,或者对审计要求非常严格(比如金融监管),直接上JDBC驱动拦截器。虽然性能有损耗,但稳
避坑技巧:如果你选JDBC驱动拦截器,建议用异步审计。比如把拦截到的SQL塞入队列,由后台线程处理。这样一来,审计本身不作为请求同步链路的一部分,性能影响能降到最低。

3.6 一张图看懂选型逻辑

下面这张图,梳理了三个拦截器的技术边界和选型策略。画它费了点心思,但看完你就明白什么场景该选什么了。

JDBC拦截器与ORM拦截器选型逻辑图 三大拦截器对比与选型逻辑 JDBC驱动拦截器 拦截java.sql接口 拿到完整SQL+参数 无侵入·性能开销大 适用:多ORM混杂场景 推荐:金融监管、审计严格 MyBatis拦截器 拦截Executor/StatementHandler 拿到SQL模板+参数 低开销·高扩展性 适用:纯MyBatis项目 推荐:优先选择 Hibernate拦截器 拦截Interceptor/EventListener 只能拿SQL模板 参数绑定需要额外代码 适用:Hibernate为主 注意:需补全参数逻辑 根据场景选择拦截器 项目是否 只用一种ORM? 否 用JDBC驱动拦截器 是 用ORM原生拦截器 选型核心:没有银弹,按场景(ORM密度/审计粒度/性能要求)做取舍

3.7 避坑指南与个人忠告

这一路走下来,我总结了几条铁律:

  1. 别想着一个拦截器搞定所有场景。我见过有人试图在MyBatis拦截器里改Hibernate的SQL,这是不可能的。各框架有自己的生命周期,混用只会让代码变得乌烟瘴气。
  2. 性能不能忽视。尤其是JDBC驱动拦截器,如果审计逻辑里做IO操作(比如写日志、访问外部API),一定要异步化。否则线上一个慢SQL就能把链路拖垮。
  3. 参数还原永远是难点。不管是MyBatis还是JDBC代理,参数解析的代码一定要写好单元测试。我曾在生产环境遇见过因为参数类型不匹配,导致审计日志里全是?=??的情况,排查起来让人崩溃。
警告:不要在拦截器里做 复杂的字符串替换 来修复SQL。你以为只是加个引号?错了,这样很容易把整条SQL搞废,甚至引发SQL注入漏洞。审计只管记录和阻断,修改SQL逻辑是别的地方该管的。