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
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/83998.shtml
英文地址,請注明出處:http://en.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…