插件机制
问题
MyBatis 的插件机制是怎样的?如何自定义一个分页插件?
答案
插件原理
MyBatis 插件基于 JDK 动态代理,可以拦截四大核心对象的方法:
| 可拦截对象 | 典型方法 | 用途 |
|---|---|---|
| Executor | update, query | SQL 执行前后处理 |
| StatementHandler | prepare, parameterize | SQL 语句处理 |
| ParameterHandler | setParameters | 参数设置 |
| ResultSetHandler | handleResultSets | 结果集处理 |
多个插件会形成代理链(责任链模式),按配置的逆序执行。
自定义插件
慢 SQL 监控插件
@Intercepts({
@Signature(
type = StatementHandler.class, // 拦截的对象
method = "query", // 拦截的方法
args = {Statement.class, ResultHandler.class} // 方法参数
)
})
public class SlowSqlPlugin implements Interceptor {
private long threshold = 1000; // 慢 SQL 阈值(毫秒)
@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();
log.warn("慢SQL [{}ms]: {}", cost, boundSql.getSql());
}
}
}
@Override
public Object plugin(Object target) {
// 如果是目标类型才创建代理,否则直接返回
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
// 读取配置属性
String t = properties.getProperty("threshold");
if (t != null) {
this.threshold = Long.parseLong(t);
}
}
}
注册插件:
application.yml(Spring Boot)
mybatis:
configuration:
plugins:
- com.example.plugin.SlowSqlPlugin
分页插件原理(PageHelper)
PageHelper 是最常用的 MyBatis 分页插件:
PageHelper 使用
// 在查询前设置分页参数
PageHelper.startPage(1, 10); // 第1页,每页10条
List<User> users = userMapper.listAll();
// users 实际类型是 Page<User>,包含分页信息
PageInfo<User> pageInfo = new PageInfo<>(users);
// pageInfo.getTotal() 总记录数
// pageInfo.getPages() 总页数
// pageInfo.getList() 当前页数据
PageHelper 的核心原理:
PageHelper 注意事项
startPage()必须紧邻查询方法,中间不能有其他查询startPage()使用 ThreadLocal 存储,如果异常导致未被消费会内存泄漏- 不支持嵌套查询的分页(如 collection 中的子查询)
常用的 MyBatis 插件
| 插件 | 功能 |
|---|---|
| PageHelper | 物理分页 |
| MyBatis-Plus | 通用 CRUD、代码生成、分页 |
| p6spy | SQL 日志打印(参数替换后的完整 SQL) |
| 自定义审计插件 | 自动填充 create_time、update_time |
| 自定义加解密插件 | 敏感字段加密存储 |
常见面试问题
Q1: MyBatis 插件的原理?
答案:
MyBatis 通过 JDK 动态代理实现插件机制。可以拦截四大核心对象(Executor、StatementHandler、ParameterHandler、ResultSetHandler)的方法。配置多个插件时按顺序创建代理链(最后配置的先执行)。自定义插件需实现 Interceptor 接口,通过 @Intercepts 注解指定拦截目标。
Q2: PageHelper 分页原理?
答案:
startPage()将分页参数存入 ThreadLocal- Mapper 执行查询时,PageHelper 的拦截器拦截
Executor.query - 从 ThreadLocal 取出分页参数,改写 SQL 添加
LIMIT/OFFSET - 同时执行
COUNT查询获取总记录数 - 清除 ThreadLocal 并返回
Page对象
Q3: MyBatis 插件能拦截哪些对象?
答案:
四种:Executor(SQL 执行)、StatementHandler(语句创建和参数设置)、ParameterHandler(参数处理)、ResultSetHandler(结果集映射)。通过 @Intercepts 和 @Signature 注解精确指定拦截的类、方法和参数。
Q4: 如何实现自动填充 createTime/updateTime?
答案:
拦截 Executor.update 方法(覆盖 insert 和 update),在执行前通过反射或 MetaObject 设置时间字段:
@Intercepts(@Signature(type = Executor.class, method = "update",
args = {MappedStatement.class, Object.class}))
public class AutoFillPlugin implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object param = invocation.getArgs()[1];
MetaObject metaObject = SystemMetaObject.forObject(param);
if (metaObject.hasSetter("updateTime")) {
metaObject.setValue("updateTime", LocalDateTime.now());
}
return invocation.proceed();
}
}
MyBatis-Plus 内置了 MetaObjectHandler 提供更优雅的自动填充方案。
相关链接
- MyBatis 插件文档
- PageHelper 文档
- 执行流程 - 插件在执行流程中的位置