本文詳細介紹如何在 SpringMVC + MyBatis 架構下,利用 MyBatis 的攔截器機制實現一個通用的分頁插件,支援 MySQL 和 Oracle 等主流資料庫方言。
首先定義分頁介面 IPage 與基礎實現 BasePage,包含頁碼、頁大小、總筆數、總頁數等核心屬性,並提供首頁、尾頁、上一頁、下一頁等判斷方法:
public abstract class BasePage implements IPage, Serializable {
public static int DEFAULT_PAGE_SIZE = 20;
private int pageSize = DEFAULT_PAGE_SIZE;
private int currentPage = 1;
private int totalCount = -1;
// 核心計算方法
public int getFirstResult() { return (this.currentPage - 1) * this.pageSize; }
public int getTotalPage() { /* 計算總頁數邏輯 */ }
public boolean isFirstPage() { return this.currentPage <= 1; }
public boolean isLastPage() { return this.currentPage >= getTotalPage(); }
// ... 其他 getter/setter
}
public class Page extends BasePage {
private List> data;
public static ThreadLocal threadLocal = new ThreadLocal<>();
// 建構子與資料存取方法
}
核心類 PagePlugin 實作 Interceptor 介面,攔截 StatementHandler.prepare 方法:
@Intercepts({ @Signature(type = StatementHandler.class, method = "prepare", args = { Connection.class }) })
public class PagePlugin implements Interceptor {
private String dialect = "";
private String pageSqlId = "";
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 獲取 RoutingStatementHandler 與 MappedStatement
// 從 ThreadLocal 取得 Page 物件
// 針對符合 pageSqlId 規則的查詢進行分頁處理
// 執行 COUNT 查詢獲取總筆數
// 根據方言生成分頁 SQL (MySQL LIMIT / Oracle ROWNUM)
// 修改 BoundSql 中的 SQL 為分頁 SQL
return invocation.proceed();
}
private String generatePageSql(String sql, Page page) {
if ("mysql".equals(dialect)) {
return sql + " LIMIT " + page.getCurrentResult() + "," + page.getPageSize();
} else if ("oracle".equals(dialect)) {
// Oracle 雙層巢狀 ROWNUM 分頁寫法
}
return sql;
}
// 參數設值、屬性配置等輔助方法
}
在 mybatis-config.xml 中註冊攔截器:
<configuration>
<plugins>
<plugin interceptor="com.framework.common.page.plugin.PagePlugin">
<property name="dialect" value="mysql" />
<property name="pageSqlId" value="ByPage" />
</plugin>
</plugins>
</configuration>
規約:需分頁的查詢語句 ID 必須以 ByPage 結尾,對應的計數查詢 ID 為將 ByPage 替換為 Count。
PageInterceptor 繼承 HandlerInterceptorAdapter,在 preHandle 中從請求參數讀取 pageNo、pageSize 建立 Page 物件存入 ThreadLocal,在 postHandle 中將分頁結果放入請求屬性並清理 ThreadLocal:
public class PageInterceptor extends HandlerInterceptorAdapter {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
Page page = new Page();
String pageSize = request.getParameter("pageSize");
String pageNo = request.getParameter("pageNo");
if (NumberUtils.isNumber(pageSize)) page.setPageSize(NumberUtils.toInt(pageSize));
if (NumberUtils.isNumber(pageNo)) page.setPageNo(NumberUtils.toInt(pageNo));
Page.threadLocal.set(page);
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) {
Page page = Page.threadLocal.get();
if (page != null) request.setAttribute("page", page);
Page.threadLocal.remove();
}
}
配置 SqlSessionFactoryBean 載入 MyBatis 設定檔、掃描 Mapper 接口,並註冊 PageInterceptor 到 RequestMappingHandlerMapping:
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="configLocation" value="classpath:mybatis-config.xml" />
<property name="mapperLocations">
<list><value>classpath:/com/framework/mapper/**/*Mapper.xml</value></list>
</property>
</bean>
<bean id="pageInterceptor" class="com.framework.common.page.interceptor.PageInterceptor"></bean>
<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping">
<property name="interceptors"><list><ref bean="pageInterceptor" /></list></property>
</bean>
在 Mapper XML 中定義以 ByPage 結尾的查詢語句,對應的 Count 查詢可選(若無則自動包裝 SELECT COUNT(1) FROM (...) T_COUNT)。Controller 方法直接接收 pageNo、pageSize 參數,查詢結果自動包含分頁資訊。