1. 程式人生 > >Mybatis外掛原理和PageHelper結合實戰分頁外掛

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());