Mybatis 拦截器 与 PageHelper 源码解析
Mybatis 拦截器 与 PageHelper 源码解析
- 一、MyBatis插件机制的设计思想
- 二、Interceptor接口核心解析
- 2.1 核心方法
- 2.2 @Intercepts、@Signature 注解
- 2.3 自定义拦截器
- 三、PageHelper 介绍
- 3.1 使用姿势
- 3.2 参数与返回值
- 3.3 使用小细节
- 四、PageHelper 核心源码解析
- 4.1 分页入口:PageHelper.startPage()
- 4.2 拦截器核心:PageInterceptor
- 4.3 分页SQL生成:AbstractHelperDialect
在
Java
开发领域,
MyBatis
作为一款优秀的持久层框架,凭借其灵活的配置和强大的功能深受开发者喜爱。其中,
MyBatis
的插件机制和基于该机制实现的
PageHelper
分页插件,更是极大地提升了开发效率。
本文将深入解析
MyBatis
拦截器以及
PageHelper
的源码,带大家领略其设计思想与实现原理。
一、MyBatis插件机制的设计思想
插件
底层依赖拦截器
来实现功能拓展,拦截器负责拦截 MyBatis
四大对象(Executor
、StatementHandler
、ParameterHandler
、ResultSetHandler
)的方法调用,而插件则是对拦截器的封装,它为开发者提供了一种更便捷的方式来拓展 MyBatis
的功能。
👉拦截器与插件的关系:
- 拦截器(Interceptor):是实现具体拦截逻辑的底层组件
- 插件(Plugin):是
MyBatis
对拦截器的包装与集成,通过动态代理将拦截器织入目标对象
👉设计模式思想:
MyBatis
的插件机制本质上是基于责任链模式和动态代理模式实现的
- 责任链模式:好比快递分拣流水线,多个插件(拦截器)按顺序 “接力” 处理
SQL
请求。
比如先记录 SQL 日志,再加密参数,最后统计耗时,每个插件各司其职,有序增强 SQL 执行过程想 - 动态代理:动态代理在
MyBatis
插件中扮演着关键角色。Interceptor
接口的plugin
方法来判断当前拦截器是否适用于目标对象,这个判断依据是@Intercepts
和@Signature
定义的拦截规则。如果适用,就会为目标对象创建一个动态代理对象。这个代理对象和目标对象具有相同的方法定义,当调用代理对象的方法时,实际上会执行拦截器的intercept
方法。开发者在intercept
方法中编写的自定义逻辑,比如修改 SQL 语句、添加性能监控代码等,就能在目标方法执行前后生效,从而实现对目标方法的功能增强
二、Interceptor接口核心解析
2.1 核心方法
public interface Interceptor {Object intercept(Invocation invocation) throws Throwable; // 核心拦截逻辑Object plugin(Object target); // 用Plugin.wrap()生成代理对象void setProperties(Properties properties); // 读取配置参数}
在 MyBatis
中,Interceptor
接口是实现插件功能的核心,它包含三个核心方法:
Object intercept(Invocation invocation)
:该方法是拦截器的核心逻辑所在,当目标方法被调用时,会进入此方法。
Invocation
对象封装了被拦截方法的信息,包括目标对象、方法参数等,开发者可以在该方法中编写自定义逻辑,例如修改参数、记录日志、添加额外功能等,执行完自定义逻辑后,通过invocation.proceed()
调用目标方法Object plugin(Object target)
:该方法用于生成目标对象的代理对象。
通常无需修改:99%的场景下,直接返回Plugin.wrap(target, this)
即可满足需求void setProperties(Properties properties)
:该方法用于设置插件的属性,在 MyBatis 的配置文件中配置的插件属性,会通过该方法传递进来,开发者可以根据这些属性来动态调整拦截器的行为
2.2 @Intercepts、@Signature 注解
@Intercepts
和@Signature
注解用于定义拦截器的拦截规则
@Intercepts
:该注解用于声明一个拦截器要拦截的多个方法签名,它包含一个@Signature
数组。@Signature
:该注解用于定义具体的拦截方法签名,它包含三个属性:type
:指定要拦截的对象类型,取值为Executor
、StatementHandler
、ParameterHandler
、ResultSetHandler
之一。method
:指定要拦截的方法名称args
:指定要拦截方法的参数类型数组
2.3 自定义拦截器
下面我们通过一个自定义 SQL 耗时拦截器的例子来进一步理解 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;@Intercepts({@Signature(type = Executor.class,method = "query",args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})}
)
public class MyInterceptor implements Interceptor {@Overridepublic Object intercept(Invocation invocation) throws Throwable {long start = System.currentTimeMillis();Object result = invocation.proceed(); // 执行原方法long cost = System.currentTimeMillis() - start;System.out.println("SQL执行耗时: " + cost + "ms");return result;}@Overridepublic Object plugin(Object target) {return Plugin.wrap(target, this);}@Overridepublic void setProperties(Properties properties) {// String logLevel = properties.getProperty("logLevel", "INFO");}
}
在 MyBatis 的配置文件中添加如下配置启用该插件:
<!--插件-->
<plugins><plugin interceptor="com.coderzpw.interceptors.MyInterceptor"/>
</plugins>
三、PageHelper 介绍
3.1 使用姿势
PageHelper
是一款非常方便的 MyBatis
分页插件,使用它可以轻松实现分页功能。使用步骤如下:
1. 在项目的pom.xml
文件中添加 PageHelper
依赖:
<dependency><groupId>com.github.pagehelper</groupId><artifactId>pagehelper</artifactId><version>5.3.3</version>
</dependency>
2. 在 MyBatis
配置文件中配置 PageHelper
插件:
<plugins><plugin interceptor="com.github.pagehelper.PageInterceptor" />
</plugins>
3. 在业务代码中使用 PageHelper
进行分页查询:
// 设置分页参数,第一个参数是页码,第二个参数是每页显示的记录数
PageHelper.startPage(2, 5);
// 执行查询
List<User> userList = userMapper.selectAllUsers();
// 获取分页信息
PageInfo<User> pageInfo = new PageInfo<>(userList);
System.out.println("总记录数:" + pageInfo.getTotal());
System.out.println("总页数:" + pageInfo.getPages());
System.out.println("当前页数据:" + pageInfo.getList());
3.2 参数与返回值
- 参数:
PageHelper.startPage(int pageNum, int pageSize)
方法中的pageNum
表示页码,从 1 开始;pageSize
表示每页显示的记录数。此外,还可以通过PageHelper.orderBy(String orderBy)
方法设置排序规则,例如PageHelper.orderBy("id desc")
- 返回值:执行完查询后,通过
PageInfo
对象可以获取到丰富的分页信息,如总记录数getTotal()
、总页数getPages()
、当前页数据getList()
、是否为第一页isIsFirstPage()
、是否为最后一页isIsLastPage()
等。
3.3 使用小细节
- 先执行
count
操作再进行分页查询:PageHelper
在执行分页查询时,会先自动执行一条COUNT(0)
语句来获取总记录数,然后再根据分页参数执行真正的分页查询(这可能会对性能产生一定影响,特别是在数据量较大的情况下)
第三个入参设置为false
,则不计总数,例如PageHelper.startPage(2, 5, false)
(不过此时PageInfo
中的总记录数和总页数将无法正确获取) - 多数据源问题:在使用多数据源时,需要注意
PageHelper
的配置和使用,确保分页插件能够正确应用到对应的数据源上。可以通过设置helperDialect
属性指定数据库方言,如mysql
、oracle
等,让PageHelper
生成正确的分页SQL
四、PageHelper 核心源码解析
4.1 分页入口:PageHelper.startPage()
public static <E> Page<E> startPage(int pageNum, int pageSize) {return startPage(pageNum, pageSize, DEFAULT_COUNT);
}
public static <E> Page<E> startPage(int pageNum, int pageSize, boolean count) {return startPage(pageNum, pageSize, count, null, null);
}
public static <E> Page<E> startPage(int pageNum, int pageSize, boolean count, Boolean reasonable, Boolean pageSizeZero) {// 1. 创建分页对象实例// 使用传入的参数初始化: 页码/每页数量/是否查询总数Page<E> page = new Page<E>(pageNum, pageSize, count);// 2. 设置合理化参数(自动修正页码)// 当reasonable=true时:页码<1自动设为1,超出最大页自动设为末页// 注意:Boolean对象允许为null,保留默认配置page.setReasonable(reasonable);// 3. 设置pageSizeZero特殊处理标志// 当pageSizeZero=true且pageSize=0时:返回所有结果(不分页)page.setPageSizeZero(pageSizeZero);// 4. 获取当前线程可能存在的旧分页对象// 通过ThreadLocal实现线程隔离的分页参数存储Page<E> oldPage = getLocalPage();// 5. 特殊处理:排序条件继承// 场景:当之前调用过orderBy()设置排序但未实际分页时// 作用:确保新的分页对象能继承之前的排序条件if (oldPage != null && oldPage.isOrderByOnly()) {// 将旧分页的排序条件(如"name ASC")复制到新分页page.setOrderBy(oldPage.getOrderBy());}// 6. 将新分页对象绑定到当前线程// 供MyBatis拦截器或后续数据库操作获取分页参数setLocalPage(page);// 7. 返回初始化完成的分页对象return page;
}
4.2 拦截器核心:PageInterceptor
@Override
public Object intercept(Invocation invocation) throws Throwable {try {// 1. 获取MyBatis执行参数Object[] args = invocation.getArgs();MappedStatement ms = (MappedStatement) args[0]; // SQL映射配置Object parameter = args[1]; // 查询参数RowBounds rowBounds = (RowBounds) args[2]; // MyBatis原生分页对象ResultHandler resultHandler = (ResultHandler) args[3];// 结果处理器// 2. 获取执行器并准备缓存KeyExecutor executor = (Executor) invocation.getTarget();CacheKey cacheKey;BoundSql boundSql;// 3. 适配不同版本的MyBatis参数结构if (args.length == 4) { // MyBatis 3.4.x及以下版本boundSql = ms.getBoundSql(parameter);cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);} else { // MyBatis 3.5.x及以上版本cacheKey = (CacheKey) args[4];boundSql = (BoundSql) args[5];}// 4. 确保分页方言初始化checkDialectExists();// 5. 执行BoundSql拦截器链(自定义SQL改写)if (dialect instanceof BoundSqlInterceptor.Chain) {boundSql = ((BoundSqlInterceptor.Chain) dialect).doBoundSql(BoundSqlInterceptor.Type.ORIGINAL, boundSql, cacheKey);}List resultList;// 6. 核心分页判断逻辑if (!dialect.skip(ms, parameter, rowBounds)) { // 需要分页// 7. 调试模式:检测分页参数未消费问题debugStackTraceLog();// 8. COUNT查询处理流程if (dialect.beforeCount(ms, parameter, rowBounds)) {// 执行COUNT查询获取总数Long count = count(executor, ms, parameter, rowBounds, null, boundSql);// 9. 根据COUNT结果判断是否继续分页if (!dialect.afterCount(count, parameter, rowBounds)) {// 总数不满足分页条件(如count=0),直接返回空结果return dialect.afterPage(new ArrayList(), parameter, rowBounds);}}// 10. 【核心】执行分页查询resultList = ExecutorUtil.pageQuery(dialect, executor, ms, parameter, rowBounds, resultHandler, boundSql, cacheKey);} else {// 11. 跳过分页:使用MyBatis原生内存分页resultList = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);}// 12. 分页后处理(包装结果集)return dialect.afterPage(resultList, parameter, rowBounds);} finally {// 13. 最终清理(确保ThreadLocal分页参数移除)if (dialect != null) {dialect.afterAll();}}
}
4.3 分页SQL生成:AbstractHelperDialect
/*** 生成分页SQL(核心分页处理逻辑)* * @param ms SQL映射配置* @param boundSql 原始SQL绑定对象* @param parameterObject 查询参数对象* @param rowBounds 分页参数(实际是Page对象)* @param pageKey 分页缓存Key* @return 处理后的分页SQL*/
public String getPageSql(MappedStatement ms, BoundSql boundSql, Object parameterObject, RowBounds rowBounds, CacheKey pageKey) {// 1. 获取原始SQL语句String sql = boundSql.getSql();// 2. 获取当前线程的分页对象(通过ThreadLocal存储)Page page = getLocalPage();// 3. 处理ORDER BY排序逻辑String orderBy = page.getOrderBy(); // 获取分页对象中的排序字段if (StringUtil.isNotEmpty(orderBy)) {// 更新缓存Key(防止排序不同导致错误缓存)pageKey.update(orderBy);// 4. 将排序条件注入原始SQL(核心排序处理)sql = OrderByParser.converToOrderBySql(sql, orderBy, jSqlParser);}// 5. 检查是否仅需排序不分页if (page.isOrderByOnly()) {// 仅排序模式:直接返回添加了ORDER BY的SQL(不进行分页处理)return sql;}// 6. 【核心】核心分页SQL生成(调用具体数据库方言实现)return getPageSql(sql, page, pageKey);
}
以MySQL
方言为例的getPageSql实现:
public String getPageSql(String sql, Page page, CacheKey pageKey) {StringBuilder sqlBuilder = new StringBuilder(sql.length() + 14);sqlBuilder.append(sql);if (page.getStartRow() == 0) {sqlBuilder.append("\n LIMIT ? ");} else {sqlBuilder.append("\n LIMIT ?, ? ");}return sqlBuilder.toString();
}