Skip to content

MyBatis 插件机制深度解析

面试官:你知道 MyBatis 的插件机制吗?它是怎么实现的?

:知道。MyBatis 插件基于 JDK 动态代理和责任链模式实现,可以拦截 Executor、StatementHandler、ParameterHandler、ResultSetHandler 四大核心对象的方法,实现分页、SQL 日志、数据权限等功能。

面试官:能详细说说 PageHelper 的分页原理吗?为什么调用 PageHelper.startPage() 就能分页?

这个追问直击核心——能说出 ThreadLocal、拦截 Executor、SQL 改写这些关键步骤的候选人,才算真正理解插件机制的实战应用。


Q1:MyBatis 插件能拦截哪些对象?每个对象能拦截什么方法?必考

Section titled “Q1:MyBatis 插件能拦截哪些对象?每个对象能拦截什么方法?”

MyBatis 插件只能拦截以下四大核心对象的方法:

四大对象拦截点对比表

对象作用可拦截方法典型应用场景
ExecutorSQL 执行调度器update, query, flushStatements, commit, rollback, getTransaction, close, isClosed读写分离、分库分表、SQL 日志
StatementHandlerStatement 管理器prepare, parameterize, batch, update, query分页(修改 SQL)、SQL 改写
ParameterHandler参数处理器getParameterObject, setParameters参数加解密、参数脱敏
ResultSetHandler结果集处理器handleResultSets, handleOutputParameters结果加解密、结果脱敏

四大对象协作关系

Executor(调度层)
└── update() / query()
└── StatementHandler(执行层)
├── prepare() ← 创建 Statement
├── parameterize() ← 设置参数
│ └── ParameterHandler.setParameters()
└── query() / update() ← 执行 SQL
└── ResultSetHandler.handleResultSets() ← 处理结果

拦截示例

// 拦截 Executor.query(用于读写分离、SQL 日志)
@Intercepts({
@Signature(
type = Executor.class,
method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
)
})
public class SqlLogPlugin implements Interceptor { }
// 拦截 StatementHandler.prepare(用于分页、SQL 改写)
@Intercepts({
@Signature(
type = StatementHandler.class,
method = "prepare",
args = {Connection.class, Integer.class}
)
})
public class PagePlugin implements Interceptor { }
// 拦截 ParameterHandler.setParameters(用于参数加密)
@Intercepts({
@Signature(
type = ParameterHandler.class,
method = "setParameters",
args = {PreparedStatement.class}
)
})
public class ParamEncryptPlugin implements Interceptor { }
// 拦截 ResultSetHandler.handleResultSets(用于结果解密)
@Intercepts({
@Signature(
type = ResultSetHandler.class,
method = "handleResultSets",
args = {Statement.class}
)
})
public class ResultDecryptPlugin implements Interceptor { }

Q2:如何编写一个 MyBatis 插件?完整步骤是什么?必考

Section titled “Q2:如何编写一个 MyBatis 插件?完整步骤是什么?”

完整实现步骤

/**
* 示例:SQL 执行耗时统计插件
*/
@Intercepts({
@Signature(
type = Executor.class, // 拦截哪个对象
method = "query", // 拦截哪个方法
args = {MappedStatement.class, Object.class, // 方法参数类型(必须精确匹配)
RowBounds.class, ResultHandler.class}
),
@Signature(
type = Executor.class,
method = "update",
args = {MappedStatement.class, Object.class}
)
})
public class SqlCostMonitorPlugin implements Interceptor {
private Properties properties; // 插件配置属性
/**
* 核心拦截方法
*/
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 1. 获取拦截对象和方法参数
MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
Object parameter = invocation.getArgs()[1];
// 2. 前置逻辑:记录开始时间
long start = System.currentTimeMillis();
try {
// 3. 执行原方法(继续责任链)
Object result = invocation.proceed();
// 4. 后置逻辑:计算耗时
long cost = System.currentTimeMillis() - start;
// 5. 记录慢 SQL(超过阈值)
if (cost > getSlowSqlThreshold()) {
String sql = ms.getBoundSql(parameter).getSql();
System.err.println("慢 SQL: " + sql + " | 耗时: " + cost + "ms");
}
return result;
} catch (Exception e) {
// 6. 异常处理
System.err.println("SQL 执行异常: " + e.getMessage());
throw e;
}
}
/**
* 生成代理对象
*/
@Override
public Object plugin(Object target) {
// 只对目标类型生成代理,提升性能
if (target instanceof Executor) {
return Plugin.wrap(target, this); // 使用 MyBatis 提供的 Plugin 工具类
}
return target;
}
/**
* 设置插件配置属性(从 mybatis-config.xml 读取)
*/
@Override
public void setProperties(Properties properties) {
this.properties = properties;
}
private long getSlowSqlThreshold() {
// 从配置中获取慢 SQL 阈值,默认 1000ms
return Long.parseLong(properties.getProperty("slowSqlThreshold", "1000"));
}
}

注册插件

mybatis-config.xml
<plugins>
<plugin interceptor="com.example.plugin.SqlCostMonitorPlugin">
<!-- 传递配置参数 -->
<property name="slowSqlThreshold" value="500"/>
</plugin>
</plugins>

Spring Boot 集成

@Configuration
public class MyBatisConfig {
@Bean
public SqlCostMonitorPlugin sqlCostMonitorPlugin() {
SqlCostMonitorPlugin plugin = new SqlCostMonitorPlugin();
Properties properties = new Properties();
properties.setProperty("slowSqlThreshold", "500");
plugin.setProperties(properties);
return plugin;
}
@Bean
public SqlSessionFactoryBean sqlSessionFactoryBean(DataSource dataSource,
SqlCostMonitorPlugin sqlCostMonitorPlugin) {
SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
factory.setDataSource(dataSource);
// 注册插件
factory.setPlugins(sqlCostMonitorPlugin);
return factory;
}
}

关键细节

  1. @Signature 必须精确匹配方法签名args 参数类型必须与方法声明完全一致,否则无法拦截
  2. invocation.proceed() 必须调用:否则后续插件和原方法都不会执行
  3. plugin() 方法优化:只对目标类型生成代理,避免不必要的代理对象
  4. 异常处理:拦截方法抛出异常会影响整个 SQL 执行链,需谨慎处理

Q3:MyBatis 插件的底层原理是什么?高频

Section titled “Q3:MyBatis 插件的底层原理是什么?”

核心原理:JDK 动态代理 + 责任链模式

初始化阶段(MyBatis 启动时):

┌──────────────────────────────────────────────────────────────┐
│ Configuration 初始化 │
│ - 解析 mybatis-config.xml │
│ - 加载所有插件(InterceptorChain) │
└───────────────────────┬──────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────┐
│ 创建 Executor │
│ executor = new SimpleExecutor(configuration, transaction) │
└───────────────────────┬──────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────┐
│ 遍历所有插件 │
│ for (Interceptor plugin : interceptorChain.getInterceptors()) │
└───────────────────────┬──────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────┐
│ 每个插件调用 plugin(executor) │
│ executor = plugin.plugin(executor) │
│ └── Plugin.wrap(target, interceptor) │
│ └── 检查插件是否关注此对象类型 │
│ ├── 是 → 生成 JDK 动态代理对象 │
│ │ return Proxy.newProxyInstance( │
│ │ classLoader, │
│ │ new Class[]{Executor.class}, │
│ │ new Plugin(target, interceptor)│
│ │ ); │
│ └── 否 → 返回原对象 │
└───────────────────────┬──────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────┐
│ 代理对象替换原对象 │
│ 最终 executor 是被代理后的对象(层层包装) │
└──────────────────────────────────────────────────────────────┘

执行阶段(调用方法时):

┌──────────────────────────────────────────────────────────────┐
│ 调用代理对象的方法 │
│ executor.query(ms, parameter, rowBounds, resultHandler) │
└───────────────────────┬──────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────┐
│ Plugin.invoke() 被触发(JDK 动态代理) │
│ public Object invoke(Object proxy, Method method, Object[] args) │
└───────────────────────┬──────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────┐
│ 检查当前方法是否在 @Signature 中声明 │
│ Set<Method> methods = signatureMap.get(target.getClass()); │
│ if (methods != null && methods.contains(method)) │
└───────────────────────┬──────────────────────────────────────┘
┌───────────┴──────────┐
│ │
▼ ▼
┌──────────────┐ ┌──────────────┐
│ 是(拦截) │ │ 否(不拦截)│
└──────┬───────┘ └──────┬───────┘
│ │
▼ ▼
┌─────────────────────┐ ┌─────────────────────┐
│ interceptor. │ │ method.invoke( │
│ intercept( │ │ target, args) │
│ invocation) │ │ 直接调用原方法 │
│ 执行插件逻辑 │ └─────────────────────┘
└──────┬──────────────┘
┌──────────────────────────────────────────────────────────────┐
│ 插件逻辑执行 │
│ - 前置处理(如:记录开始时间) │
│ - invocation.proceed() → 继续责任链(调用下一个插件或原方法)│
│ - 后置处理(如:计算耗时、记录日志) │
└──────────────────────────────────────────────────────────────┘

源码分析

// Plugin 类(MyBatis 提供的代理工具类)
public class Plugin implements InvocationHandler {
private final Object target; // 被代理对象
private final Interceptor interceptor; // 插件实现
private final Map<Class<?>, Set<Method>> signatureMap; // 拦截方法映射
// 生成代理对象
public static Object wrap(Object target, Interceptor interceptor) {
// 1. 解析 @Signature,生成签名映射表
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
// 2. 检查是否需要代理
Class<?> type = target.getClass();
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
if (interfaces.length > 0) {
// 3. 生成 JDK 动态代理
return Proxy.newProxyInstance(
type.getClassLoader(),
interfaces,
new Plugin(target, interceptor, signatureMap)
);
}
return target; // 不需要代理,返回原对象
}
// 代理方法调用
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
// 1. 检查当前方法是否在拦截列表中
Set<Method> methods = signatureMap.get(method.getDeclaringClass());
if (methods != null && methods.contains(method)) {
// 2. 在拦截列表中,调用插件拦截方法
return interceptor.intercept(new Invocation(target, method, args));
}
// 3. 不在拦截列表中,直接调用原方法
return method.invoke(target, args);
} catch (Exception e) {
throw ExceptionUtil.unwrapThrowable(e);
}
}
}

本质一句话:MyBatis 插件通过 JDK 动态代理为四大对象生成代理,拦截指定方法,多个插件形成责任链,按配置顺序层层包装。


Q4:多个插件的执行顺序是什么?如何理解”洋葱模型”?高频

Section titled “Q4:多个插件的执行顺序是什么?如何理解”洋葱模型”?”

插件配置顺序 vs 执行顺序

<plugins>
<plugin interceptor="PluginA"/> <!-- 第 1 个配置,最外层 -->
<plugin interceptor="PluginB"/> <!-- 第 2 个配置,中间层 -->
<plugin interceptor="PluginC"/> <!-- 第 3 个配置,最内层 -->
</plugins>

代理对象结构(类似装饰器模式):

原始 Executor
↓ 包装
PluginA 代理(外层)
↓ 包装
PluginB 代理(中层)
↓ 包装
PluginC 代理(内层)
↓ 最终对象
executor = PluginC 代理对象

执行流程(洋葱模型)

调用 executor.query()
┌─────────────────────────────────────────┐
│ PluginA.invoke() │
│ ┌───────────────────────────────────┐ │
│ │ 前置逻辑 A(开始) │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌─────────────────────────────────┐ │
│ │ │ PluginB.invoke() │ │
│ │ │ ┌───────────────────────────┐ │ │
│ │ │ │ 前置逻辑 B(开始) │ │ │
│ │ │ │ │ │ │ │
│ │ │ │ ▼ │ │ │
│ │ │ │ ┌─────────────────────────┐ │ │
│ │ │ │ │ PluginC.invoke() │ │ │
│ │ │ │ │ ┌───────────────────┐ │ │ │
│ │ │ │ │ │ 前置逻辑 C(开始)│ │ │ │
│ │ │ │ │ │ │ │ │ │ │
│ │ │ │ │ │ ▼ │ │ │ │
│ │ │ │ │ │ 原始 query() 方法 │ │ │ │
│ │ │ │ │ │ │ │ │ │ │
│ │ │ │ │ │ ▼ │ │ │ │
│ │ │ │ │ │ 后置逻辑 C(结束)│ │ │ │
│ │ │ │ │ └───────────────────┘ │ │ │
│ │ │ │ └─────────────────────────┘ │ │
│ │ │ │ │ │ │ │
│ │ │ │ ▼ │ │ │
│ │ │ │ 后置逻辑 B(结束) │ │ │
│ │ │ └───────────────────────────┘ │ │
│ │ │ └─────────────────────────────┘ │
│ │ │ │ │
│ │ │ ▼ │
│ │ └───────────────────────────────────┘ │
│ └─────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 后置逻辑 A(结束) │
└─────────────────────────────────────────┘

关键规律

阶段执行顺序说明
前置逻辑PluginA → PluginB → PluginC按配置顺序执行
原始方法只执行一次invocation.proceed()
后置逻辑PluginC → PluginB → PluginA按配置逆序执行

代码示例

// PluginA
@Override
public Object intercept(Invocation invocation) throws Throwable {
System.out.println("A 前置");
Object result = invocation.proceed(); // 调用下一个插件
System.out.println("A 后置");
return result;
}
// PluginB
@Override
public Object intercept(Invocation invocation) throws Throwable {
System.out.println("B 前置");
Object result = invocation.proceed();
System.out.println("B 后置");
return result;
}
// PluginC
@Override
public Object intercept(Invocation invocation) throws Throwable {
System.out.println("C 前置");
Object result = invocation.proceed();
System.out.println("C 后置");
return result;
}
// 输出:
// A 前置
// B 前置
// C 前置
// [原始 query() 执行]
// C 后置
// B 后置
// A 后置

Q5:PageHelper 是怎么实现分页的?为什么调用 PageHelper.startPage() 就能分页?必考

Section titled “Q5:PageHelper 是怎么实现分页的?为什么调用 PageHelper.startPage() 就能分页?”

这是 MyBatis 插件最典型的应用案例,面试官很喜欢考。

PageHelper 分页实现原理(三步走)

第一步:PageHelper.startPage() 将分页参数存入 ThreadLocal

PageHelper.java
public class PageHelper {
private static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal<>();
public static <E> Page<E> startPage(int pageNum, int pageSize) {
Page<E> page = new Page<>(pageNum, pageSize);
// 关键:存入 ThreadLocal,与当前线程绑定
LOCAL_PAGE.set(page);
return page;
}
public static Page getLocalPage() {
return LOCAL_PAGE.get();
}
public static void clearPage() {
LOCAL_PAGE.remove(); // 清除,防止内存泄漏
}
}

第二步:PageInterceptor 拦截 Executor.query() 方法,改写 SQL

@Intercepts({
@Signature(
type = Executor.class,
method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
)
})
public class PageInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 1. 从 ThreadLocal 取出分页参数
Page page = PageHelper.getLocalPage();
if (page == null) {
// 无分页参数,正常执行
return invocation.proceed();
}
// 2. 获取原始 SQL
Object[] args = invocation.getArgs();
MappedStatement ms = (MappedStatement) args[0];
Object parameter = args[1];
BoundSql boundSql = ms.getBoundSql(parameter);
String originalSql = boundSql.getSql();
// 3. 改写 SQL,加上 LIMIT 语句(MySQL)
String pageSql = originalSql + " LIMIT " + page.getStartRow() + "," + page.getPageSize();
// 4. 使用反射修改 BoundSql 中的 SQL
// (BoundSql.sql 是 private final,需要反射修改)
Field field = boundSql.getClass().getDeclaredField("sql");
field.setAccessible(true);
field.set(boundSql, pageSql);
// 5. 执行 COUNT 查询(如果需要总数)
Long total = null;
if (page.isCount()) {
String countSql = "SELECT COUNT(0) FROM (" + originalSql + ") tmp";
// 执行 COUNT 查询
total = executeCount(ms, parameter, boundSql, countSql);
page.setTotal(total);
}
// 6. 执行改写后的分页查询
Object result = invocation.proceed();
// 7. 封装分页结果
if (result instanceof List) {
page.addAll((List) result);
return page; // 返回 Page 对象(包含分页信息)
}
return result;
}
}

第三步:查询完成后,从 ThreadLocal 清除分页参数(防止内存泄漏)

PageInterceptor.java
@Override
public Object intercept(Invocation invocation) throws Throwable {
try {
// ... 分页逻辑
return result;
} finally {
// 关键:清除 ThreadLocal,防止内存泄漏
PageHelper.clearPage();
}
}

完整执行流程图

┌──────────────────────────────────────────────────────────────┐
│ 1. 调用 PageHelper.startPage(1, 10) │
│ - 创建 Page 对象(pageNum=1, pageSize=10) │
│ - 存入 ThreadLocal │
└───────────────────────┬──────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────┐
│ 2. 调用 userMapper.selectAll() │
│ - JDK 动态代理拦截 │
│ - 进入 Executor.query() │
└───────────────────────┬──────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────┐
│ 3. PageInterceptor.intercept() 被触发 │
│ - 从 ThreadLocal 取出 Page 对象 │
│ - 获取原始 SQL:SELECT * FROM user │
│ - 改写 SQL:SELECT * FROM user LIMIT 0,10 │
│ - 执行 COUNT 查询(可选):SELECT COUNT(0) FROM user │
└───────────────────────┬──────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────┐
│ 4. 执行改写后的 SQL │
│ - PreparedStatement.executeQuery() │
│ - 返回结果集(10 条数据) │
└───────────────────────┬──────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────┐
│ 5. 封装分页结果 │
│ - Page 对象继承 ArrayList,包含数据 + 分页信息 │
│ {pageNum=1, pageSize=10, total=100, pages=10, [...]} │
│ - 清除 ThreadLocal(finally 块) │
└───────────────────────┬──────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────┐
│ 6. 返回 Page 对象 │
│ List<User> users = userMapper.selectAll(); │
│ // users 实际类型是 Page<User> │
│ // 可强制转换:Page<User> page = (Page<User>) users; │
└──────────────────────────────────────────────────────────────┘

关键设计要点

设计点实现目的
ThreadLocal 存储分页参数LOCAL_PAGE.set(page)线程安全,同一线程内的分页参数隔离
拦截 Executor.query@Signature(type = Executor.class, ...)最上层拦截,可获取完整 SQL
反射修改 BoundSql.sqlfield.set(boundSql, pageSql)BoundSql.sql 是 final,只能反射修改
finally 清除 ThreadLocalPageHelper.clearPage()防止内存泄漏,避免影响后续查询
Page 继承 ArrayListpublic class Page<E> extends ArrayList<E>兼容 List 返回类型,无需修改 Mapper 接口

Q6:如何用插件实现数据权限控制(自动拼接 WHERE 条件)?实战

Section titled “Q6:如何用插件实现数据权限控制(自动拼接 WHERE 条件)?”

场景:所有查询自动追加 WHERE dept_id = #{currentUserDeptId},实现数据权限隔离。

实现方案

@Intercepts({
@Signature(
type = StatementHandler.class,
method = "prepare",
args = {Connection.class, Integer.class}
)
})
public class DataPermissionPlugin implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 1. 获取 StatementHandler
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
// 2. 通过 MetaObject 反射获取私有字段
MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
// 3. 获取 BoundSql(包含 SQL)
BoundSql boundSql = statementHandler.getBoundSql();
String originalSql = boundSql.getSql();
// 4. 判断是否需要数据权限控制(根据 Mapper 方法名、注解等)
MappedStatement ms = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
if (!needDataPermission(ms)) {
return invocation.proceed(); // 不需要数据权限,直接执行
}
// 5. 获取当前用户的部门 ID
Long deptId = SecurityContext.getCurrentUser().getDeptId();
// 6. 改写 SQL,追加数据权限条件
String newSql = wrapDataPermission(originalSql, deptId);
// 7. 反射修改 BoundSql.sql
Field field = boundSql.getClass().getDeclaredField("sql");
field.setAccessible(true);
field.set(boundSql, newSql);
// 8. 执行改写后的 SQL
return invocation.proceed();
}
/**
* 判断是否需要数据权限控制
*/
private boolean needDataPermission(MappedStatement ms) {
// 方法1:根据 Mapper 方法名判断
String statementId = ms.getId();
if (statementId.contains("DictMapper")) {
return false; // 字典表不需要数据权限
}
// 方法2:根据方法注解判断
try {
String className = statementId.substring(0, statementId.lastIndexOf("."));
String methodName = statementId.substring(statementId.lastIndexOf(".") + 1);
Class<?> mapperClass = Class.forName(className);
Method[] methods = mapperClass.getMethods();
for (Method method : methods) {
if (method.getName().equals(methodName)) {
DataPermission annotation = method.getAnnotation(DataPermission.class);
return annotation != null && annotation.enable();
}
}
} catch (Exception e) {
// ignore
}
return true; // 默认需要数据权限
}
/**
* 改写 SQL,追加数据权限条件
*/
private String wrapDataPermission(String originalSql, Long deptId) {
// 方案1:简单拼接(适用于简单 SQL)
// SELECT * FROM user → SELECT * FROM user WHERE dept_id = 123
// 方案2:使用子查询(适用于复杂 SQL)
// SELECT * FROM user WHERE name = '张三'
// → SELECT * FROM (SELECT * FROM user WHERE name = '张三') tmp WHERE dept_id = 123
if (originalSql.toUpperCase().contains("WHERE")) {
// 已有 WHERE 条件,使用子查询
return "SELECT * FROM (" + originalSql + ") tmp WHERE dept_id = " + deptId;
} else {
// 无 WHERE 条件,直接追加
return originalSql + " WHERE dept_id = " + deptId;
}
}
}

注解方式标记数据权限

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DataPermission {
boolean enable() default true;
String deptIdField() default "dept_id";
}
@Mapper
public interface UserMapper {
@DataPermission(enable = true) // 启用数据权限
List<User> selectAll();
@DataPermission(enable = false) // 禁用数据权限
List<User> selectAllWithoutPermission();
}

使用示例

// 原始 SQL
<select id="selectAll" resultType="User">
SELECT * FROM user WHERE status = 1
</select>
// 自动改写后的 SQL
SELECT * FROM (SELECT * FROM user WHERE status = 1) tmp WHERE dept_id = 123
// 效果:当前用户只能看到本部门的用户数据


@Intercepts({
@Signature(type = Executor.class, method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
@Signature(type = Executor.class, method = "update",
args = {MappedStatement.class, Object.class})
})
public class SlowSqlMonitorPlugin implements Interceptor {
private long slowSqlThreshold = 1000; // 默认 1 秒
private final Logger logger = LoggerFactory.getLogger(SlowSqlMonitorPlugin.class);
@Override
public Object intercept(Invocation invocation) throws Throwable {
MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
Object parameter = invocation.getArgs()[1];
long start = System.currentTimeMillis();
try {
Object result = invocation.proceed();
return result;
} finally {
long cost = System.currentTimeMillis() - start;
if (cost > slowSqlThreshold) {
BoundSql boundSql = ms.getBoundSql(parameter);
String sql = boundSql.getSql();
logger.warn("慢 SQL | 耗时: {}ms | SQL: {}", cost, sql);
}
}
}
@Override
public void setProperties(Properties properties) {
String threshold = properties.getProperty("slowSqlThreshold");
if (threshold != null) {
this.slowSqlThreshold = Long.parseLong(threshold);
}
}
}

配置

<plugin interceptor="com.example.SlowSqlMonitorPlugin">
<property name="slowSqlThreshold" value="500"/>
</plugin>
@Intercepts({
@Signature(type = Executor.class, method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
@Signature(type = Executor.class, method = "update",
args = {MappedStatement.class, Object.class})
})
public class ReadWriteSplittingPlugin implements Interceptor {
private DataSource masterDataSource; // 主库
private DataSource slaveDataSource; // 从库
@Override
public Object intercept(Invocation invocation) throws Throwable {
MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
SqlCommandType sqlCommandType = ms.getSqlCommandType();
// 读操作路由到从库,写操作路由到主库
if (sqlCommandType == SqlCommandType.SELECT) {
DynamicDataSource.setDataSource(slaveDataSource);
} else {
DynamicDataSource.setDataSource(masterDataSource);
}
try {
return invocation.proceed();
} finally {
DynamicDataSource.clearDataSource();
}
}
}

  1. 四大拦截对象:Executor、StatementHandler、ParameterHandler、ResultSetHandler
  2. 核心原理:JDK 动态代理 + 责任链模式
  3. 关键注解:@Intercepts + @Signature(必须精确匹配方法签名)
  4. 执行顺序:洋葱模型(前置外→内,后置内→外)
  5. PageHelper 原理:ThreadLocal + 拦截 Executor + SQL 改写 + finally 清除
  6. 典型应用:分页、SQL 日志、数据权限、读写分离、加解密
  7. 线程安全:插件是全局单例,intercept() 方法会被多线程并发调用
  8. 性能优化:只对目标类型生成代理,缓存反射结果