Mybatis外掛原理和PageHelper結合實戰分頁外掛
今天和大家分享下mybatis的一個分頁外掛PageHelper,在講解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初始化
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());