大家好,今天继续和大家聊聊 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[返回结果给业务]
简单说就是:
-
Executor 说:“我要查个数据”(开始调度)
-
StatementHandler 说:“好的,SQL 我准备好了”(准备语句)
-
ParameterHandler 说:“参数我都填进去了”(设置参数)
-
数据库执行查询
-
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 执行。该捕获的异常要捕获,别因为插件的问题导致主业务流程失败。
六、总结一下
好了,我们来总结一下今天聊的重点:
-
MyBatis 插件靠的是“四大金刚”:Executor、StatementHandler、ParameterHandler、ResultSetHandler,它们分别掌管 SQL 执行的不同阶段。
-
插件本质是“拦截”:在关键方法执行前后插入我们的逻辑,用的就是动态代理和责任链模式。
-
选对扩展点事半功倍:想做分页就别用 ParameterHandler,想加解密就别用 Executor,选对了才能用最少的代码做最多的事。
-
现成的轮子很多:大部分常见需求都有现成插件,先看看社区有没有,不用重复造轮子。
理解插件机制,不仅能让我们更好地使用第三方插件,还能在遇到特殊需求时,自己动手写出合适的插件。毕竟,知道原理,用起来才不慌嘛!
2086

被折叠的 条评论
为什么被折叠?



