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


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

  MyBatis 通過提供插件機制,讓我們可以根據自己的需要去增強MyBatis 的功能。需要注意的是,如果沒有完全理解MyBatis 的運行原理和插件的工作方式,最好不要使用插件,因為它會改變系底層的工作邏輯,給系統帶來很大的影響。

  MyBatis 的插件可以在不修改原來的代碼的情況下,通過攔截的方式,改變四大核心對象的行為,比如處理參數,處理SQL,處理結果。

第一個問題:

  不修改對象的代碼,怎么對對象的行為進行修改,比如說在原來的方法前面做一點事情,在原來的方法后面做一點事情?

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

第二個問題:

  我們可以定義很多的插件,那么這種所有的插件會形成一個鏈路,比如我們提交一個休假申請,先是項目經理審批,然后是部門經理審批,再是HR 審批,再到總經理審批,怎么實現層層的攔截?

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

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

  Executor 會攔截到CachingExcecutor 或者BaseExecutor。因為創建Executor 時是先創建CachingExcecutor,再包裝攔截。從代碼順序上能看到。我們可以通過mybatis的分頁插件來看看整個插件從包裝攔截器鏈到執行攔截器鏈的過程。

  在查看插件原理的前提上,我們需要來看看官網對於自定義插件是怎么來做的,官網上有介紹:通過 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 從而達到一個分頁的效果。

   關鍵類總結:

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM