數據分頁功能是咱們軟件系統中必備的功能,在持久層使用mybatis的狀況下,pageHelper來實現後臺分頁則是咱們經常使用的一個選擇,因此本文專門類介紹下。java
相關依賴mysql
<dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.2.8</version> </dependency> <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper</artifactId> <version>1.2.15</version> </dependency>
要使用PageHelper首先在mybatis的全局配置文件中配置。以下:git
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> <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> </configuration>
咱們經過以下幾行代碼來演示過程github
// 獲取配置文件 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");
加載配置文件咱們從這行代碼開始sql
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對象的獲取,咱們加載了全局配置文件及映射文件同時還==將配置的攔截器添加到了攔截器鏈中==。數據庫
咱們來看下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中已經定義了該攔截器攔截的方法是什麼。session
接下來咱們須要分析下SqlSession的實例化過程當中Executor發生了什麼。咱們須要從這行代碼開始跟蹤mybatis
SqlSession session = factory.openSession();
public SqlSession openSession() { return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false); }
加強Executor
app
到此咱們明白了,Executor對象其實被咱們生存的代理類加強了。invoke的代碼爲
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { try { Set<Method> 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); } }
/** * Mybatis攔截器方法 * * @param invocation 攔截器入參 * @return 返回執行結果 * @throws Throwable 拋出異常 */ 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實現分頁的具體過程。
接下來咱們經過代碼跟蹤來看下具體的分頁流程,咱們須要分別從兩行代碼開始:
PageHelper.startPage(1, 5);
/** * 開始分頁 * * @param params */ public static <E> Page<E> startPage(Object params) { Page<E> page = SqlUtil.getPageFromObject(params); //當已經執行過orderBy的時候 Page<E> oldPage = SqlUtil.getLocalPage(); if (oldPage != null && oldPage.isOrderByOnly()) { page.setOrderBy(oldPage.getOrderBy()); } SqlUtil.setLocalPage(page); return page; }
/** * 開始分頁 * * @param pageNum 頁碼 * @param pageSize 每頁顯示數量 * @param count 是否進行count查詢 * @param reasonable 分頁合理化,null時用默認配置 */ public static <E> Page<E> startPage(int pageNum, int pageSize, boolean count, Boolean reasonable) { return startPage(pageNum, pageSize, count, reasonable, null); }
/** * 開始分頁 * * @param offset 頁碼 * @param limit 每頁顯示數量 * @param count 是否進行count查詢 */ public static <E> Page<E> offsetPage(int offset, int limit, boolean count) { Page<E> page = new Page<E>(new int[]{offset, limit}, count); //當已經執行過orderBy的時候 Page<E> oldPage = SqlUtil.getLocalPage(); if (oldPage != null && oldPage.isOrderByOnly()) { page.setOrderBy(oldPage.getOrderBy()); } // 這是重點!!! SqlUtil.setLocalPage(page); return page; }
private static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal<Page>(); // 將分頁信息保存在ThreadLocal中 線程安全! public static void setLocalPage(Page page) { LOCAL_PAGE.set(page); }
session.selectList("com.bobo.UserMapper.query");
public <E> List<E> selectList(String statement) { return this.selectList(statement, null); } public <E> List<E> selectList(String statement, Object parameter) { return this.selectList(statement, parameter, RowBounds.DEFAULT); }
咱們須要回到invoke方法中繼續看
/** * Mybatis攔截器方法 * * @param invocation 攔截器入參 * @return 返回執行結果 * @throws Throwable 拋出異常 */ 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);方法
/** * Mybatis攔截器方法 * * @param invocation 攔截器入參 * @return 返回執行結果 * @throws Throwable 拋出異常 */ private Object _processPage(Invocation invocation) throws Throwable { final Object[] args = invocation.getArgs(); Page page = null; //支持方法參數時,會先嚐試獲取Page if (supportMethodsArguments) { // 從線程本地變量中獲取Page信息,就是咱們剛剛設置的 page = getPage(args); } //分頁信息 RowBounds rowBounds = (RowBounds) args[2]; //支持方法參數時,若是page == null就說明沒有分頁條件,不須要分頁查詢 if ((supportMethodsArguments && page == null) //當不支持分頁參數時,判斷LocalPage和RowBounds判斷是否須要分頁 || (!supportMethodsArguments && SqlUtil.getLocalPage() == null && rowBounds == RowBounds.DEFAULT)) { return invocation.proceed(); } else { //不支持分頁參數時,page==null,這裏須要獲取 if (!supportMethodsArguments && page == null) { page = getPage(args); } // 進入查看 return doProcessPage(invocation, page, args); } }
/** * Mybatis攔截器方法 * * @param invocation 攔截器入參 * @return 返回執行結果 * @throws Throwable 拋出異常 */ 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語句拼接了分頁的語句,從而實現了從數據庫中分頁獲取的過程。