今天和大家分享下mybatis的一個(gè)分頁插件PageHelper,在講解PageHelper之前我們需要先了解下mybatis的插件原理寸宵。PageHelper
的官方網(wǎng)站:https://github.com/pagehelper/Mybatis-PageHelper
一患久、Plugin接口
mybatis定義了一個(gè)插件接口org.apache.ibatis.plugin.Interceptor,任何自定義插件都需要實(shí)現(xiàn)這個(gè)接口PageHelper就實(shí)現(xiàn)了改接口
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 攔截器示启,它將直接覆蓋掉你真實(shí)攔截對象的方法兢哭。
2:plugin方法它是一個(gè)生成動(dòng)態(tài)代理對象的方法
3:setProperties它是允許你在使用插件的時(shí)候設(shè)置參數(shù)值。
看下com.github.pagehelper.PageHelper分頁的實(shí)現(xiàn)了那些
/**
* Mybatis攔截器方法
*
* @param invocation 攔截器入?yún)? * @return 返回執(zhí)行結(jié)果
* @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);
}
}
這個(gè)方法獲取了是分頁核心代碼夫嗓,重新構(gòu)建了BoundSql對象下面會(huì)詳細(xì)分析
/**
* 只攔截Executor
*
* @param target
* @return
*/
public Object plugin(Object target) {
if (target instanceof Executor) {
return Plugin.wrap(target, this);
} else {
return target;
}
}
這個(gè)方法是正對Executor進(jìn)行攔截
/**
* 設(shè)置屬性值
*
* @param p 屬性值
*/
public void setProperties(Properties p) {
checkVersion();
//多數(shù)據(jù)源時(shí)迟螺,獲取jdbcurl后是否關(guān)閉數(shù)據(jù)源
String closeConn = p.getProperty("closeConn");
//解決#97
if(StringUtil.isNotEmpty(closeConn)){
this.closeConn = Boolean.parseBoolean(closeConn);
}
//初始化SqlUtil的PARAMS
SqlUtil.setParams(p.getProperty("params"));
//數(shù)據(jù)庫方言
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);
}
}
基本的屬性設(shè)置
二、Plugin初始化
初始化和所有mybatis的初始化一樣的在之前的文章里面已經(jīng)分析了 《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);
}
}
}
這里是講多個(gè)實(shí)例化的插件對象放入configuration,addInterceptor最終存放到一個(gè)list里面的舍咖,以為這可以同時(shí)存放多個(gè)Plugin
三矩父、Plugin攔截
插件可以攔截mybatis的4大對象ParameterHandler、ResultSetHandler谎仲、StatementHandler浙垫、Executor,源碼如下圖
在Configuration類里面可以找到
PageHelper使用了Executor進(jìn)行攔截郑诺,上面的的源碼里面已經(jīng)可以看到了夹姥。
我看下上圖newExecutor方法
executor = (Executor) interceptorChain.pluginAll(executor);
這個(gè)是生產(chǎn)一個(gè)代理對象,生產(chǎn)了代理對象就運(yùn)行帶invoke方法
四辙诞、Plugin運(yùn)行
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方法是為了生成一個(gè)動(dòng)態(tài)代理類。
invoke方法是代理綁定的方法飞涂,該方法首先判定簽名類和方法是否存在旦部,如果不存在則直接反射調(diào)度被攔截對象的方法,如果存在則調(diào)度插件的interceptor方法较店,這時(shí)候會(huì)初始化一個(gè)Invocation對象
我們在具體看下PageHelper士八,當(dāng)執(zhí)行到invoke后程序?qū)⑻D(zhuǎn)到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攔截器方法,這一步嵌套為了在出現(xiàn)異常時(shí)也可以清空Threadlocal
*
* @param invocation 攔截器入?yún)?br>
* @return 返回執(zhí)行結(jié)果
* @throws Throwable 拋出異常
*/
public Object processPage(Invocation invocation) throws Throwable {
try {
Object result = _processPage(invocation);
return result;
} finally {
clearLocalPage();
}
}
繼續(xù)跟進(jìn)
/**
* Mybatis攔截器方法
*
* @param invocation 攔截器入?yún)?br>
* @return 返回執(zhí)行結(jié)果
* @throws Throwable 拋出異常
*/
private Object _processPage(Invocation invocation) throws Throwable {
final Object[] args = invocation.getArgs();
Page page = null;
//支持方法參數(shù)時(shí)梁呈,會(huì)先嘗試獲取Page
if (supportMethodsArguments) {
page = getPage(args);
}
//分頁信息
RowBounds rowBounds = (RowBounds) args[2];
//支持方法參數(shù)時(shí)婚度,如果page == null就說明沒有分頁條件,不需要分頁查詢
if ((supportMethodsArguments && page == null)
//當(dāng)不支持分頁參數(shù)時(shí)官卡,判斷LocalPage和RowBounds判斷是否需要分頁
|| (!supportMethodsArguments && SqlUtil.getLocalPage() == null && rowBounds == RowBounds.DEFAULT)) {
return invocation.proceed();
} else {
//不支持分頁參數(shù)時(shí)蝗茁,page==null醋虏,這里需要獲取
if (!supportMethodsArguments && page == null) {
page = getPage(args);
}
return doProcessPage(invocation, page, args);
}
}
這些都只是分裝page方法,真正的核心是doProcessPage
/**
* Mybatis攔截器方法
*
* @param invocation 攔截器入?yún)?br>
* @return 返回執(zhí)行結(jié)果
* @throws Throwable 拋出異常
*/
private Page doProcessPage(Invocation invocation, Page page, Object[] args) throws Throwable {
//保存RowBounds狀態(tài)
RowBounds rowBounds = (RowBounds) args[2];
//獲取原始的ms
MappedStatement ms = (MappedStatement) args[0];
//判斷并處理為PageSqlSource
if (!isPageSqlSource(ms)) {
processMappedStatement(ms);
}
//設(shè)置當(dāng)前的parser哮翘,后面每次使用前都會(huì)set颈嚼,ThreadLocal的值不會(huì)產(chǎn)生不良影響
((PageSqlSource)ms.getSqlSource()).setParser(parser);
try {
//忽略RowBounds-否則會(huì)進(jìn)行Mybatis自帶的內(nèi)存分頁
args[2] = RowBounds.DEFAULT;
//如果只進(jìn)行排序 或 pageSizeZero的判斷
if (isQueryOnly(page)) {
return doQueryOnly(page, invocation);
}
//簡單的通過total的值來判斷是否進(jìn)行count查詢
if (page.isCount()) {
page.setCountSignal(Boolean.TRUE);
//替換MS
args[0] = msCountMap.get(ms.getId());
//查詢總數(shù)
Object result = invocation.proceed();
//還原ms
args[0] = ms;
//設(shè)置總數(shù)
page.setTotal((Integer) ((List) result).get(0));
if (page.getTotal() == 0) {
return page;
}
} else {
page.setTotal(-1l);
}
//pageSize>0的時(shí)候執(zhí)行分頁查詢,pageSize<=0的時(shí)候不執(zhí)行相當(dāng)于可能只返回了一個(gè)count
if (page.getPageSize() > 0 &&
((rowBounds == RowBounds.DEFAULT && page.getPageNum() > 0)
|| rowBounds != RowBounds.DEFAULT)) {
//將參數(shù)中的MappedStatement替換為新的qs
page.setCountSignal(null);
BoundSql boundSql = ms.getBoundSql(args[1]);
args[1] = parser.setPageParameter(ms, args[1], boundSql, page);
page.setCountSignal(Boolean.FALSE);
//執(zhí)行分頁查詢
Object result = invocation.proceed();
//得到處理結(jié)果
page.addAll((List) result);
}
} finally {
((PageSqlSource)ms.getSqlSource()).removeParser();
}
//返回結(jié)果
return page;
}
上面的有兩個(gè) Object result = invocation.proceed()執(zhí)行饭寺,第一個(gè)是執(zhí)行統(tǒng)計(jì)總條數(shù)阻课,第二個(gè)是執(zhí)行執(zhí)行分頁的查詢的數(shù)據(jù)
里面用到了代理。最終第一回返回一個(gè)總條數(shù)佩研,第二個(gè)把分頁的數(shù)據(jù)得到柑肴。
五:PageHelper使用
以上講解了Mybatis的插件原理和PageHelper相關(guān)的內(nèi)部實(shí)現(xiàn),下面具體講講PageHelper使用
1:先增加maven依賴:
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>4.1.6</version>
</dependency
2:配置configuration.xml文件加入如下配置(plugins應(yīng)該在environments的上面 )
<plugins>
<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>
相關(guān)字段說明可以查看SqlUtilConfig源碼里面都用說明
注意配置的時(shí)候順序不能亂了否則報(bào)錯(cuò)
Caused by: org.apache.ibatis.builder.BuilderException: Error creating document instance. Cause: org.xml.sax.SAXParseException; lineNumber: 57; columnNumber: 17; 元素類型為 "configuration" 的內(nèi)容必須匹配 "(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
意思是配置里面的節(jié)點(diǎn)順序是properties->settings->typeAliases->typeHandlers->objectFactory->objectWrapperFactory->plugins->environments->databaseIdProvider->mappers plugins應(yīng)該在environments之前objectWrapperFactory之后 這個(gè)順序不能亂了
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:查詢總條數(shù)
PageInfo<User> info=new PageInfo<>(userMapper.findUserAll());