mybatis插件機制及分頁插件原理

MyBatis 插件原理與自定義插件:

  MyBatis 經過提供插件機制,讓咱們能夠根據本身的須要去加強MyBatis 的功能。須要注意的是,若是沒有徹底理解MyBatis 的運行原理和插件的工做方式,最好不要使用插件,由於它會改變系底層的工做邏輯,給系統帶來很大的影響。html

  MyBatis 的插件能夠在不修改原來的代碼的狀況下,經過攔截的方式,改變四大核心對象的行爲,好比處理參數,處理SQL,處理結果。git

第一個問題:github

  不修改對象的代碼,怎麼對對象的行爲進行修改,好比說在原來的方法前面作一點事情,在原來的方法後面作一點事情?spring

  答案:你們很容易能想到用代理模式,這個也確實是MyBatis 插件的原理。sql

第二個問題:apache

  咱們能夠定義不少的插件,那麼這種全部的插件會造成一個鏈路,好比咱們提交一個休假申請,先是項目經理審批,而後是部門經理審批,再是HR 審批,再到總經理審批,怎麼實現層層的攔截?設計模式

  答案:插件是層層攔截的,咱們又須要用到另外一種設計模式——責任鏈模式。session

  在以前的源碼中咱們也發現了,mybatis內部對於插件的處理確實使用的代理模式,既然是代理模式,咱們應該瞭解MyBatis 容許哪些對象的哪些方法容許被攔截,並非每個運行的節點都是能夠被修改的。只有清楚了這些對象的方法的做用,當咱們本身編寫插件的時候才知道從哪裏去攔截。在MyBatis 官網有答案,咱們來看一下:http://www.mybatis.org/mybatis-3/zh/configuration.html#pluginsmybatis

  Executor 會攔截到CachingExcecutor 或者BaseExecutor。由於建立Executor 時是先建立CachingExcecutor,再包裝攔截。從代碼順序上能看到。咱們能夠經過mybatis的分頁插件來看看整個插件從包裝攔截器鏈到執行攔截器鏈的過程。app

  在查看插件原理的前提上,咱們須要來看看官網對於自定義插件是怎麼來作的,官網上有介紹:經過 MyBatis 提供的強大機制,使用插件是很是簡單的,只需實現 Interceptor 接口,並指定想要攔截的方法簽名便可。這裏本人踩了一個坑,在Springboot中集成,同時引入了pagehelper-spring-boot-starter 致使RowBounds參數的值被刷掉了,也就是走到了個人攔截其中沒有被設置值,這裏須要注意,攔截器出了問題,能夠Debug看一下Configuration配置類中攔截器鏈的包裝狀況。

@Intercepts({//須要攔截的方法
   @Signature(type = Executor.class,method = "query",
        args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
), @Signature(type = Executor.class,method = "query",
        args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}
)})
public class MyPageInterceptor implements Interceptor {


    // 用於覆蓋被攔截對象的原有方法(在調用代理對象Plugin 的invoke()方法時被調用)
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        System.out.println("將邏輯分頁改成物理分頁");
        Object[] args = invocation.getArgs();
        MappedStatement ms = (MappedStatement) args[0]; // MappedStatement
        BoundSql boundSql = ms.getBoundSql(args[1]); // Object parameter
        RowBounds rb = (RowBounds) args[2]; // RowBounds
        // RowBounds爲空,無需分頁
        if (rb == RowBounds.DEFAULT) {
            return invocation.proceed();
        }// 在SQL後加上limit語句
        String sql = boundSql.getSql();
        String limit = String.format("LIMIT %d,%d", rb.getOffset(), rb.getLimit());
        sql = sql + " " + limit;

        // 自定義sqlSource
        SqlSource sqlSource = new StaticSqlSource(ms.getConfiguration(), sql, boundSql.getParameterMappings());

        // 修改原來的sqlSource
        Field field = MappedStatement.class.getDeclaredField("sqlSource");
        field.setAccessible(true);
        field.set(ms, sqlSource);

        // 執行被攔截方法
        return invocation.proceed();
    }

    // target 是被攔截對象,這個方法的做用是給被攔截對象生成一個代理對象,並返回它
    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }


    // 設置參數
    @Override
    public void setProperties(Properties properties) {
    }
}

  插件註冊,在mybatis-config.xml 中註冊插件:

<plugins>
  <plugin interceptor="com.github.pagehelper.PageInterceptor">
    <property name="offsetAsPageNum" value="true"/>
      ……後面所有省略……
  </plugin>
</plugins>

  攔截簽名跟參數的順序有嚴格要求,若是按照順序找不到對應方法會拋出異常:

    org.apache.ibatis.exceptions.PersistenceException:
            ### Error opening session.  Cause: org.apache.ibatis.plugin.PluginException: Could not find method on interface org.apache.ibatis.executor.Executor named query

  MyBatis 啓動時掃描<plugins> 標籤, 註冊到Configuration 對象的 InterceptorChain 中。property 裏面的參數,會調用setProperties()方法處理。

代理和攔截是怎麼實現的?

  上面提到的能夠被代理的四大對象都是何時被代理的呢?Executor 是openSession() 的時候建立的; StatementHandler 是SimpleExecutor.doQuery()建立的;裏面包含了處理參數的ParameterHandler 和處理結果集的ResultSetHandler 的建立,建立以後即調用InterceptorChain.pluginAll(),返回層層代理後的對象。代理是由Plugin 類建立。在咱們重寫的 plugin() 方法裏面能夠直接調用returnPlugin.wrap(target, this);返回代理對象。

  當個插件的狀況下,代理能不能被代理?代理順序和調用順序的關係? 能夠被代理。

  由於代理類是Plugin,因此最後調用的是Plugin 的invoke()方法。它先調用了定義的攔截器的intercept()方法。能夠經過invocation.proceed()調用到被代理對象被攔截的方法。

  調用流程時序圖:

PageHelper 原理:

  先來看一下分頁插件的簡單用法:

PageHelper.startPage(1, 3);
List<Blog> blogs = blogMapper.selectBlogById2(blog);
PageInfo page = new PageInfo(blogs, 3);

  對於插件機制咱們上面已經介紹過了,在這裏咱們天然的會想到其所涉及的核心類 :PageInterceptor。攔截的是Executor 的兩個query()方法,要實現分頁插件的功能,確定是要對咱們寫的sql進行改寫,那麼必定是在 intercept 方法中進行操做的,咱們會發現這麼一行代碼:

 String pageSql = this.dialect.getPageSql(ms, boundSql, parameter, rowBounds, cacheKey);

  調用到 AbstractHelperDialect 中的  getPageSql 方法:

public String getPageSql(MappedStatement ms, BoundSql boundSql, Object parameterObject, RowBounds rowBounds, CacheKey pageKey) {
// 獲取sql String sql
= boundSql.getSql();
//獲取分頁參數對象 Page page
= this.getLocalPage(); return this.getPageSql(sql, page, pageKey); }

  這裏能夠看到會去調用 this.getLocalPage(),咱們來看看這個方法:

public <T> Page<T> getLocalPage() {
  return PageHelper.getLocalPage();
}
//線程獨享
protected static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal();
public static <T> Page<T> getLocalPage() {
  return (Page)LOCAL_PAGE.get();
}

  能夠發現這裏是調用的是PageHelper的一個本地線程變量中的一個 Page對象,從其中獲取咱們所設置的  PageSize 與 PageNum,那麼他是怎麼設置值的呢?請看:

PageHelper.startPage(1, 3);

public static <E> Page<E> startPage(int pageNum, int pageSize) {
        return startPage(pageNum, pageSize, true);
}

public static <E> Page<E> startPage(int pageNum, int pageSize, boolean count, Boolean reasonable, Boolean pageSizeZero) {
        Page<E> page = new Page(pageNum, pageSize, count);
        page.setReasonable(reasonable);
        page.setPageSizeZero(pageSizeZero);
        Page<E> oldPage = getLocalPage();
        if (oldPage != null && oldPage.isOrderByOnly()) {
            page.setOrderBy(oldPage.getOrderBy());
     }
        //設置頁數,行數信息
        setLocalPage(page);
        return page;
}

protected static void setLocalPage(Page page) {
//設置值 LOCAL_PAGE.
set(page); }

  在咱們調用 PageHelper.startPage(1, 3); 的時候,系統會調用 LOCAL_PAGE.set(page) 進行設置,從而在分頁插件中能夠獲取到這個本地變量對象中的參數進行 SQL 的改寫,因爲改寫有不少實現,咱們這裏用的Mysql的實現:

  在這裏咱們會發現分頁插件改寫SQL的核心代碼,這個代碼就很清晰了,沒必要過多贅述:

public String getPageSql(String sql, Page page, CacheKey pageKey) {
        StringBuilder sqlBuilder = new StringBuilder(sql.length() + 14);
        sqlBuilder.append(sql);
        if (page.getStartRow() == 0) {
            sqlBuilder.append(" LIMIT ");
            sqlBuilder.append(page.getPageSize());
        } else {
            sqlBuilder.append(" LIMIT ");
            sqlBuilder.append(page.getStartRow());
            sqlBuilder.append(",");
            sqlBuilder.append(page.getPageSize());
            pageKey.update(page.getStartRow());
        }

        pageKey.update(page.getPageSize());
        return sqlBuilder.toString();
}

  PageHelper 就是這麼一步一步的改寫了咱們的SQL 從而達到一個分頁的效果。

   關鍵類總結:

相關文章
相關標籤/搜索