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