一句话总结
MyBatis 插件机制基于 责任链模式 + JDK 动态代理,可拦截四大核心对象:Executor(SQL 执行)、StatementHandler(语句处理)、ParameterHandler(参数设置)、ResultSetHandler(结果处理)。每个插件实现 Interceptor 接口,通过 @Intercepts + @Signature 注解指定拦截目标,最终通过 InterceptorChain.pluginAll() 层层代理包裹目标对象。著名应用:PageHelper 分页插件拦截 Executor.query() 自动追加 LIMIT。
初级理解
四大可拦截对象
| 拦截对象 | 可拦截方法 | 作用 |
| Executor | update、query、flushStatements、commit、rollback | SQL 执行全过程 |
| StatementHandler | prepare、parameterize、batch、update、query | SQL 语句预处理 |
| ParameterHandler | getParameterObject、setParameters | 参数设置 |
| ResultSetHandler | handleResultSets、handleOutputParameters | 结果集映射 |
插件使用步骤
# 1. 实现 Interceptor 接口
@Intercepts({
@Signature(
type = Executor.class, // 拦截 Executor
method = "query", // 拦截 query 方法
args = {MappedStatement.class, Object.class,
RowBounds.class, ResultHandler.class}
)
})
public class MyPlugin implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 前置增强
System.out.println("before query");
Object result = invocation.proceed(); // 执行原方法
// 后置增强
System.out.println("after query");
return result;
}
}
# 2. 注册插件(mybatis-config.xml)
<plugins>
<plugin interceptor="com.example.plugin.MyPlugin"/>
</plugins>
# 3. Spring Boot 注册方式
@Configuration
public class MyBatisConfig {
@Bean
public MyPlugin myPlugin() {
return new MyPlugin();
}
}
中级深入
责任链模式实现
# InterceptorChain 插件链核心代码
public class InterceptorChain {
private final List<Interceptor> interceptors = new ArrayList<>();
public void addInterceptor(Interceptor interceptor) {
interceptors.add(interceptor);
}
// 对所有拦截器依次代理
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}
}
# Interceptor.plugin() 默认实现
default Object plugin(Object target) {
return Plugin.wrap(target, this);
}
# Plugin.wrap() 核心逻辑
public static Object wrap(Object target, Interceptor interceptor) {
// 1. 获取拦截器的 @Signature 注解信息
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
Class<?> type = target.getClass();
// 2. 获取 target 实现的所有接口
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
// 3. 如果有匹配的接口,创建 JDK 动态代理
if (interfaces.length > 0) {
return Proxy.newProxyInstance(
type.getClassLoader(), interfaces,
new Plugin(target, interceptor, signatureMap));
}
// 4. 没有匹配,返回原对象
return target;
}
# 多层代理效果:
# Executor → Plugin1代理 → Plugin2代理 → Plugin3代理
# 调用 executor.query() 时:
# Plugin3.invoke() → 执行拦截逻辑 → Plugin2.invoke()
# → 执行拦截逻辑 → Plugin1.invoke() → 执行拦截逻辑
# → executor.query()(真正执行)
PageHelper 分页插件原理
# PageHelper 拦截 Executor.query()
@Intercepts({
@Signature(type = Executor.class, method = "query",
args = {MappedStatement.class, Object.class,
RowBounds.class, ResultHandler.class}),
@Signature(type = Executor.class, method = "query",
args = {MappedStatement.class, Object.class,
RowBounds.class, ResultHandler.class,
CacheKey.class, BoundSql.class})
})
public class PageInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 1. 获取参数中的分页信息
Page page = PageHelper.getLocalPage();
if (page == null) return invocation.proceed();
// 2. 查询总记录数
Long count = executeCount(invocation);
page.setTotal(count);
// 3. 修改原 SQL,追加 LIMIT 子句
String pageSql = getPageSql(boundSql, page);
// 4. 执行分页查询
return executePageQuery(invocation, pageSql);
}
}
# 使用方式:
PageHelper.startPage(1, 10); // 第1页,每页10条
List<User> users = userMapper.selectAll(); // 自动分页
PageInfo<User> pageInfo = new PageInfo<>(users);
# PageHelper 核心:ThreadLocal 传递分页参数
# PageHelper.startPage() → 设置 ThreadLocal
# Executor.query() 被拦截 → 读取 ThreadLocal → 修改 SQL
# 方法结束后 → 清除 ThreadLocal(防止内存泄漏)
高级进阶
多个插件执行顺序
# 插件执行顺序取决于注册顺序
# XML 配置中先注册的先执行(最外层代理)
<plugins>
<plugin interceptor="com.example.PluginA"/> <!-- 最外层 -->
<plugin interceptor="com.example.PluginB"/> <!-- 中间层 -->
<plugin interceptor="com.example.PluginC"/> <!-- 最内层 -->
</plugins>
# 执行顺序(类似洋葱模型):
# PluginA.invoke() 前置
# → PluginB.invoke() 前置
# → PluginC.invoke() 前置
# → 真实方法执行
# → PluginC.invoke() 后置
# → PluginB.invoke() 后置
# → PluginA.invoke() 后置
# 不同拦截对象的执行顺序:
# Executor 插件(最外层)
# → StatementHandler 插件
# → ParameterHandler 插件
# → ResultSetHandler 插件
自定义插件实战:SQL 性能监控
# 自定义慢 SQL 监控插件
@Intercepts({
@Signature(type = StatementHandler.class,
method = "query",
args = {Statement.class, ResultHandler.class})
})
public class SlowSqlPlugin implements Interceptor {
// 慢 SQL 阈值(毫秒)
private long threshold = 1000;
@Override
public Object intercept(Invocation invocation) throws Throwable {
long start = System.currentTimeMillis();
try {
return invocation.proceed();
} finally {
long cost = System.currentTimeMillis() - start;
if (cost >= threshold) {
StatementHandler handler = (StatementHandler)
invocation.getTarget();
BoundSql boundSql = handler.getBoundSql();
String sql = boundSql.getSql()
.replaceAll("\\s+", " ");
log.warn("慢SQL [{}ms]: {}", cost, sql);
}
}
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
this.threshold = Long.parseLong(
properties.getProperty("threshold", "1000"));
}
}
# 注册时设置属性
<plugin interceptor="com.example.SlowSqlPlugin">
<property name="threshold" value="500"/>
</plugin>
插件 vs AOP
| 对比维度 | MyBatis 插件 | Spring AOP |
| 拦截层 | 持久层(MyBatis 内部) | Service 层/任意 Bean |
| 拦截对象 | 固定的四大对象 | 任意 Spring Bean |
| 粒度 | SQL 执行过程 | 方法调用 |
| 适用场景 | 分页、加密、脱敏、监控 | 日志、事务、权限 |
实战场景
# 场景1:数据脱敏插件
@Intercepts({
@Signature(type = ResultSetHandler.class,
method = "handleResultSets", args = {Statement.class})
})
public class DesensitizePlugin implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
List<Object> result = (List<Object>) invocation.proceed();
for (Object obj : result) {
// 反射处理敏感字段
Field[] fields = obj.getClass().getDeclaredFields();
for (Field field : fields) {
Sensitive sensitive = field.getAnnotation(Sensitive.class);
if (sensitive != null) {
field.setAccessible(true);
String value = (String) field.get(obj);
field.set(obj, desensitize(value, sensitive.type()));
}
}
}
return result;
}
}
# 场景2:数据加密插件(拦截 ParameterHandler 和 ResultSetHandler)
# 写入时加密:拦截 ParameterHandler.setParameters()
# 读取时解密:拦截 ResultSetHandler.handleResultSets()
# 场景3:多数据源自动切换插件
# 拦截 Executor.query()/update()
# 根据 ThreadLocal 中的数据源标识
# 动态切换数据源
面试模拟
面试官:MyBatis 插件原理是什么?PageHelper 是怎么实现分页的?
你:MyBatis 插件基于责任链模式 + JDK 动态代理。实现 Interceptor 接口,通过 @Intercepts + @Signature 指定拦截的方法,Plugin.wrap() 创建代理对象。PageHelper 拦截 Executor.query(),通过 ThreadLocal 获取分页参数,先执行 COUNT 查询总记录数,再修改原 SQL 追加 LIMIT 子句实现分页。
面试官:多个插件时执行顺序是怎样的?
你:类似洋葱模型,按注册顺序层层包裹。先注册的是最外层代理,前置先执行、后置后执行。不同类型的拦截对象也有顺序:Executor 最先被拦截,然后是 StatementHandler、ParameterHandler,最后是 ResultSetHandler。