分頁查詢是企業級應用的高頻需求。MyBatis 通過攔截器機制實現無侵入式分頁,本文深度解析 PageInterceptor 的核心原理、MySQL/Oracle 方言適配、總記錄數統計優化及 Spring 整合配置全流程。
SQL 執行流程:
StatementHandler.prepare()
→ RoutingStatementHandler
→ BaseStatementHandler (Simple/Prepared/Callable)
→ 實際 SQL 執行
攔截點精準鎖定 StatementHandler.prepare(Connection) 方法,在 SQL 預編譯前完成分頁改寫。
Interceptor 接口,核心邏輯載體@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class})})
public class PageInterceptor implements Interceptor {
public Object intercept(Invocation invocation) throws Throwable {
// 1. 獲取 RoutingStatementHandler
RoutingStatementHandler handler = (RoutingStatementHandler) invocation.getTarget();
// 2. 反射獲取委託對象
StatementHandler delegate = (StatementHandler) ReflectHelper.getFieldValue(handler, "delegate");
BoundSql boundSql = delegate.getBoundSql();
// 3. 參數校驗:必須包含 Page 對象
Object paramObj = boundSql.getParameterObject();
if (paramObj instanceof Page>) {
Page> page = (Page>) paramObj;
// 4. 獲取 MappedStatement(包含原始 SQL)
MappedStatement ms = (MappedStatement) ReflectHelper.getFieldValue(delegate, "mappedStatement");
Connection conn = (Connection) invocation.getArgs()[0];
String originalSql = boundSql.getSql();
// 5. 統計總記錄數(關鍵優化點)
setTotalRecord(page, ms, conn);
// 6. 生成分頁 SQL
String pageSql = getPageSql(page, originalSql);
// 7. 反射修改 BoundSql 中的 SQL
ReflectHelper.setFieldValue(boundSql, "sql", pageSql);
}
return invocation.proceed();
}
}
private void setTotalRecord(Page> page, MappedStatement ms, Connection conn) {
BoundSql boundSql = ms.getBoundSql(page);
String originalSql = boundSql.getSql();
// 智能生成 COUNT SQL:從 FROM 關鍵字截取
String countSql = "SELECT COUNT(*) " + originalSql.substring(originalSql.toLowerCase().indexOf("from"));
// 復用參數映射關係
List mappings = boundSql.getParameterMappings();
BoundSql countBoundSql = new BoundSql(ms.getConfiguration(), countSql, mappings, page);
// 使用 MyBatis 內建參數處理器
ParameterHandler ph = new DefaultParameterHandler(ms, page, countBoundSql);
try (PreparedStatement ps = conn.prepareStatement(countSql)) {
ph.setParameters(ps);
try (ResultSet rs = ps.executeQuery()) {
if (rs.next()) page.setTotalRecord(rs.getInt(1));
}
}
}
private String getPageSql(Page> page, String sql) {
StringBuffer sb = new StringBuffer(sql);
if ("mysql".equalsIgnoreCase(dialect)) {
// MySQL: LIMIT offset, size
int offset = (page.getPage() - 1) * page.getRows();
return sb.append(" LIMIT ").append(offset).append(",").append(page.getRows()).toString();
}
else if ("oracle".equalsIgnoreCase(dialect)) {
// Oracle: 雙層 ROWNUM 嵌套
int offset = (page.getPage() - 1) * page.getRows() + 1;
int end = offset + page.getRows() - 1;
sb.insert(0, "SELECT u.*, ROWNUM r FROM (").append(") u WHERE ROWNUM <= ").append(end);
sb.insert(0, "SELECT * FROM (").append(") WHERE r >= ").append(offset);
return sb.toString();
}
return sb.toString();
}
<!-- Mapper 掃描器 -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.yidao.mybatis.dao"/>
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
</bean>
<!-- 分頁攔截器 -->
<bean id="paginationInterceptor" class="com.mybatis.interceptor.PageInterceptor">
<property name="dialect" value="mysql"/>
<!-- 正則匹配需分頁的 Statement ID -->
<property name="pageSqlId" value=".*query$"/>
</bean>
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="plugins">
<array>
<ref bean="paginationInterceptor"/>
</array>
</property>
</bean>
public interface UserMapper {
// 方法名匹配 pageSqlId 正則
List queryUserList(Page page, @Param("name") String name);
}
<select id="queryUserList" resultType="User" parameterType="Page">
SELECT * FROM t_user
<where>
<if test="name != null">AND name LIKE CONCAT('%', #{name}, '%')</if>
</where>
</select>
public Page getUsers(int pageNo, int pageSize, String name) {
Page page = new Page<>();
page.setPage(pageNo);
page.setRows(pageSize);
// MyBatis 自動填充 totalRecord 並改寫 SQL
List list = userMapper.queryUserList(page, name);
page.setResults(list);
return page;
}
MyBatis 分頁攔截器通過「反射突破封裝 + SQL 動態改寫 + 參數映射復用」三大技術,實現了零業務代碼侵入的通用分頁方案。掌握其核心原理,可根據業務場景進行深度定製,滿足高性能、多數據庫、複雜查詢等進階需求。