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();
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.pswp.cn/bicheng/83998.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Linux中 SONAME 的作用

🧠 一、从 -lexample 到 SONAME ✅ 假设你有以下文件结构: /libexample.so → libexample.so.1 /libexample.so.1 → libexample.so.1.0.0 /libexample.so.1.0.0 # SONAME: libexample.so.1/libexample.so.2 → libexample.so.2.0.0 /libexample.so.2.0…

热门消息中间件汇总

文章目录 前言RabbitMQ基本介绍核心特性适用场景 Kafka基本介绍核心特性适用场景 RocketMQ基本介绍核心特性适用场景 NATS基本介绍核心特性适用场景 总结选型建议与未来趋势选型建议未来趋势 结语 前言 大家后&#xff0c;我是沛哥儿。作为技术领域的老湿机&#xff0c;在消息…

【DAY42】Grad-CAM与Hook函数

内容来自浙大疏锦行python打卡训练营 浙大疏锦行 知识点: 回调函数lambda函数hook函数的模块钩子和张量钩子Grad-CAM的示例 作业&#xff1a;理解下今天的代码即可 在深度学习中&#xff0c;我们经常需要查看或修改模型中间层的输出或梯度。然而&#xff0c;标准的前向传播和反…

C++032(static变量)

static变量 static变量是静态存储变量&#xff0c;定义变量时系统就会为其分配固定的存储单元&#xff0c;直至整个程序运行结束。之前我们接触过的全局变量即为static变量&#xff0c;它们存放在静态存储区中。使用static关键字&#xff0c;可将变量声明成static变量。例如&a…

N元语言模型 —— 一文讲懂!!!

目录 引言 一. 基本知识 二.参数估计 三.数据平滑 一.加1法 二.减值法/折扣法 ​编辑 1.Good-Turing 估计 ​编辑 2.Back-off (后备/后退)方法 3.绝对减值法 ​编辑4.线性减值法 5.比较 三.删除插值法(Deleted interpolation) 四.模型自适应 引言 本章节讲的…

SpringAI Alibaba实战文生图

1️⃣ 前置准备&#xff1a;搭建开发环境与服务配置&#x1f680; &#x1f527; 1.1 环境要求 JDK 17&#xff08;推荐 JDK 21&#xff09;、Spring Boot 3.x&#xff08;本案例使用 3.3.4&#xff09;、阿里云百炼大模型服务 API Key。需在阿里云控制台完成服务开通并获取有…

实战二:开发网页端界面完成黑白视频转为彩色视频

​一、需求描述 设计一个简单的视频上色应用&#xff0c;用户可以通过网页界面上传黑白视频&#xff0c;系统会自动将其转换为彩色视频。整个过程对用户来说非常简单直观&#xff0c;不需要了解技术细节。 效果图 ​二、实现思路 总体思路&#xff1a; 用户通过Gradio界面上…

Kotlin List 操作全面指南

在传统 Java 开发 List 相关的 API 中&#xff0c;有着样板代码冗长、缺乏链式调用、空安全等问题。 Kotlin 这门语言 为 List 提供了丰富的扩展函数&#xff0c;这些函数大大简化了集合操作&#xff0c;解决了传统 Java 集合 API 中的许多痛点。 一、基础操作 1. 创建 List …

硬盘寻址全解析:从 CHS 三维迷宫到 LBA 线性王国

在数字存储的底层世界&#xff0c;硬盘如同一个巨大的 “数据图书馆”&#xff0c;而寻址模式就是决定如何高效找到 “书籍”&#xff08;扇区&#xff09;的核心规则。从早期基于物理结构的 CHS&#xff08;柱面 - 磁头 - 扇区&#xff09;三维寻址&#xff0c;到现代抽象化的…

oracle 11g ADG备库报错ORA-00449 lgwr unexpectedly分析处理

问题背景 昨天遇到群友提问&#xff0c;遇到ADG备库挂了的情况 数据版本:11.2.0.4 操作系统:Centos7.9 环境&#xff1a;ADG主备库&#xff0c;主库为RAC&#xff0c;备库也是RAC 具体报错ORA-00449以及ORA-04021 看样子是LGWR挂了&#xff0c;还有个锁等待。 问题分析 先…

Python——day46通道注意力(SE注意力)

一、 什么是注意力 注意力机制是一种让模型学会「选择性关注重要信息」的特征提取器&#xff0c;就像人类视觉会自动忽略背景&#xff0c;聚焦于图片中的主体&#xff08;如猫、汽车&#xff09;。 transformer中的叫做自注意力机制&#xff0c;他是一种自己学习自己的机制&…

入门AJAX——XMLHttpRequest(Post)

一、前言 在上篇文章中&#xff0c;我们已经介绍了 HMLHttpRequest 的GET 请求的基本用法&#xff0c;并基于我提供的接口练习了两个简单的例子。如果你还没有看过第一篇文章&#xff0c;强烈建议你在学习完上篇文章后再学习本篇文章&#xff1a; &#x1f517;入门AJAX——XM…

​BEV和OCC学习-3:mmdet3d 坐标系

目录 坐标系 转向角 (yaw) 的定义 框尺寸的定义 与支持的数据集的原始坐标系的关系 KITTI Waymo NuScenes Lyft ScanNet SUN RGB-D S3DIS 坐标系 坐标系 — MMDetection3D 1.4.0 文档https://mmdetection3d.readthedocs.io/zh-cn/latest/user_guides/coord_sys_tuto…

Redis高可用架构

概述 Redis作为常用的缓存中间件&#xff0c;因其高性能&#xff0c;丰富的数据结构&#xff0c;使用简单等&#xff0c;常被用在需要一定高性能的To C业务场景中&#xff0c;如「秒杀场景」「用户信息中心」「帖子」「群聊」等等大家常见的业务场景中&#xff0c;以提高服务的…

使用WPF的Microsoft.Xaml.Behaviors.Wpf中通用 UI 元素事件

Nuget下载之后记得要先引用下面的 xmlns:i"http://schemas.microsoft.com/xaml/behaviors" <!-- 鼠标事件 --> <i:EventTrigger EventName"MouseEnter"/> <!-- 鼠标进入 --> <i:EventTrigger EventName"MouseLeave"/&g…

敏捷开发中如何避免过度加班

在敏捷开发过程中避免过度加班&#xff0c;需要明确敏捷原则、合理规划迭代任务、加强团队沟通、优化流程效率、设定合理的工作负荷、注重团队士气和成员健康。明确敏捷原则&#xff0c;即保证可持续发展的步调&#xff0c;避免频繁地变更需求、过度承诺任务量。合理规划迭代任…

JSON解析崩溃原因及解决方案

问题记录&#xff1a; /************************************************| * 描述: 将ID124执行NFC操作-JSON解析为结构体* 函数名: cJSON_ID124_to_struct* 参数[ I]: *json_string 待解析的指针* 参数[II]: *wireless_rxd 结构体指针* 返回: 成功返回0 失…

业务系统对接大模型的基础方案:架构设计与关键步骤

业务系统对接大模型&#xff1a;架构设计与关键步骤 在当今数字化转型的浪潮中&#xff0c;大语言模型&#xff08;LLM&#xff09;已成为企业提升业务效率和创新能力的关键技术之一。将大模型集成到业务系统中&#xff0c;不仅可以优化用户体验&#xff0c;还能为业务决策提供…

Edge(Bing)自动领积分脚本部署——基于python和Selenium(附源码)

微软的 Microsoft Rewards 计划可以通过 Bing 搜索赚取积分&#xff0c;积分可以兑换礼品卡、游戏等。每天的搜索任务不多&#xff0c;我们可以用脚本自动完成&#xff0c;提高效率&#xff0c;解放双手。 本文将手把手教你如何部署一个自动刷积分脚本&#xff0c;并解释其背…

前端基础之《Vue(19)—状态管理》

一、什么是状态管理 1、Vue版本问题 Vue2 Vuex3 Vue3 Vuex4 / Pinia2 在使用任何技术的时候&#xff0c;都先要去搜索一下版本&#xff0c;你的版本和脚手架环境是否兼容。 2、安装Vuex yarn add vuex3.6.2 3、状态管理 状态&#xff0c;在应用程序中表示数据&#xff0c…