关键词:MyBatis, 插件机制, Interceptor, 动态代理, PageHelper, 分页原理
在日常的开发工作中,你是否遇到过这样的需求:想要在 SQL 执行前后添加一些通用逻辑,比如分页、日志记录、性能监控、数据脱敏等?如果每次都修改业务代码,不仅重复劳动,还容易造成代码混乱。MyBatis 的插件机制正是为了解决这类问题的利器。本文将从自定义插件的实现讲起,深入剖析其底层原理(JDK 动态代理),并以 PageHelper 为例,带你彻底搞懂分页插件的工作机制。
📑 目录
一、MyBatis 插件简介
插件是一种常见的扩展方式,大多数开源框架也都支持用户通过添加自定义插件的方式来扩展或者改变原有的功能。MyBatis 中也提供的有插件,虽然叫插件,但是实际上是通过**拦截器(Interceptor)**实现的。
在 MyBatis 的插件模块中涉及到两种重要的设计模式:
- 责任链模式:多个插件按顺序执行
- JDK 动态代理:实现方法的拦截和增强
这两种设计模式的技术知识是大家要掌握的,也是理解 MyBatis 插件机制的基础。
二、自定义插件实战
下面我们来看下如何实现一个自定义的插件。
2.1 创建 Interceptor 实现类
我们创建的拦截器必须要实现 Interceptor 接口,该接口的定义如下:
public interface Interceptor {
// 执行拦截逻辑的方法
Object intercept(Invocation invocation) throws Throwable;
// 决定是否触发 intercept() 方法
default Object plugin(Object target) {
return Plugin.wrap(target, this);
}
// 根据配置初始化 Intercept 对象
default void setProperties(Properties properties) {
// NOP
}
}
在 MyBatis 中,Interceptor 允许拦截的内容是:
| 目标对象 | 可拦截的方法 |
|---|---|
| Executor | update, query, flushStatements, commit, rollback, getTransaction, close, isClosed |
| ParameterHandler | getParameterObject, setParameters |
| ResultSetHandler | handleResultSets, handleOutputParameters |
| StatementHandler | prepare, parameterize, batch, update, query |
我们创建一个拦截 Executor 中的 query 和 close 方法的示例:
package com.boboedu.interceptor;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import java.util.Properties;
/**
* 自定义的拦截器
* @Signature 注解表示一个方法签名,唯一确定一个方法
*/
@Intercepts({
@Signature(
type = Executor.class // 需要拦截的类型
, method = "query" // 需要拦截的方法
// args 中指定被拦截方法的参数列表
, args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
),
@Signature(
type = Executor.class
, method = "close"
, args = {boolean.class}
)
})
public class FirstInterceptor implements Interceptor {
private int testProp;
/**
* 执行拦截逻辑的方法
*/
@Override
public Object intercept(Invocation invocation) throws Throwable {
System.out.println("FirstInterceptor 拦截之前 ....");
Object obj = invocation.proceed(); // 执行目标方法
System.out.println("FirstInterceptor 拦截之后 ....");
return obj;
}
/**
* 决定是否触发 intercept 方法
*/
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
System.out.println("---->" + properties.get("testProp"));
}
// getter & setter
public int getTestProp() {
return testProp;
}
public void setTestProp(int testProp) {
this.testProp = testProp;
}
}
关键点说明:
@Intercepts:声明这是一个拦截器@Signature:定义要拦截的方法签名,包含type(目标类)、method(方法名)、args(参数类型)intercept():核心方法,编写拦截逻辑invocation.proceed():调用目标方法,继续执行链
2.2 配置拦截器
创建好自定义的拦截器后,需要在 MyBatis 全局配置文件中注册:
<plugins>
<plugin interceptor="com.bobo.interceptor.FirstInterceptor">
<property name="testProp" value="1000"/>
</plugin>
</plugins>
2.3 运行程序
执行查询操作进行测试:
@Test
public void test1() throws Exception {
// 1. 获取配置文件
InputStream in = Resources.getResourceAsStream("mybatis-config.xml");
// 2. 加载解析配置文件并获取 SqlSessionFactory 对象
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(in);
// 3. 根据 SqlSessionFactory 对象获取 SqlSession 对象
SqlSession sqlSession = factory.openSession();
// 4. 通过 SqlSession 中提供的 API 方法来操作数据库
List<User> list = sqlSession.selectList("com.bobo.mapper.UserMapper.selectUserList");
for (User user : list) {
System.out.println(user);
}
// 5. 关闭会话
sqlSession.close();
}
运行后,控制台会输出:
FirstInterceptor 拦截之前 ....
FirstInterceptor 拦截之后 ....
三、插件实现原理深度解析
自定义插件的步骤虽然简单,但背后的实现原理却很精妙。下面我们来深入分析。
3.1 初始化操作
首先,我们来看下全局配置文件加载解析时做了什么操作。
MyBatis 会解析 <plugins> 标签,对应的代码逻辑如下:
private void pluginElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
// 获取 <plugin> 节点的 interceptor 属性的值
String interceptor = child.getStringAttribute("interceptor");
// 获取 <plugin> 下的所有的 properties 子节点
Properties properties = child.getChildrenAsProperties();
// 获取 Interceptor 对象
Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor)
.getDeclaredConstructor().newInstance();
// 设置 interceptor 的属性
interceptorInstance.setProperties(properties);
// Configuration 中记录 Interceptor
configuration.addInterceptor(interceptorInstance);
}
}
}
该方法用来解析全局配置文件中的 plugins 标签,然后创建对应的 Interceptor 对象,并封装属性信息。最后调用了 Configuration 对象中的方法:
public void addInterceptor(Interceptor interceptor) {
interceptorChain.addInterceptor(interceptor);
}
InterceptorChain 是拦截器链,负责管理所有的拦截器:
public class InterceptorChain {
// 保存所有的 Interceptor
private final List<Interceptor> interceptors = new ArrayList<>();
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target); // 创建对应的拦截器的代理对象
}
return target;
}
public void addInterceptor(Interceptor interceptor) {
interceptors.add(interceptor);
}
public List<Interceptor> getInterceptors() {
return Collections.unmodifiableList(interceptors);
}
}
3.2 代理对象的创建
在解析时创建了对应的 Interceptor 对象并保存在 InterceptorChain 中,那么这个拦截器是如何和目标对象关联的呢?
可拦截的四个核心对象:
- Executor(SQL 执行器)
- ParameterHandler(参数处理器)
- ResultSetHandler(结果集处理器)
- StatementHandler(SQL 语句处理器)
这些对象在创建时都会调用 pluginAll() 方法:
// Executor 创建时的代码示例
executor = (Executor) interceptorChain.pluginAll(executor);
// StatementHandler 创建时的代码示例
statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
进入 plugin() 方法,默认实现是:
default Object plugin(Object target) {
return Plugin.wrap(target, this);
}
然后进入 Plugin 工具类的 wrap() 方法:
/**
* 创建目标对象的代理对象
* 目标对象:Executor、ParameterHandler、ResultSetHandler、StatementHandler
*/
public static Object wrap(Object target, Interceptor interceptor) {
// 获取用户自定义 Interceptor 中 @Signature 注解的信息
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
// 获取目标类型
Class<?> type = target.getClass();
// 获取目标类型实现的所有接口
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
// 如果目标类型有实现的接口,就创建代理对象
if (interfaces.length > 0) {
return Proxy.newProxyInstance(
type.getClassLoader(),
interfaces,
new Plugin(target, interceptor, signatureMap));
}
// 否则原封不动地返回目标对象
return target;
}
Plugin 类的核心方法:
public class Plugin implements InvocationHandler {
private final Object target; // 目标对象
private final Interceptor interceptor; // 拦截器
private final Map<Class<?>, Set<Method>> signatureMap; // 记录 @Signature 注解的信息
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
// 获取当前方法所在类或接口中,可被当前 Interceptor 拦截的方法
Set<Method> methods = signatureMap.get(method.getDeclaringClass());
if (methods != null && methods.contains(method)) {
// 当前调用的方法需要被拦截,执行拦截操作
return interceptor.intercept(new Invocation(target, method, args));
}
// 不需要拦截,则调用目标对象中的方法
return method.invoke(target, args);
} catch (Exception e) {
throw ExceptionUtil.unwrapThrowable(e);
}
}
}
流程图解析:
请求 -> Executor.query() -> Plugin.invoke() ->
是否需要拦截?
├── 是 -> FirstInterceptor.intercept() -> invocation.proceed() -> 目标方法
└── 否 -> 直接执行目标方法
3.3 执行流程详解
以 Executor 的 query 方法为例,当查询请求到来时:
- 调用
executor.query()方法(实际调用的是代理对象的方法) - 触发
Plugin.invoke()方法 - 在
invoke()中判断是否需要拦截 - 如果需要拦截,执行自定义的
intercept()方法 - 在
intercept()中通过invocation.proceed()调用目标方法 - 返回结果
3.4 多拦截器执行顺序
如果配置了多个拦截器,执行顺序是怎样的呢?
配置顺序和执行顺序的关系:
- InterceptorChain 的 List 按照配置从上到下的顺序解析、添加
- 创建代理时也是按照 List 的顺序代理
- 执行时是从最后代理的对象开始(即配置顺序和执行顺序相反)
例如,配置了两个拦截器:Interceptor1、Interceptor2
配置顺序:Interceptor1 -> Interceptor2
执行顺序:Interceptor2 -> Interceptor1
相关对象作用总结:
| 对象 | 作用 |
|---|---|
| Interceptor | 自定义插件需要实现的接口 |
| InterceptorChain | 配置的插件解析后保存在 Configuration 中 |
| Plugin | 触发管理类,用于创建代理对象 |
| Invocation | 对被代理类进行包装,可调用 proceed() 执行被拦截的方法 |
四、PageHelper 分页插件原理
PageHelper 是 MyBatis 中最常用的分页插件。我们来看看它的实现原理。
4.1 PageHelper 的基本使用
添加依赖:
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>4.1.6</version>
</dependency>
配置插件:
<plugin interceptor="com.github.pagehelper.PageHelper">
<property name="dialect" value="mysql" />
<property name="offsetAsPageNum" value="true" />
<property name="rowBoundsWithCount" value="true" />
<property name="pageSizeZero" value="true" />
<property name="reasonable" value="false" />
</plugin>
使用示例:
// 设置分页参数
PageHelper.startPage(1, 5);
// 执行查询
List<User> list = userMapper.selectUserList();
就是这么简单!一行代码就实现了分页。
4.2 实现原理剖析
PageHelper 同样实现了 Interceptor 接口:
@Intercepts({
@Signature(type = Executor.class, method = "query", ...)
})
public class PageHelper implements Interceptor {
// ...
}
核心拦截逻辑:
public Object intercept(Invocation invocation) throws Throwable {
if (autoRuntimeDialect) { // 多数据源
SqlUtil sqlUtil = getSqlUtil(invocation);
return sqlUtil.processPage(invocation);
} else { // 单数据源
if (autoDialect) {
initSqlUtil(invocation);
}
return sqlUtil.processPage(invocation);
}
}
SqlUtil 的作用:
- 数据库类型专用的 SQL 工具类
- 一个数据库 URL 对应一个 SqlUtil 实例
- 内部有一个 Parser 对象(如 MySQL 对应 MysqlParser)
- 负责执行 count 查询、分页查询、保存 Page 对象等
Parser 创建:
public static Parser newParser(Dialect dialect) {
Parser parser = null;
switch (dialect) {
case mysql:
case mariadb:
case sqlite:
parser = new MysqlParser();
break;
case oracle:
parser = new OracleParser();
break;
case sqlserver:
parser = new SqlServerParser();
break;
// ... 其他数据库方言
}
return parser;
}
分页 SQL 的生成:
@Override
protected BoundSql getPageBoundSql(Object parameterObject) {
String tempSql = sql;
String orderBy = PageHelper.getOrderBy();
if (orderBy != null) {
tempSql = OrderByParser.converToOrderBySql(sql, orderBy);
}
// 根据方言生成对应的分页 SQL
tempSql = localParser.get().getPageSql(tempSql);
return new BoundSql(configuration, tempSql, ...);
}
MySQL 最终生成的分页 SQL:
SELECT * FROM user LIMIT ?, ?
五、应用场景分析
MyBatis 插件可以应用于多种场景:
| 应用场景 | 描述 | 实现方式 |
|---|---|---|
| 水平分表 | 按月度拆分费用表(如 fee_202001 ~ fee_202012) | 对 query/update 方法拦截,根据条件修改表名 |
| 数据脱敏 | 手机号中间四位、身份证号出生日期脱敏 | 对 query 方法拦截,对结果集脱敏处理 |
| 菜单权限控制 | 不同用户展示不同菜单 | 对 query 方法拦截,根据用户权限添加过滤条件 |
| 黑白名单 | 禁止执行某些危险 SQL(如 like %%) | 对 Executor 的 update/query 方法拦截,检查 SQL |
| 全局唯一 ID | 高并发环境下生成唯一 ID | 拦截 insert 方法,通过雪花算法生成 ID |
六、总结
本文从自定义插件的实现入手,深入剖析了 MyBatis 插件机制的核心原理:
- 插件本质:通过 Interceptor 接口和 JDK 动态代理实现方法拦截
- 可拦截对象:Executor、ParameterHandler、ResultSetHandler、StatementHandler
- 执行流程:解析配置 → 创建拦截器 → 创建代理对象 → 调用时判断拦截 → 执行自定义逻辑
- 多拦截器顺序:配置顺序和执行顺序相反
- PageHelper 原理:通过拦截 Executor.query(),动态生成分页 SQL
使用建议:
- 合理使用插件,避免过多插件影响性能
- 注意拦截器的执行顺序,避免逻辑冲突
- 分页插件建议使用最新版本,支持更多数据库和特性
希望这篇文章能帮助你深入理解 MyBatis 的插件机制,在实际项目中灵活运用!
💡 温馨提示:如果文章对你有帮助,欢迎点赞、收藏、关注!有任何问题欢迎在评论区留言讨论。
参考资源:
- MyBatis 官方文档:https://mybatis.org/mybatis-3/
- PageHelper GitHub:https://github.com/pagehelper/Mybatis-PageHelper
&spm=1001.2101.3001.5002&articleId=161199076&d=1&t=3&u=bbffcaad737d48d48adb1f61fa36e6ed)
1476

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



