分析Mybatis的分頁外掛PageHelper的原始碼
阿新 • • 發佈:2018-12-20
本次我們分析PageHelper的原始碼,檢視它的執行過程;
1、PageHelper的版本
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>3.7.5</version>
</dependency>
2、首先我們先看在程式碼中怎麼使用的
PageHelper.startPage(1,2); List<User> users = sqlSession.selectList("UserMapper.queryAllUser"); users.forEach(u -> System.out.println(u));
3、分析PageHelper.startPage(1,2),這個是在設定當前頁和每頁查詢的資料量
PageHelper裡面有一個靜態方法: public static Page startPage(int pageNum, int pageSize) { return startPage(pageNum, pageSize, true); } 最後呼叫的方法: public static Page startPage(int pageNum, int pageSize, boolean count, Boolean reasonable, Boolean pageSizeZero) { Page page = new Page(pageNum, pageSize, count); page.setReasonable(reasonable); page.setPageSizeZero(pageSizeZero); SqlUtil.setLocalPage(page); return page; } 其實就是建立了一個Page物件,設定一些引數,然後把Page物件放入了ThreadLocal物件裡面。 建立物件的時候還計算了開始和結束的行 private Page(int pageNum, int pageSize, int total, Boolean reasonable) { super(0); if (pageNum == 1 && pageSize == Integer.MAX_VALUE) { pageSizeZero = true; pageSize = 0; } this.pageNum = pageNum; this.pageSize = pageSize; this.total = total; calculateStartAndEndRow();// 計算起止行號 setReasonable(reasonable); } private void calculateStartAndEndRow() { this.startRow = this.pageNum > 0 ? (this.pageNum - 1) * this.pageSize : 0; this.endRow = this.startRow + this.pageSize * (this.pageNum > 0 ? 1 : 0); } // 將建立的page物件放入了ThreadLocal物件中 SqlUtil.setLocalPage(page); public static void setLocalPage(Page page) { LOCAL_PAGE.set(page); } private static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal<Page>();
4、PageHelper實現Interceptor介面,在介面上面有一個註解:
@Intercepts(@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}));意思也就是說攔截了Executor的query方法,方法的引數是:MappedStatement,Object,RowBounds,ResultHandler。
實現Interceptor介面會重寫3個方法:
/** * Mybatis攔截器方法 * * @param invocation 攔截器入參 * @return 返回執行結果 * @throws Throwable 丟擲異常 */ public Object intercept(Invocation invocation) throws Throwable { return sqlUtil.processPage(invocation); } /** * 只攔截Executor * * @param target * @return */ public Object plugin(Object target) { if (target instanceof Executor) { return Plugin.wrap(target, this); } else { return target; } } /** * 設定屬性值 * * @param p 屬性值 */ public void setProperties(Properties p) { //MyBatis3.2.0版本校驗 try { Class.forName("org.apache.ibatis.scripting.xmltags.SqlNode");//SqlNode是3.2.0之後新增的類 } catch (ClassNotFoundException e) { throw new RuntimeException("您使用的MyBatis版本太低,MyBatis分頁外掛PageHelper支援MyBatis3.2.0及以上版本!"); } //資料庫方言 String dialect = p.getProperty("dialect"); sqlUtil = new SqlUtil(dialect); sqlUtil.setProperties(p); }
5、我們的分頁邏輯在intercept(Invocation invocation)方法裡面,所以主要來分析這個方法
public Object intercept(Invocation invocation) throws Throwable {
return sqlUtil.processPage(invocation);
}
// 呼叫的是這個方法
public Object processPage(Invocation invocation) throws Throwable {
try {
Object result = _processPage(invocation);
return result;// 這裡返回的其實是一個Page物件
} finally {
clearLocalPage();
}
}
// 接下來
private Object _processPage(Invocation invocation) throws Throwable {
final Object[] args = invocation.getArgs();
RowBounds rowBounds = (RowBounds) args[2];
if (SqlUtil.getLocalPage() == null && rowBounds == RowBounds.DEFAULT) {
return invocation.proceed();
} else {
//忽略RowBounds-否則會進行Mybatis自帶的記憶體分頁
args[2] = RowBounds.DEFAULT;
//分頁資訊
Page page = getPage(rowBounds);
//pageSizeZero的判斷
if ((page.getPageSizeZero() != null && page.getPageSizeZero()) && page.getPageSize() == 0) {
//執行正常(不分頁)查詢
Object result = invocation.proceed();
//得到處理結果
page.addAll((List) result);
//相當於查詢第一頁
page.setPageNum(1);
//這種情況相當於pageSize=total
page.setPageSize(page.size());
//仍然要設定total
page.setTotal(page.size());
//返回結果仍然為Page型別 - 便於後面對接收型別的統一處理
return page;
}
//獲取原始的ms
MappedStatement ms = (MappedStatement) args[0];
SqlSource sqlSource = ms.getSqlSource();
//簡單的通過total的值來判斷是否進行count查詢
if (page.isCount()) {
//將引數中的MappedStatement替換為新的qs
msUtils.processCountMappedStatement(ms, sqlSource, args);
//查詢總數
Object result = invocation.proceed();
//設定總數
page.setTotal((Integer) ((List) result).get(0));
if (page.getTotal() == 0) {
return page;
}
}
//pageSize>0的時候執行分頁查詢,pageSize<=0的時候不執行相當於可能只返回了一個count
if (page.getPageSize() > 0 &&
((rowBounds == RowBounds.DEFAULT && page.getPageNum() > 0)
|| rowBounds != RowBounds.DEFAULT)) {
//將引數中的MappedStatement替換為新的qs
msUtils.processPageMappedStatement(ms, sqlSource, page, args);
//執行分頁查詢
Object result = invocation.proceed();
//得到處理結果
page.addAll((List) result);
}
//返回結果
return page;
}
}
6、分頁其實分成兩步:第一步,查詢一共有多少條資料(Count);第二步,查詢當前頁的資料(LIMIT ?,?);我們先分析統計總數的程式碼
# 分析count的原始碼
主要的程式碼如下:
MappedStatement ms = (MappedStatement) args[0];
SqlSource sqlSource = ms.getSqlSource();
//簡單的通過total的值來判斷是否進行count查詢
if (page.isCount()) {
//將引數中的MappedStatement替換為新的qs
msUtils.processCountMappedStatement(ms, sqlSource, args);
//查詢總數
Object result = invocation.proceed();
//設定總數
page.setTotal((Integer) ((List) result).get(0));
if (page.getTotal() == 0) {
return page;
}
}
# 關鍵程式碼:msUtils.processCountMappedStatement(ms, sqlSource, args);//將引數中的MappedStatement替換為新的qs
public void processCountMappedStatement(MappedStatement ms, SqlSource sqlSource, Object[] args) {
args[0] = getMappedStatement(ms, sqlSource, args[1], SUFFIX_COUNT);
}
public MappedStatement getMappedStatement(MappedStatement ms, SqlSource sqlSource, Object parameterObject, String suffix) {
MappedStatement qs = null;
if (ms.getId().endsWith(SUFFIX_PAGE) || ms.getId().endsWith(SUFFIX_COUNT)) {
throw new RuntimeException("分頁外掛配置錯誤:請不要在系統中配置多個分頁外掛(使用Spring時,mybatis-config.xml和Spring<bean>配置方式,請選擇其中一種,不要同時配置多個分頁外掛)!");
}
if (parser.isSupportedMappedStatementCache()) {
try {
qs = ms.getConfiguration().getMappedStatement(ms.getId() + suffix);
} catch (Exception e) {
//ignore
}
}
if (qs == null) {
//建立一個新的MappedStatement
qs = newMappedStatement(ms, getsqlSource(ms, sqlSource, parameterObject, suffix == SUFFIX_COUNT), suffix);
if (parser.isSupportedMappedStatementCache()) {
try {
ms.getConfiguration().addMappedStatement(qs);
} catch (Exception e) {
//ignore
}
}
}
return qs;
}
# 關鍵程式碼:qs = newMappedStatement(ms, getsqlSource(ms, sqlSource, parameterObject, suffix == SUFFIX_COUNT), suffix);中的getsqlSource(ms, sqlSource, parameterObject, suffix == SUFFIX_COUNT)
public SqlSource getsqlSource(MappedStatement ms, SqlSource sqlSource, Object parameterObject, boolean count) {
if (sqlSource instanceof DynamicSqlSource) {//動態sql
MetaObject msObject = SystemMetaObject.forObject(ms);
SqlNode sqlNode = (SqlNode) msObject.getValue("sqlSource.rootSqlNode");
MixedSqlNode mixedSqlNode;
if (sqlNode instanceof MixedSqlNode) {
mixedSqlNode = (MixedSqlNode) sqlNode;
} else {
List<SqlNode> contents = new ArrayList<SqlNode>(1);
contents.add(sqlNode);
mixedSqlNode = new MixedSqlNode(contents);
}
return new PageDynamicSqlSource(this, ms.getConfiguration(), mixedSqlNode, count);
} else if (sqlSource instanceof ProviderSqlSource) {//註解式sql
return new PageProviderSqlSource(parser, ms.getConfiguration(), (ProviderSqlSource) sqlSource, count);
} else if (count) {//RawSqlSource和StaticSqlSource
return getStaticCountSqlSource(ms.getConfiguration(), sqlSource, parameterObject);
} else {
return getStaticPageSqlSource(ms.getConfiguration(), sqlSource, parameterObject);
}
}
# 關鍵程式碼
else if (count) {//RawSqlSource和StaticSqlSource
return getStaticCountSqlSource(ms.getConfiguration(), sqlSource, parameterObject);
}
public SqlSource getStaticCountSqlSource(Configuration configuration, SqlSource sqlSource, Object parameterObject) {
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
return new StaticSqlSource(configuration, parser.getCountSql(boundSql.getSql()), boundSql.getParameterMappings());
}
# 關鍵程式碼:parser.getCountSql(boundSql.getSql())
public String getCountSql(final String sql) {
return sqlParser.getSmartCountSql(sql);
}
# 接下來這個方法就是將sql處理為count
public String getSmartCountSql(String sql) {
//校驗是否支援該sql
isSupportedSql(sql);
if (CACHE.get(sql) != null) {
return CACHE.get(sql);
}
//解析SQL
Statement stmt = null;
try {
stmt = CCJSqlParserUtil.parse(sql);
} catch (Throwable e) {
//無法解析的用一般方法返回count語句
String countSql = getSimpleCountSql(sql);
CACHE.put(sql, countSql);
return countSql;
}
Select select = (Select) stmt;
SelectBody selectBody = select.getSelectBody();
//處理body-去order by
processSelectBody(selectBody);
//處理with-去order by
processWithItemsList(select.getWithItemsList());
//處理為count查詢
sqlToCount(select);
String result = select.toString();
CACHE.put(sql, result);
return result;
}
# 關鍵程式碼:sqlToCount(select);
public void sqlToCount(Select select) {
SelectBody selectBody = select.getSelectBody();
// 是否能簡化count查詢
if (selectBody instanceof PlainSelect && isSimpleCount((PlainSelect) selectBody)) {
((PlainSelect) selectBody).setSelectItems(COUNT_ITEM);
} else {
PlainSelect plainSelect = new PlainSelect();
SubSelect subSelect = new SubSelect();
subSelect.setSelectBody(selectBody);
subSelect.setAlias(TABLE_ALIAS);
plainSelect.setFromItem(subSelect);
plainSelect.setSelectItems(COUNT_ITEM);
select.setSelectBody(plainSelect);
}
}
# 最終將原來的sql改成countsql之後返回
Object result = invocation.proceed();# 執行sql完成查詢
page.setTotal((Integer) ((List) result).get(0));// 設定總數
if (page.getTotal() == 0) {
return page;// 如果count的結果為0,就不進行limit查詢了,直接返回page物件。
}
# 所以count這一步分析完畢了。
7、分析limit查詢的這一步驟
//將引數中的MappedStatement替換為新的qs
msUtils.processPageMappedStatement(ms, sqlSource, page, args);
Object result = invocation.proceed();
//得到處理結果
page.addAll((List) result);
# 關鍵程式碼:msUtils.processPageMappedStatement(ms, sqlSource, page, args);
public void processPageMappedStatement(MappedStatement ms, SqlSource sqlSource, Page page, Object[] args) {
args[0] = getMappedStatement(ms, sqlSource, args[1], SUFFIX_PAGE);
//處理入參
args[1] = setPageParameter((MappedStatement) args[0], args[1], page);
}
# 先看處理sql,其實就是加上limit ?,?(args[0] = getMappedStatement(ms, sqlSource, args[1], SUFFIX_PAGE);)
public MappedStatement getMappedStatement(MappedStatement ms, SqlSource sqlSource, Object parameterObject, String suffix) {
MappedStatement qs = null;
if (ms.getId().endsWith(SUFFIX_PAGE) || ms.getId().endsWith(SUFFIX_COUNT)) {
throw new RuntimeException("分頁外掛配置錯誤:請不要在系統中配置多個分頁外掛(使用Spring時,mybatis-config.xml和Spring<bean>配置方式,請選擇其中一種,不要同時配置多個分頁外掛)!");
}
if (parser.isSupportedMappedStatementCache()) {
try {
qs = ms.getConfiguration().getMappedStatement(ms.getId() + suffix);
} catch (Exception e) {
//ignore
}
}
if (qs == null) {
//建立一個新的MappedStatement
qs = newMappedStatement(ms, getsqlSource(ms, sqlSource, parameterObject, suffix == SUFFIX_COUNT), suffix);
if (parser.isSupportedMappedStatementCache()) {
try {
ms.getConfiguration().addMappedStatement(qs);
} catch (Exception e) {
//ignore
}
}
}
return qs;
}
# 關鍵程式碼:getsqlSource(ms, sqlSource, parameterObject, suffix == SUFFIX_COUNT)
public SqlSource getsqlSource(MappedStatement ms, SqlSource sqlSource, Object parameterObject, boolean count) {
if (sqlSource instanceof DynamicSqlSource) {//動態sql
MetaObject msObject = SystemMetaObject.forObject(ms);
SqlNode sqlNode = (SqlNode) msObject.getValue("sqlSource.rootSqlNode");
MixedSqlNode mixedSqlNode;
if (sqlNode instanceof MixedSqlNode) {
mixedSqlNode = (MixedSqlNode) sqlNode;
} else {
List<SqlNode> contents = new ArrayList<SqlNode>(1);
contents.add(sqlNode);
mixedSqlNode = new MixedSqlNode(contents);
}
return new PageDynamicSqlSource(this, ms.getConfiguration(), mixedSqlNode, count);
} else if (sqlSource instanceof ProviderSqlSource) {//註解式sql
return new PageProviderSqlSource(parser, ms.getConfiguration(), (ProviderSqlSource) sqlSource, count);
} else if (count) {//RawSqlSource和StaticSqlSource
return getStaticCountSqlSource(ms.getConfiguration(), sqlSource, parameterObject);
} else {
return getStaticPageSqlSource(ms.getConfiguration(), sqlSource, parameterObject);// 這次是走這裡
}
}
# 關鍵程式碼:return getStaticPageSqlSource(ms.getConfiguration(), sqlSource, parameterObject);
public SqlSource getStaticPageSqlSource(Configuration configuration, SqlSource sqlSource, Object parameterObject) {
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
return new StaticSqlSource(configuration, parser.getPageSql(boundSql.getSql()), parser.getPageParameterMapping(configuration, boundSql));
}
# 關鍵程式碼:parser.getPageSql(boundSql.getSql())
public String getPageSql(String sql) {
StringBuilder sqlBuilder = new StringBuilder(sql.length() + 14);
sqlBuilder.append(sql);
sqlBuilder.append(" limit ?,?");
return sqlBuilder.toString();
}
# 就是在後面加上了" limit ?,?"。最終返回了
# 再看怎麼處理引數
args[1] = setPageParameter((MappedStatement) args[0], args[1], page);
public Map setPageParameter(MappedStatement ms, Object parameterObject, Page page) {
BoundSql boundSql = ms.getBoundSql(parameterObject);
return parser.setPageParameter(ms, parameterObject, boundSql, page);
}
public Map setPageParameter(MappedStatement ms, Object parameterObject, BoundSql boundSql, Page page) {
Map paramMap = super.setPageParameter(ms, parameterObject, boundSql, page);
paramMap.put(PAGEPARAMETER_FIRST, page.getStartRow());// 設定第一個引數
paramMap.put(PAGEPARAMETER_SECOND, page.getPageSize());// 設定第二個引數
return paramMap;
}
# 其實就是設定limit後面的引數,兩個引數的分別為:First_PageHelper(值為page物件的startRow),Second_PageHelper(值為page物件的pageSize)
# 返回替換的sql,和新增引數,直接分頁查詢
Object result = invocation.proceed();
# 將結果新增到page中,page繼承了ArrayList
page.addAll((List) result);
最終返回了page物件
8、至此,原始碼分析完畢。注意Page繼承了ArrayList