Skip to content

MyBatis 拦截器

MyBatis 拦截器用于在 SQL 执行流程中插入自定义逻辑,例如分页、SQL 审计、数据权限、慢 SQL 记录、字段加密等。

为什么需要拦截器

有些逻辑不是某一个 Mapper 独有,而是所有 SQL 都可能需要。例如慢 SQL 日志、租户过滤、分页改写、数据权限。如果每个 Mapper 方法都手写这些逻辑,会大量重复,而且容易漏。

拦截器的价值是把横切逻辑放到 MyBatis 执行链路中统一处理。

如果滥用拦截器,会出现两个后果:

  1. SQL 被隐式改写,业务排查时看到的 Mapper SQL 和最终执行 SQL 不一致。
  2. 拦截器影响范围过大,一个 bug 可能影响所有查询。

可拦截对象

MyBatis 插件可以拦截四类核心对象:

对象作用
Executor执行 SQL,处理查询和更新
StatementHandler创建和处理 Statement
ParameterHandler设置 SQL 参数
ResultSetHandler处理结果集映射

执行流程

mermaid
flowchart TD
    A[Mapper 方法调用] --> B[Executor]
    B --> C[StatementHandler]
    C --> D[ParameterHandler 设置参数]
    D --> E[JDBC 执行 SQL]
    E --> F[ResultSetHandler 映射结果]
    F --> G[返回 Java 对象]

拦截器本质上是代理增强。它会包装目标对象,在目标方法执行前后加入自定义逻辑。

插件原理

MyBatis 插件通过动态代理包装核心对象。当调用被拦截的方法时,会先进入插件的 intercept 方法,插件可以在 invocation.proceed() 前后增加逻辑。

mermaid
flowchart TD
    A["原始 StatementHandler"] --> B["Plugin.wrap 生成代理"]
    B --> C["调用 prepare"]
    C --> D["进入 Interceptor.intercept"]
    D --> E["执行自定义逻辑"]
    E --> F["invocation.proceed"]

示例结构

java
@Intercepts({
    @Signature(
        type = StatementHandler.class,
        method = "prepare",
        args = {Connection.class, Integer.class}
    )
})
public class SqlLogInterceptor implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 执行前逻辑
        Object result = invocation.proceed();
        // 执行后逻辑
        return result;
    }
}

常见用途

分页

在 SQL 执行前改写 SQL,自动追加 limit。

数据权限

根据当前用户的数据范围,给 SQL 自动追加部门、租户等过滤条件。

SQL 日志

记录 SQL、参数、耗时,帮助排查慢查询。

注意事项

  1. 拦截器会影响所有匹配 SQL,逻辑必须足够谨慎。
  2. 改写 SQL 时要考虑不同数据库方言。
  3. 不要在拦截器中执行复杂远程调用。
  4. 多个拦截器有执行顺序,顺序不同可能结果不同。

拦截器到底拦在哪里

MyBatis 执行一条 SQL 时,大致经过:

mermaid
flowchart TD
    A["Executor.query/update"] --> B["StatementHandler.prepare"]
    B --> C["ParameterHandler.setParameters"]
    C --> D["JDBC 执行"]
    D --> E["ResultSetHandler.handleResultSets"]

四个可拦截点分别适合不同事情:

拦截点适合做什么不适合做什么
Executor统计 SQL、审计入口、统一拦截查询更新直接拼复杂 SQL
StatementHandler分页改写、SQL 改写、拿 BoundSql读取业务结果
ParameterHandler参数加密、脱敏、参数审计做远程鉴权
ResultSetHandler返回结果脱敏、字段处理大量复杂业务组装

为什么分页插件常拦 StatementHandler?因为这时已经有最终 SQL,可以根据数据库方言追加 limitoffset 或包一层分页 SQL。

插件链执行顺序

多个插件会形成代理链。顺序不同,结果可能不同。

mermaid
flowchart TD
    A["原始 StatementHandler"] --> B["插件1包装"]
    B --> C["插件2包装"]
    C --> D["插件3包装"]
    D --> E["调用 prepare"]
    E --> F["插件3 intercept"]
    F --> G["插件2 intercept"]
    G --> H["插件1 intercept"]
    H --> I["原始方法"]

如果一个插件先做分页,另一个插件再做数据权限改写,和先数据权限再分页,最终 SQL 可能不同。生产中要明确插件顺序,并写集成测试覆盖关键 SQL。

BoundSql 和最终 SQL

插件经常需要拿到 BoundSql

java
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
BoundSql boundSql = statementHandler.getBoundSql();
String sql = boundSql.getSql();
Object parameterObject = boundSql.getParameterObject();

BoundSql 里包含:

  1. 最终 SQL 文本。
  2. 参数映射列表。
  3. 参数对象。
  4. 动态 SQL 生成的附加参数。

注意:日志里打印 SQL 时,不能只打印 SQL 文本,也要打印参数,否则 ? 很难排查。也不要把身份证、手机号、token、密码明文打到日志。

分页插件原理

分页插件通常做两件事:

  1. 生成 count SQL,查询总数。
  2. 改写原 SQL,追加分页语法。
mermaid
flowchart TD
    A["原始查询 SQL"] --> B["分页插件拦截 StatementHandler"]
    B --> C["生成 count SQL"]
    C --> D["查询总数"]
    B --> E["按数据库方言改写分页 SQL"]
    E --> F["执行分页查询"]
    D --> G["组装 Page 对象"]
    F --> G

MySQL 示例:

sql
select id, asset_code
from asset
where dept_id = ?
order by create_time desc
limit ?, ?

风险:

  1. count SQL 对复杂 join、group by、distinct 可能很慢。
  2. 深分页仍然慢,插件不能改变数据库跳过大量行的事实。
  3. 不同数据库分页方言不同。
  4. SQL 改写错误会影响所有分页查询。

数据权限插件原理

数据权限插件可能给 SQL 自动追加部门或租户条件:

sql
select id, asset_code
from asset
where status = ?

改写为:

sql
select id, asset_code
from asset
where status = ?
  and dept_id in (?, ?, ?)

这种能力很强,也很危险:

  1. SQL 解析不能靠简单字符串拼接,复杂 SQL 容易改错。
  2. 管理员、系统任务、导出任务可能有不同权限规则。
  3. count SQL、分页 SQL、子查询都要考虑。
  4. 权限条件要能被索引支持,否则所有查询变慢。

商业建议:

  1. 优先在业务查询条件中显式表达权限。
  2. 插件适合做兜底或统一补充,不适合承载全部复杂权限模型。
  3. 对被改写 SQL 做日志和测试。

慢 SQL 插件 Demo

java
@Intercepts({
    @Signature(type = StatementHandler.class,
            method = "query",
            args = {Statement.class, ResultHandler.class}),
    @Signature(type = StatementHandler.class,
            method = "update",
            args = {Statement.class})
})
public class SlowSqlInterceptor implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        long start = System.currentTimeMillis();
        try {
            return invocation.proceed();
        } finally {
            long cost = System.currentTimeMillis() - start;
            if (cost > 1000) {
                Object target = invocation.getTarget();
                if (target instanceof StatementHandler) {
                    StatementHandler handler = (StatementHandler) target;
                    BoundSql boundSql = handler.getBoundSql();
                    System.out.println("slow sql cost=" + cost + "ms, sql=" + boundSql.getSql());
                }
            }
        }
    }
}

真实项目中不要只 System.out.println,应该接入日志框架、traceId、接口名、Mapper 方法名、脱敏后的参数和耗时。

参数加密和脱敏

ParameterHandler 可以在参数设置前处理字段,例如手机号加密。

风险在于:

  1. 加密后查询方式会变化,模糊查询可能不可用。
  2. 加密字段需要统一算法和密钥管理。
  3. 日志打印不能泄露明文。
  4. 插件处理范围要可控,不能误处理所有字符串。

更常见做法是:核心加密逻辑放在明确的 TypeHandler、领域服务或数据库加密方案中,拦截器只做统一审计或兜底。

商业场景:医疗资产平台数据权限

需求:普通用户只能看自己科室资产,院级管理员可以看全院资产。

方案一:业务显式传条件。

xml
where hospital_id = #{hospitalId}
<if test="deptIds != null and deptIds.size() > 0">
    and dept_id in
    <foreach collection="deptIds" item="deptId" open="(" separator="," close=")">
        #{deptId}
    </foreach>
</if>

方案二:插件统一追加。

适合多系统统一兜底,但必须解决:

  1. 哪些 Mapper 需要数据权限。
  2. 哪些 Mapper 排除,例如字典表。
  3. 管理员如何放行。
  4. 子查询和别名如何识别。
  5. 如何保证索引支持权限字段。

在真实商业系统里,数据权限插件很少是“简单追加字符串”能搞定的,通常需要 SQL 解析器、注解标记、白名单、测试用例和审计日志。

常见坑

问题后果建议
拦截器里远程调用SQL 执行链路变慢甚至阻塞不做远程调用
简单字符串改 SQL复杂 SQL 被改坏使用解析器或显式查询条件
插件顺序不清楚分页、权限、租户结果异常固定顺序并测试
打印敏感参数泄露手机号、身份证、token日志脱敏
count SQL 自动生成不准分页总数慢或错误复杂查询手写 count
插件写业务逻辑隐式行为难排查业务逻辑放 Service

线上排查流程

mermaid
flowchart TD
    A["SQL 行为异常"] --> B["确认是否启用插件"]
    B --> C["打印原始 SQL 和改写后 SQL"]
    C --> D{"是分页问题吗"}
    D -- "是" --> E["检查 count SQL、limit、排序索引"]
    D -- "否" --> F{"是权限/租户问题吗"}
    F -- "是" --> G["检查追加条件、别名、白名单"]
    F -- "否" --> H["检查插件顺序和参数处理"]
    H --> I["拿最终 SQL 去数据库 EXPLAIN"]

面试标准回答

text
MyBatis 拦截器是通过插件机制对 Executor、StatementHandler、ParameterHandler、ResultSetHandler 四类核心对象做动态代理,在目标方法执行前后插入逻辑。分页插件通常拦截 StatementHandler,拿到 BoundSql 后生成 count SQL 并按数据库方言改写分页 SQL;慢 SQL、审计、数据权限、租户隔离也可以通过插件实现。拦截器属于横切增强,不能滥用,尤其不能在里面写复杂业务逻辑或远程调用。改写 SQL 要考虑方言、插件顺序、参数脱敏、复杂 SQL 和执行计划。

更多 ORM 内容可以回到 ORM 面试题