MyBatis 插件机制深度解析
面试官:你知道 MyBatis 的插件机制吗?它是怎么实现的?
你:知道。MyBatis 插件基于 JDK 动态代理和责任链模式实现,可以拦截 Executor、StatementHandler、ParameterHandler、ResultSetHandler 四大核心对象的方法,实现分页、SQL 日志、数据权限等功能。
面试官:能详细说说 PageHelper 的分页原理吗?为什么调用
PageHelper.startPage()就能分页?
这个追问直击核心——能说出 ThreadLocal、拦截 Executor、SQL 改写这些关键步骤的候选人,才算真正理解插件机制的实战应用。
链式追问一:插件基础
Section titled “链式追问一:插件基础”Q1:MyBatis 插件能拦截哪些对象?每个对象能拦截什么方法?必考
Section titled “Q1:MyBatis 插件能拦截哪些对象?每个对象能拦截什么方法?”MyBatis 插件只能拦截以下四大核心对象的方法:
四大对象拦截点对比表:
| 对象 | 作用 | 可拦截方法 | 典型应用场景 |
|---|---|---|---|
| Executor | SQL 执行调度器 | update, query, flushStatements, commit, rollback, getTransaction, close, isClosed | 读写分离、分库分表、SQL 日志 |
| StatementHandler | Statement 管理器 | 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")); }}注册插件:
<plugins> <plugin interceptor="com.example.plugin.SqlCostMonitorPlugin"> <!-- 传递配置参数 --> <property name="slowSqlThreshold" value="500"/> </plugin></plugins>Spring Boot 集成:
@Configurationpublic 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; }}关键细节:
@Signature必须精确匹配方法签名:args参数类型必须与方法声明完全一致,否则无法拦截invocation.proceed()必须调用:否则后续插件和原方法都不会执行plugin()方法优化:只对目标类型生成代理,避免不必要的代理对象- 异常处理:拦截方法抛出异常会影响整个 SQL 执行链,需谨慎处理
链式追问二:插件原理
Section titled “链式追问二:插件原理”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@Overridepublic Object intercept(Invocation invocation) throws Throwable { System.out.println("A 前置"); Object result = invocation.proceed(); // 调用下一个插件 System.out.println("A 后置"); return result;}
// PluginB@Overridepublic Object intercept(Invocation invocation) throws Throwable { System.out.println("B 前置"); Object result = invocation.proceed(); System.out.println("B 后置"); return result;}
// PluginC@Overridepublic 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 后置链式追问三:PageHelper 分页原理
Section titled “链式追问三:PageHelper 分页原理”Q5:PageHelper 是怎么实现分页的?为什么调用 PageHelper.startPage() 就能分页?必考
Section titled “Q5:PageHelper 是怎么实现分页的?为什么调用 PageHelper.startPage() 就能分页?”这是 MyBatis 插件最典型的应用案例,面试官很喜欢考。
PageHelper 分页实现原理(三步走):
第一步:PageHelper.startPage() 将分页参数存入 ThreadLocal
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 清除分页参数(防止内存泄漏)
@Overridepublic 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.sql | field.set(boundSql, pageSql) | BoundSql.sql 是 final,只能反射修改 |
| finally 清除 ThreadLocal | PageHelper.clearPage() | 防止内存泄漏,避免影响后续查询 |
| Page 继承 ArrayList | public 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";}
@Mapperpublic 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>
// 自动改写后的 SQLSELECT * FROM (SELECT * FROM user WHERE status = 1) tmp WHERE dept_id = 123
// 效果:当前用户只能看到本部门的用户数据高频面试题总结
Section titled “高频面试题总结”实战案例:性能优化
Section titled “实战案例:性能优化”案例 1:慢 SQL 监控插件
Section titled “案例 1:慢 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 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>案例 2:读写分离插件
Section titled “案例 2:读写分离插件”@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(); } }}关键知识点速记
Section titled “关键知识点速记”- 四大拦截对象:Executor、StatementHandler、ParameterHandler、ResultSetHandler
- 核心原理:JDK 动态代理 + 责任链模式
- 关键注解:@Intercepts + @Signature(必须精确匹配方法签名)
- 执行顺序:洋葱模型(前置外→内,后置内→外)
- PageHelper 原理:ThreadLocal + 拦截 Executor + SQL 改写 + finally 清除
- 典型应用:分页、SQL 日志、数据权限、读写分离、加解密
- 线程安全:插件是全局单例,intercept() 方法会被多线程并发调用
- 性能优化:只对目标类型生成代理,缓存反射结果