当前位置: 首页 > news >正文

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 四大对象(ExecutorStatementHandlerParameterHandlerResultSetHandler)的方法调用,而插件则是对拦截器的封装,它为开发者提供了一种更便捷的方式来拓展 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:指定要拦截的对象类型,取值为ExecutorStatementHandlerParameterHandlerResultSetHandler之一。
    • 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属性指定数据库方言,如mysqloracle等,让 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();
}

相关文章:

  • 阿里云MaxCompute入门
  • 6月7日day47打卡
  • pycharm中提示C++ compiler not found -- please install a compiler
  • git小乌龟不显示图标状态解决方案
  • 【android bluetooth 协议分析 15】【SPP详解 1】【SPP 介绍】
  • 《JavaAI:稳定、高效、跨平台的AI编程工具优势解析》
  • Redis高可用架构
  • golang项目中如何使用私密仓库的扩展包
  • 【LLM大模型技术专题】「入门到精通系列教程」基于ai-openai-spring-boot-starter集成开发实战指南
  • uniapp- UTS 插件鸿蒙端开发示例 虽然我们这个示例简单 但是这个是难住很多人的一大步
  • vite+tailwind封装组件库
  • Android和硬件通信
  • 408第一季 - 数据结构 - 字符串和KMP算法
  • phosphobot开源程序是控制您的 SO-100 和 SO-101 机器人并训练 VLA AI 机器人开源模型
  • ubuntu20使用自主探索算法explore_lite实现机器人自主探索导航建图
  • PGSR : 基于平面的高斯溅射高保真表面重建【全流程分析与测试!】【2025最新版!!】
  • 中山大学美团港科大提出首个音频驱动多人对话视频生成MultiTalk,输入一个音频和提示,即可生成对应唇部、音频交互视频。
  • 【python与生活】如何构建一个解读IPO招股书的算法?
  • 机器学习的数学基础:神经网络
  • PCA笔记
  • 做网站 广州/最吸引人的引流话术
  • 郑州app拉新项目/长沙seo顾问
  • 免费项目管理软件app/太原seo全网营销
  • 做地方门户网站的排名/网站建设详细方案模板
  • 手机适配网站/企业seo优化服务
  • 迅睿cms建站/爱站网关键字挖掘