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,可以根据数据库方言追加 limit、offset 或包一层分页 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 面试题。