2019-02-25 Mybatis插件原理和PageHelper結(jié)合實(shí)戰(zhàn)分頁插件

今天和大家分享下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());

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末晰骑,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子绊序,更是在濱河造成了極大的恐慌硕舆,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,183評論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件骤公,死亡現(xiàn)場離奇詭異抚官,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)阶捆,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,850評論 3 399
  • 文/潘曉璐 我一進(jìn)店門凌节,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人洒试,你說我怎么就攤上這事倍奢。” “怎么了垒棋?”我有些...
    開封第一講書人閱讀 168,766評論 0 361
  • 文/不壞的土叔 我叫張陵卒煞,是天一觀的道長。 經(jīng)常有香客問我叼架,道長畔裕,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,854評論 1 299
  • 正文 為了忘掉前任乖订,我火速辦了婚禮扮饶,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘乍构。我一直安慰自己甜无,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,871評論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著毫蚓,像睡著了一般。 火紅的嫁衣襯著肌膚如雪昔善。 梳的紋絲不亂的頭發(fā)上元潘,一...
    開封第一講書人閱讀 52,457評論 1 311
  • 那天,我揣著相機(jī)與錄音君仆,去河邊找鬼翩概。 笑死,一個(gè)胖子當(dāng)著我的面吹牛返咱,可吹牛的內(nèi)容都是我干的钥庇。 我是一名探鬼主播,決...
    沈念sama閱讀 40,999評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼咖摹,長吁一口氣:“原來是場噩夢啊……” “哼评姨!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起萤晴,我...
    開封第一講書人閱讀 39,914評論 0 277
  • 序言:老撾萬榮一對情侶失蹤吐句,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后店读,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體嗦枢,經(jīng)...
    沈念sama閱讀 46,465評論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,543評論 3 342
  • 正文 我和宋清朗相戀三年屯断,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了文虏。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,675評論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡殖演,死狀恐怖氧秘,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情剃氧,我是刑警寧澤敏储,帶...
    沈念sama閱讀 36,354評論 5 351
  • 正文 年R本政府宣布,位于F島的核電站朋鞍,受9級特大地震影響已添,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜滥酥,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,029評論 3 335
  • 文/蒙蒙 一更舞、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧坎吻,春花似錦缆蝉、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,514評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽黍瞧。三九已至,卻和暖如春原杂,著一層夾襖步出監(jiān)牢的瞬間印颤,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,616評論 1 274
  • 我被黑心中介騙來泰國打工穿肄, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留年局,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 49,091評論 3 378
  • 正文 我出身青樓咸产,卻偏偏與公主長得像矢否,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子脑溢,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,685評論 2 360

推薦閱讀更多精彩內(nèi)容

  • 我是誰屑彻?我究竟是一個(gè)什么的人衣迷?我不知道我為什么可以專一到喜歡足球十八年,我也不知道沒什么我可以專一到第一次看到雷克...
    有關(guān)于你的記憶閱讀 255評論 1 1
  • 推門聽課的尷尬 原文作者:農(nóng)村學(xué)校生活 2010-12-17 21:41 和大家分享這篇日志:此文很具諷...
    我是蘭姐閱讀 232評論 0 1
  • 勞勞碌碌錄啦來來來
    gbeckontoy閱讀 134評論 0 0
  • 2017.5.3開始探索我的天賦 感覺好幾天沒有寫覺察日記了酱酬,最近一個(gè)星期家里裝修壶谒,雖然是租的,想著三年內(nèi)不打算買...
    Ali阿厘閱讀 108評論 0 0