PageHelper原理
相關依賴
org.mybatis mybatis 3.2.8 com.github.pagehelper pagehelper 1.2.15 1.添加plugin 要使用PageHelper首先在mybatis的全局配置檔案中配置。如下: <?xml version="1.0" encoding="UTF-8"?>
<plugins>
<!-- com.github.pagehelper為PageHelper類所在包名 -->
<plugin interceptor="com.github.pagehelper.PageHelper">
<property name="dialect" value="mysql" />
<!-- 該參數預設為false -->
<!-- 設定為true時,會将RowBounds第一個參數offset當成pageNum頁碼使用 -->
<!-- 和startPage中的pageNum效果一樣 -->
<property name="offsetAsPageNum" value="true" />
<!-- 該參數預設為false -->
<!-- 設定為true時,使用RowBounds分頁會進行count查詢 -->
<property name="rowBoundsWithCount" value="true" />
<!-- 設定為true時,如果pageSize=0或者RowBounds.limit = 0就會查詢出全部的結果 -->
<!-- (相當于沒有執行分頁查詢,但是傳回結果仍然是Page類型) -->
<property name="pageSizeZero" value="true" />
<!-- 3.3.0版本可用 - 分頁參數合理化,預設false禁用 -->
<!-- 啟用合理化時,如果pageNum<1會查詢第一頁,如果pageNum>pages會查詢最後一頁 -->
<!-- 禁用合理化時,如果pageNum<1或pageNum>pages會傳回空資料 -->
<property name="reasonable" value="false" />
<!-- 3.5.0版本可用 - 為了支援startPage(Object params)方法 -->
<!-- 增加了一個`params`參數來配置參數映射,用于從Map或ServletRequest中取值 -->
<!-- 可以配置pageNum,pageSize,count,pageSizeZero,reasonable,不配置映射的用預設值 -->
<!-- 不了解該含義的前提下,不要随便複制該配置 -->
<property name="params" value="pageNum=start;pageSize=limit;" />
<!-- always總是傳回PageInfo類型,check檢查傳回類型是否為PageInfo,none傳回Page -->
<property name="returnPageInfo" value="check" />
</plugin>
</plugins>
2.加載過程 我們通過如下幾行代碼來示範過程
// 擷取配置檔案
InputStream inputStream = Resources.getResourceAsStream(“mybatis/mybatis-config.xml”);
// 通過加載配置檔案擷取SqlSessionFactory對象
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(inputStream);
// 擷取SqlSession對象
SqlSession session = factory.openSession();
PageHelper.startPage(1, 5);
session.selectList(“com.bobo.UserMapper.query”);
加載配置檔案我們從這行代碼開始
new SqlSessionFactoryBuilder().build(inputStream);
public SqlSessionFactory build(InputStream inputStream) {
return build(inputStream, null, null);
}
在這裡插入圖檔描述
在這裡插入圖檔描述
在這裡插入圖檔描述
private void pluginElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
// 擷取到内容:com.github.pagehelper.PageHelper
String interceptor = child.getStringAttribute(“interceptor”);
// 擷取配置的屬性資訊
Properties properties = child.getChildrenAsProperties();
// 建立的攔截器執行個體
Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
// 将屬性和攔截器綁定
interceptorInstance.setProperties(properties);
// 這個方法需要進入檢視
configuration.addInterceptor(interceptorInstance);
}
}
}
public void addInterceptor(Interceptor interceptor) {
// 将攔截器添加到了 攔截器鍊中 而攔截器鍊本質上就是一個List有序集合
interceptorChain.addInterceptor(interceptor);
}
在這裡插入圖檔描述
小結:通過SqlSessionFactory對象的擷取,我們加載了全局配置檔案及映射檔案同時還将配置的攔截器添加到了攔截器鍊中。
3.PageHelper定義的攔截資訊
我們來看下PageHelper的源代碼的頭部定義
@SuppressWarnings(“rawtypes”)
@Intercepts(
@Signature(
type = Executor.class,
method = “query”,
args = {MappedStatement.class
, Object.class
, RowBounds.class
, ResultHandler.class
}))
public class PageHelper implements Interceptor {
//sql工具類
private SqlUtil sqlUtil;
//屬性參數資訊
private Properties properties;
//配置對象方式
private SqlUtilConfig sqlUtilConfig;
//自動擷取dialect,如果沒有setProperties或setSqlUtilConfig,也可以正常進行
private boolean autoDialect = true;
//運作時自動擷取dialect
private boolean autoRuntimeDialect;
//多資料源時,擷取jdbcurl後是否關閉資料源
private boolean closeConn = true;
// 定義的是攔截 Executor對象中的
// query(MappedStatement ms,Object o,RowBounds ob ResultHandler rh)
// 這個方法
type = Executor.class,
method = “query”,
args = {MappedStatement.class
, Object.class
, RowBounds.class
, ResultHandler.class
}))
PageHelper中已經定義了該攔截器攔截的方法是什麼。
4.Executor
接下來我們需要分析下SqlSession的執行個體化過程中Executor發生了什麼。我們需要從這行代碼開始跟蹤
SqlSession session = factory.openSession();
public SqlSession openSession() {
return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
}
在這裡插入圖檔描述
在這裡插入圖檔描述
在這裡插入圖檔描述
在這裡插入圖檔描述
增強Executor
在這裡插入圖檔描述
在這裡插入圖檔描述
到此我們明白了,Executor對象其實被我們生存的代理類增強了。invoke的代碼為
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
Set methods = signatureMap.get(method.getDeclaringClass());
// 如果是定義的攔截的方法 就執行intercept方法
if (methods != null && methods.contains(method)) {
// 進入檢視 該方法增強
return interceptor.intercept(new Invocation(target, method, args));
}
// 不是需要攔截的方法 直接執行
return method.invoke(target, args);
} catch (Exception e) {
throw ExceptionUtil.unwrapThrowable(e);
}
}
public Object intercept(Invocation invocation) throws Throwable {
if (autoRuntimeDialect) {
SqlUtil sqlUtil = getSqlUtil(invocation);
return sqlUtil.processPage(invocation);
} else {
if (autoDialect) {
initSqlUtil(invocation);
}
return sqlUtil.processPage(invocation);
}
}
該方法中的内容我們後面再分析。Executor的分析我們到此,接下來看下PageHelper實作分頁的具體過程。
5.分頁過程
接下來我們通過代碼跟蹤來看下具體的分頁流程,我們需要分别從兩行代碼開始:
5.1 startPage
PageHelper.startPage(1, 5);
public static Page offsetPage(int offset, int limit, boolean count) {
Page page = new Page(new int[]{offset, limit}, count);
//當已經執行過orderBy的時候
Page oldPage = SqlUtil.getLocalPage();
if (oldPage != null && oldPage.isOrderByOnly()) {
page.setOrderBy(oldPage.getOrderBy());
}
// 這是重點!!!
SqlUtil.setLocalPage(page);
return page;
}
private static final ThreadLocal LOCAL_PAGE = new ThreadLocal();
// 将分頁資訊儲存在ThreadLocal中 線程安全!
public static void setLocalPage(Page page) {
LOCAL_PAGE.set(page);
}
5.2selectList方法
session.selectList(“com.bobo.UserMapper.query”);
public List selectList(String statement) {
return this.selectList(statement, null);
}
public List selectList(String statement, Object parameter) {
return this.selectList(statement, parameter, RowBounds.DEFAULT);
}
在這裡插入圖檔描述
我們需要回到invoke方法中繼續看
public Object intercept(Invocation invocation) throws Throwable {
if (autoRuntimeDialect) {
SqlUtil sqlUtil = getSqlUtil(invocation);
return sqlUtil.processPage(invocation);
} else {
if (autoDialect) {
initSqlUtil(invocation);
}
return sqlUtil.processPage(invocation);
}
}
進入sqlUtil.processPage(invocation);方法
private Page doProcessPage(Invocation invocation, Page page, Object[] args) throws Throwable {
//儲存RowBounds狀态
RowBounds rowBounds = (RowBounds) args[2];
//擷取原始的ms
MappedStatement ms = (MappedStatement) args[0];
//判斷并處理為PageSqlSource
if (!isPageSqlSource(ms)) {
processMappedStatement(ms);
}
//設定目前的parser,後面每次使用前都會set,ThreadLocal的值不會産生不良影響
((PageSqlSource)ms.getSqlSource()).setParser(parser);
try {
//忽略RowBounds-否則會進行Mybatis自帶的記憶體分頁
args[2] = RowBounds.DEFAULT;
//如果隻進行排序 或 pageSizeZero的判斷
if (isQueryOnly(page)) {
return doQueryOnly(page, invocation);
}
//簡單的通過total的值來判斷是否進行count查詢
if (page.isCount()) {
page.setCountSignal(Boolean.TRUE);
//替換MS
args[0] = msCountMap.get(ms.getId());
//查詢總數
Object result = invocation.proceed();
//還原ms
args[0] = ms;
//設定總數
page.setTotal((Integer) ((List) result).get(0));
if (page.getTotal() == 0) {
return page;
}
} else {
page.setTotal(-1l);
}
//pageSize>0的時候執行分頁查詢,pageSize<=0的時候不執行相當于可能隻傳回了一個count
if (page.getPageSize() > 0 &&
((rowBounds == RowBounds.DEFAULT && page.getPageNum() > 0)
|| rowBounds != RowBounds.DEFAULT)) {
//将參數中的MappedStatement替換為新的qs
page.setCountSignal(null);
// 重點是檢視該方法
BoundSql boundSql = ms.getBoundSql(args[1]);
args[1] = parser.setPageParameter(ms, args[1], boundSql, page);
page.setCountSignal(Boolean.FALSE);
//執行分頁查詢
Object result = invocation.proceed();
//得到處理結果
page.addAll((List) result);
}
} finally {
((PageSqlSource)ms.getSqlSource()).removeParser();
}
//傳回結果
return page;
}
進入 BoundSql boundSql = ms.getBoundSql(args[1])方法跟蹤到PageStaticSqlSource類中的
@Override
protected BoundSql getPageBoundSql(Object parameterObject) {
String tempSql = sql;
String orderBy = PageHelper.getOrderBy();
if (orderBy != null) {
tempSql = OrderByParser.converToOrderBySql(sql, orderBy);
}
tempSql = localParser.get().getPageSql(tempSql);
return new BoundSql(configuration, tempSql, localParser.get().getPageParameterMapping(configuration, original.getBoundSql(parameterObject)), parameterObject);
}
在這裡插入圖檔描述
在這裡插入圖檔描述
也可以看Oracle的分頁實作
在這裡插入圖檔描述
至此我們發現PageHelper分頁的實作原來是在我們執行SQL語句之前動态的将SQL語句拼接了分頁的語句,進而實作了從資料庫中分頁擷取的過程。