MyBatis插件的四大金刚

大家好,今天继续和大家聊聊 MyBatis 的插件机制。很多朋友可能用过一些 MyBatis 插件,比如分页、加解密之类的,但不太清楚它们到底是怎么“插”进去工作的。其实理解这个的关键,就在于弄明白 MyBatis 给咱们留下的四个“扩展点”。

一、先来张图,一目了然

在深入之前,咱们先看张图,对这四个扩展点有个整体印象:

接口

典型可拦截方法

场景举例

Executor

update, query, commit, rollback, close

慢 SQL 监控、读写分离、多租户数据隔离

StatementHandler

prepare, parameterize, batch, update, query

分页改写 SQL、SQL 语句添加 Hint

ParameterHandler

setParameters

统一数据加密、敏感字段脱敏

ResultSetHandler

handleResultSets, handleOutputParameters

数据解密、字典翻译、驼峰下划线转换

简单来说,这四个接口就像是 SQL 执行流水线上的四个关键工位,各自负责一段工作,也正好给我们留下了“插手”的机会。


二、四大扩展点

1. Executor:执行总指挥

是什么?

你可以把 Executor 想象成项目里的项目经理,它不干具体编码的活,但所有 SQL 执行的调度、事务的管理、缓存的协调,都归它管。它是整个执行链条的起点。

有啥用?重点能“卡”住哪儿?

  • query():查询的时候

  • update():增删改的时候

  • commit()/ rollback():事务提交或回滚时

怎么用?

因为它“位高权重”,所以特别适合做一些全局性、管控类的事情。比如:

  • 监控慢 SQL:在 query方法前后计时,时间太长了就记下来。

  • 实现读写分离:根据 SQL 是读还是写,决定用主库还是从库。

  • 多租户数据隔离:自动在所有查询里加上租户 ID 条件。

// 举个例子:慢SQL监控
@Intercepts({
    @Signature(type = Executor.class, method = "query", 
               args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})
public class SlowSqlPlugin implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        long start = System.currentTimeMillis();
        Object result = invocation.proceed(); // 放行,执行真正的SQL
        long end = System.currentTimeMillis();
        
        if (end - start > 1000) { // 超过1秒就算慢SQL
            log.warn("发现慢SQL,执行耗时:{}ms", end - start);
        }
        return result;
    }
}

2. StatementHandler:SQL 语句操盘手

是什么?

这位是具体干活的开发同学,负责和 JDBC 的 Statement直接打交道,比如准备 SQL 语句、设置参数、执行查询,都是它的活儿。

有啥用?重点能“卡”住哪儿?

  • prepare():SQL 准备阶段

  • parameterize():参数设置前

  • query()/ update():执行 SQL 前

怎么用?

因为它直接操作 SQL 语句本身,所以非常适合做 SQL 改写:

  • 分页插件:把 SELECT * FROM user改写成 SELECT * FROM user LIMIT 0, 10

  • 动态表名:根据业务规则替换 SQL 中的表名

  • SQL 优化:自动给查询加上数据库特定的 Hint

// 比如分页插件核心思路
@Intercepts({
    @Signature(type = StatementHandler.class, method = "prepare", 
               args = {Connection.class, Integer.class})
})
public class PagePlugin implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        StatementHandler handler = (StatementHandler) invocation.getTarget();
        String originalSql = handler.getBoundSql().getSql();
        
        // 看看有没有分页参数
        if (有分页参数) {
            // 把 SELECT * FROM table 改成 SELECT * FROM table LIMIT ...
            String newSql = addLimit(originalSql, 分页参数);
            反射设置回去(handler.getBoundSql(), "sql", newSql);
        }
        
        return invocation.proceed();
    }
}

3. ParameterHandler:参数搬运工

是什么?

这位专门负责给 SQL 语句里的问号 ?填值。你的 Java 对象里的属性,就是通过它变成 PreparedStatement 里的参数。

有啥用?重点能“卡”住哪儿?

就一个关键点:setParameters(),在给 SQL 设置参数的时候。

怎么用?

既然所有参数都经过它手,那做统一处理就太方便了:

  • 数据加密:在入库前,把手机号、身份证号等敏感信息加密

  • 数据脱敏:在日志场景下,把敏感信息部分替换成 *

  • 格式统一:比如把所有日期都转成统一格式

// 数据加密插件示例
@Intercepts({
    @Signature(type = ParameterHandler.class, method = "setParameters", 
               args = {PreparedStatement.class})
})
public class EncryptPlugin implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        ParameterHandler handler = (ParameterHandler) invocation.getTarget();
        Object paramObj = handler.getParameterObject();
        
        if (paramObj != null) {
            // 遍历对象字段,找到需要加密的进行加密
            encryptFields(paramObj);
        }
        
        return invocation.proceed();
    }
}

4. ResultSetHandler:结果翻译官

是什么?

SQL 执行完,数据库返回的是一行行的数据(ResultSet),这位翻译官的工作就是把这些“数据库语言”转换成咱们的 Java 对象。

有啥用?重点能“卡”住哪儿?

  • handleResultSets():处理普通查询结果

  • handleCursorResultSets():处理游标结果

  • handleOutputParameters():处理存储过程的输出参数

怎么用?

结果出来之后,返回给业务代码之前,可以做很多“美化”工作:

  • 数据解密:和上面的加密插件配合,读取时自动解密

  • 字典翻译:把数据库存的代码(如 1、2、3)转成中文含义

  • 格式转换:下划线字段名转驼峰,日期格式转换等

// 字典翻译插件示例
@Intercepts({
    @Signature(type = ResultSetHandler.class, method = "handleResultSets", 
               args = {Statement.class})
})
public class DictTranslatePlugin implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 1. 先让MyBatis完成默认的结果集映射
        List<Object> resultList = (List<Object>) invocation.proceed();
        
        // 2. 对结果进行后处理
        for (Object record : resultList) {
            // 比如把 status=1 转成 "启用",status=0 转成 "停用"
            translateStatus(record);
        }
        
        return resultList;
    }
}

三、它们是怎么配合工作的?

用一个简单的查询流程,你就能明白它们是怎么接力工作的:

graph LR
    A[业务调用查询] --> B[Executor.query]
    B --> C[StatementHandler.prepare<br/>准备SQL]
    C --> D[ParameterHandler.setParameters<br/>设置参数]
    D --> E[执行JDBC查询]
    E --> F[ResultSetHandler.handleResultSets<br/>处理结果]
    F --> G[返回结果给业务]

简单说就是:

  1. Executor​ 说:“我要查个数据”(开始调度)

  2. StatementHandler​ 说:“好的,SQL 我准备好了”(准备语句)

  3. ParameterHandler​ 说:“参数我都填进去了”(设置参数)

  4. 数据库执行查询

  5. ResultSetHandler​ 说:“结果我转成 Java 对象了”(映射结果)

这个流程里的每个环节,咱们都能用插件“插一脚”进去做点事情。


四、常见插件

理解了原理,我们再看看一些流行的 MyBatis 插件是怎么利用这些扩展点的:

名称

作用

备注

MyBatis-Plus 分页插件

物理分页

基于 Executor.query

PageHelper

物理分页

基于 Executor.query

MyBatis-Flex 审计插件

自动填充时间、操作人

基于 Executor.update

shardingsphere-jdbc

分库分表

包装 Executor

mybatis-encrypt

字段加解密

基于 ParameterHandler + ResultSetHandler

这里有个有趣的现象大家发现没?分页插件大多基于 Executor,为什么呢?因为分页不是简单地加个 LIMIT,而是要两步走:先查总数,再查数据。如果等到 StatementHandler.prepare 阶段才介入,SQL 都快发出去了,这时候再去查总数?太晚了!而且容易造成事务混乱、连接占用等问题。StatementHandler适合做 SQL 微调(比如参数替换),不适合做流程控制。

所以必须在查询入口处拦截——也就是 Executor.query。这里是整个流程的起点,拥有完整的上下文信息,最适合统筹安排“先 count + 再 limit”的完整策略。

就好比,你要组织一场演出,是在彩排现场调整节目单,还是在策划阶段就定好流程?显然是后者!

加解密插件用 ParameterHandler + ResultSetHandler 组合,一个管写入加密,一个管读取解密,分工明确。


五、咱们自己写插件要注意什么?

1. 选对扩展点

  • 监控或控制整个 SQL 执行​ → 用 Executor

  • 改写 SQL 语句本身​ → 用 StatementHandler

  • 统一处理参数​ → 用 ParameterHandler

  • 统一处理查询结果​ → 用 ResultSetHandler

2. 性能要小心

插件虽好,但不能滥用。每加一个插件,就多一层代理调用,会有性能开销。特别是那些频繁执行的简单查询,插件里的逻辑要尽量轻量。

3. 顺序很重要

如果你的系统里配了多个插件,它们的执行顺序就是配置文件的书写顺序。前面的插件先执行,后面的后执行,有些像过滤器链。

4. 异常处理好

插件里的代码如果抛异常,会影响整个 SQL 执行。该捕获的异常要捕获,别因为插件的问题导致主业务流程失败。


六、总结一下

好了,我们来总结一下今天聊的重点:

  1. MyBatis 插件靠的是“四大金刚”:Executor、StatementHandler、ParameterHandler、ResultSetHandler,它们分别掌管 SQL 执行的不同阶段。

  2. 插件本质是“拦截”:在关键方法执行前后插入我们的逻辑,用的就是动态代理和责任链模式。

  3. 选对扩展点事半功倍:想做分页就别用 ParameterHandler,想加解密就别用 Executor,选对了才能用最少的代码做最多的事。

  4. 现成的轮子很多:大部分常见需求都有现成插件,先看看社区有没有,不用重复造轮子。

理解插件机制,不仅能让我们更好地使用第三方插件,还能在遇到特殊需求时,自己动手写出合适的插件。毕竟,知道原理,用起来才不慌嘛!

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值