今天和大家分享下mybatis的一個分頁插件PageHelper,在講解PageHelper之前我們需要先了解下mybatis的插件原理。PageHelper
的官方網站:https://github.com/pagehelper/Mybatis-PageHelper
一、Plugin接口
mybatis定義了一個插件接口org.apache.ibatis.plugin.Interceptor,任何自定義插件都需要實現這個接口PageHelper就實現了改接口
package org.apache.ibatis.plugin;
import java.util.Properties;
/**
* @author Clinton Begin
*/
public interface Interceptor {
Object intercept(Invocation invocation) throws Throwable;
Object plugin(Object target);
void setProperties(Properties properties);
}
1:intercept 攔截器,它將直接覆蓋掉你真實攔截對象的方法。
2:plugin方法它是一個生成動態代理對象的方法
3:setProperties它是允許你在使用插件的時候設置參數值。
看下com.github.pagehelper.PageHelper分頁的實現了那些
/**
* 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);
}
}
這個方法獲取了是分頁核心代碼,重新構建了BoundSql對象下面會詳細分析
/**
* 只攔截Executor
*
* @param target
* @return
*/
public Object plugin(Object target) {
if (target instanceof Executor) {
return Plugin.wrap(target, this);
} else {
return target;
}
}
這個方法是正對Executor進行攔截
/**
* 設置屬性值
*
* @param p 屬性值
*/
public void setProperties(Properties p) {
checkVersion();
//多數據源時,獲取jdbcurl后是否關閉數據源
String closeConn = p.getProperty("closeConn");
//解決#97
if(StringUtil.isNotEmpty(closeConn)){
this.closeConn = Boolean.parseBoolean(closeConn);
}
//初始化SqlUtil的PARAMS
SqlUtil.setParams(p.getProperty("params"));
//數據庫方言
String dialect = p.getProperty("dialect");
String runtimeDialect = p.getProperty("autoRuntimeDialect");
if (StringUtil.isNotEmpty(runtimeDialect) && runtimeDialect.equalsIgnoreCase("TRUE")) {
this.autoRuntimeDialect = true;
this.autoDialect = false;
this.properties = p;
} else if (StringUtil.isEmpty(dialect)) {
autoDialect = true;
this.properties = p;
} else {
autoDialect = false;
sqlUtil = new SqlUtil(dialect);
sqlUtil.setProperties(p);
}
}
基本的屬性設置
二、Plugin初始化
初始化和所有mybatis的初始化一樣的在之前的文章里面已經分析了 《Mybatis源碼分析之SqlSessionFactory(一)》
private void pluginElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
String interceptor = child.getStringAttribute("interceptor");
Properties properties = child.getChildrenAsProperties();
Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
interceptorInstance.setProperties(properties);
configuration.addInterceptor(interceptorInstance);
}
}
}
這里是講多個實例化的插件對象放入configuration,addInterceptor最終存放到一個list里面的,以為這可以同時存放多個Plugin
三、Plugin攔截
插件可以攔截mybatis的4大對象ParameterHandler、ResultSetHandler、StatementHandler、Executor,源碼如下圖
在Configuration類里面可以找到

PageHelper使用了Executor進行攔截,上面的的源碼里面已經可以看到了。
我看下上圖newExecutor方法
executor = (Executor) interceptorChain.pluginAll(executor);
這個是生產一個代理對象,生產了代理對象就運行帶invoke方法
四、Plugin運行
mybatis自己帶了Plugin方法,源碼如下
public class Plugin implements InvocationHandler {
private Object target;
private Interceptor interceptor;
private Map<Class<?>, Set<Method>> signatureMap;
private Plugin(Object target, Interceptor interceptor, Map<Class<?>, Set<Method>> signatureMap) {
this.target = target;
this.interceptor = interceptor;
this.signatureMap = signatureMap;
}
public static Object wrap(Object target, Interceptor interceptor) {
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
Class<?> type = target.getClass();
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
if (interfaces.length > 0) {
return Proxy.newProxyInstance(
type.getClassLoader(),
interfaces,
new Plugin(target, interceptor, signatureMap));
}
return target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
Set<Method> methods = signatureMap.get(method.getDeclaringClass());
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);
}
}
private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {
Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);
// issue #251
if (interceptsAnnotation == null) {
throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName());
}
Signature[] sigs = interceptsAnnotation.value();
Map<Class<?>, Set<Method>> signatureMap = new HashMap<Class<?>, Set<Method>>();
for (Signature sig : sigs) {
Set<Method> methods = signatureMap.get(sig.type());
if (methods == null) {
methods = new HashSet<Method>();
signatureMap.put(sig.type(), methods);
}
try {
Method method = sig.type().getMethod(sig.method(), sig.args());
methods.add(method);
} catch (NoSuchMethodException e) {
throw new PluginException("Could not find method on " + sig.type() + " named " + sig.method() + ". Cause: " + e, e);
}
}
return signatureMap;
}
private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) {
Set<Class<?>> interfaces = new HashSet<Class<?>>();
while (type != null) {
for (Class<?> c : type.getInterfaces()) {
if (signatureMap.containsKey(c)) {
interfaces.add(c);
}
}
type = type.getSuperclass();
}
return interfaces.toArray(new Class<?>[interfaces.size()]);
}
}
wrap方法是為了生成一個動態代理類。
invoke方法是代理綁定的方法,該方法首先判定簽名類和方法是否存在,如果不存在則直接反射調度被攔截對象的方法,如果存在則調度插件的interceptor方法,這時候會初始化一個Invocation對象
我們在具體看下PageHelper,當執行到invoke后程序將跳轉到PageHelper.intercept
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方法
/**
* Mybatis攔截器方法,這一步嵌套為了在出現異常時也可以清空Threadlocal
*
* @param invocation 攔截器入參
* @return 返回執行結果
* @throws Throwable 拋出異常
*/
public Object processPage(Invocation invocation) throws Throwable {
try {
Object result = _processPage(invocation);
return result;
} finally {
clearLocalPage();
}
}
繼續跟進
/**
* 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 = 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);
}
}
這些都只是分裝page方法,真正的核心是doProcessPage
/**
* 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;
}
上面的有兩個 Object result = invocation.proceed()執行,第一個是執行統計總條數,第二個是執行執行分頁的查詢的數據
里面用到了代理。最終第一回返回一個總條數,第二個把分頁的數據得到。
五:PageHelper使用
以上講解了Mybatis的插件原理和PageHelper相關的內部實現,下面具體講講PageHelper使用
1:先增加maven依賴:
<dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper</artifactId> <version>4.1.6</version> </dependency
2:配置configuration.xml文件加入如下配置(plugins應該在environments的上面 )
<plugins> <!-- PageHelper4.1.6 --> <plugin interceptor="com.github.pagehelper.PageHelper"> <property name="dialect" value="mysql"/> <property name="offsetAsPageNum" value="false"/> <property name="rowBoundsWithCount" value="false"/> <property name="pageSizeZero" value="true"/> <property name="reasonable" value="false"/> <property name="supportMethodsArguments" value="false"/> <property name="returnPageInfo" value="none"/> </plugin> </plugins>
相關字段說明可以查看SqlUtilConfig源碼里面都用說明
注意配置的時候順序不能亂了否則報錯
Caused by: org.apache.ibatis.builder.BuilderException: Error creating document instance. Cause: org.xml.sax.SAXParseException; lineNumber: 57; columnNumber: 17; 元素類型為 "configuration" 的內容必須匹配 "(properties?,settings?,typeAliases?,typeHandlers?,objectFactory?,objectWrapperFactory?,plugins?,environments?,databaseIdProvider?,mappers?)"。 at org.apache.ibatis.parsing.XPathParser.createDocument(XPathParser.java:259) at org.apache.ibatis.parsing.XPathParser.<init>(XPathParser.java:120) at org.apache.ibatis.builder.xml.XMLConfigBuilder.<init>(XMLConfigBuilder.java:66) at org.apache.ibatis.session.SqlSessionFactoryBuilder.build(SqlSessionFactoryBuilder.java:49) ... 2 more
意思是配置里面的節點順序是properties->settings->typeAliases->typeHandlers->objectFactory->objectWrapperFactory->plugins->environments->databaseIdProvider->mappers plugins應該在environments之前objectWrapperFactory之后 這個順序不能亂了
3:具體使用
1:分頁
SqlSession sqlSession = sessionFactory.openSession(); UserMapper userMapper = sqlSession.getMapper(UserMapper.class); PageHelper.startPage(1,10,true); //第一頁 每頁顯示10條 Page<User> page=userMapper.findUserAll();
2:不分頁
PageHelper.startPage(1,-1,true);
3:查詢總條數
PageInfo<User> info=new PageInfo<>(userMapper.findUserAll());
