关键词:MyBatis, 插件机制, Interceptor, 动态代理, PageHelper, 分页原理
在日常的开发工作中,你是否遇到过这样的需求:想要在 SQL 执行前后添加一些通用逻辑,比如分页、日志记录、性能监控、数据脱敏等?如果每次都修改业务代码,不仅重复劳动,还容易造成代码混乱。MyBatis 的插件机制正是为了解决这类问题的利器。本文将从自定义插件的实现讲起,深入剖析其底层原理(JDK 动态代理),并以 PageHelper 为例,带你彻底搞懂分页插件的工作机制。
📑 目录
一、MyBatis 插件简介 二、自定义插件实战 2.1 创建 Interceptor 实现类 2.2 配置拦截器 2.3 运行程序 三、插件实现原理深度解析 3.1 初始化操作 3.2 代理对象的创建 3.3 执行流程详解 3.4 多拦截器执行顺序 四、PageHelper 分页插件原理 4.1 PageHelper 的基本使用 4.2 实现原理剖析 五、应用场景分析 六、总结
一、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 | |
| ParameterHandler | |
| ResultSetHandler | |
| StatementHandler |
我们创建一个拦截 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;/*** 执行拦截逻辑的方法*/@Overridepublic Object intercept(Invocation invocation) throws Throwable {System.out.println("FirstInterceptor 拦截之前 ....");Object obj = invocation.proceed(); // 执行目标方法System.out.println("FirstInterceptor 拦截之后 ....");return obj;}/*** 决定是否触发 intercept 方法*/@Overridepublic Object plugin(Object target) {return Plugin.wrap(target, this);}@Overridepublic void setProperties(Properties properties) {System.out.println("---->" + properties.get("testProp"));}// getter & setterpublic int getTestProp() {return testProp;}public void setTestProp(int testProp) {this.testProp = testProp;}}
关键点说明:
@Intercepts:声明这是一个拦截器 @Signature:定义要拦截的方法签名,包含 type(目标类)、method(方法名)、args(参数类型)intercept():核心方法,编写拦截逻辑 invocation.proceed():调用目标方法,继续执行链
2.2 配置拦截器
创建好自定义的拦截器后,需要在 MyBatis 全局配置文件中注册:
<plugins><plugininterceptor="com.bobo.interceptor.FirstInterceptor"><propertyname="testProp"value="1000"/></plugin></plugins>
2.3 运行程序
执行查询操作进行测试:
@Testpublic 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 中记录 Interceptorconfiguration.addInterceptor(interceptorInstance);}}}
该方法用来解析全局配置文件中的 plugins 标签,然后创建对应的 Interceptor 对象,并封装属性信息。最后调用了 Configuration 对象中的方法:
public void addInterceptor(Interceptor interceptor) {interceptorChain.addInterceptor(interceptor);}
InterceptorChain 是拦截器链,负责管理所有的拦截器:
public class InterceptorChain {// 保存所有的 Interceptorprivate 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 classPluginimplementsInvocationHandler{private final Object target; // 目标对象private final Interceptor interceptor; // 拦截器private final Map<Class<?>, Set<Method>> signatureMap; // 记录 @Signature 注解的信息@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throwsThrowable{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 | |
| Plugin | |
| Invocation |
四、PageHelper 分页插件原理
PageHelper 是 MyBatis 中最常用的分页插件。我们来看看它的实现原理。
4.1 PageHelper 的基本使用
添加依赖:
<dependency><groupId>com.github.pagehelper</groupId><artifactId>pagehelper</artifactId><version>4.1.6</version></dependency>
配置插件:
<plugininterceptor="com.github.pagehelper.PageHelper"><propertyname="dialect"value="mysql" /><propertyname="offsetAsPageNum"value="true" /><propertyname="rowBoundsWithCount"value="true" /><propertyname="pageSizeZero"value="true" /><propertyname="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 的生成:
@Overrideprotected BoundSql getPageBoundSql(Object parameterObject) {String tempSql = sql;String orderBy = PageHelper.getOrderBy();if (orderBy != null) {tempSql = OrderByParser.converToOrderBySql(sql, orderBy);}// 根据方言生成对应的分页 SQLtempSql = localParser.get().getPageSql(tempSql);return new BoundSql(configuration, tempSql, ...);}
MySQL 最终生成的分页 SQL:
SELECT * FROM user LIMIT ?, ?五、应用场景分析
MyBatis 插件可以应用于多种场景:
| 水平分表 | ||
| 数据脱敏 | ||
| 菜单权限控制 | ||
| 黑白名单 | ||
| 全局唯一 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
夜雨聆风