淺析MyBatis(三):聊一聊MyBatis的實用外掛與自定義外掛

Chiakiiii發表於2021-03-18

在前面的文章中,筆者詳細介紹了 ?MyBatis 框架的底層框架與執行流程,並且在理解執行流程的基礎上手寫了一個自己的 MyBatis 框架。看完前兩篇文章後,相信讀者對 MyBatis 的偏底層原理和執行流程已經有了自己的認知,並且對其在實際開發過程中使用步驟也已是輕車熟路。所謂實踐是檢驗真理的唯一標準,本文將為大家介紹一些 MyBatis 使用中的一些實用外掛與自定義外掛。本文涉及到的程式碼已上傳至 GitHub: ?mypagehelper-demo

話不多說,現在開始???!

1. Lombok外掛

1.1 Lombok簡介

在編寫 Java 程式時經常會用到很多實體類物件 ,其建立的一般流程就是定義成員變數,然後定義對應的 Constructor(有參/無參構造方法)、Getter and Setter 方法、toString() 方法等等。在 IDEA 中,可以通過 alt + insert 快捷鍵來快速插入這些方法,操作起來感覺還是很方便的。但是,在實際的業務開發中這些實體物件的屬性可能經常發生變化(成員變數命名變化、成員變數個數變化等等),比如在 Web 專案的開發中入參和出參的 DTO 類的屬性經常會有所變化。這樣在每次發生屬性變化時,都需要去修改成員變數對應的構造方法、 Getter and Setter 方法以及 toString() 方法等等,這些操作既繁瑣又浪費時間還沒有技術含量,降低了實際的開發效率。對於這些問題,Lombok 給出了完美的解決方案。

Lombok 是一種 Java 實用工具,它通過註解方式來幫助開發人員消除程式碼的冗長。在 IDEA 的外掛庫搜尋 Lombok 便可完成外掛的安裝,同時在專案裡引入 Lombok 依賴可以提供編譯支援。

<dependency>
  <groupId>org.projectlombok</groupId>
  <artifactId>lombok</artifactId>
  <version>1.18.8</version>
  <scope>provided</scope>
</dependency>

1.2 Lombok的使用

在 Lombok 的包中會提供很多註解,有的作用在類上,有的作用在變數上,而有的註解在方法上,下面會對實際開發中常用的註解進行介紹。

註解名稱 作用位置 作用 註解效果
@Data 為類提供 Getter and Setter、equals、canEqual、hashCode、toString 方法 @Date 效果
@Value 為類提供全參構造、equals、hashCode、toString 方法,為類的屬性提供 Getter 方法 @Value 效果
@AllArgsConstructor 為類提供一個全參構造 @AllArgsConstructor 效果
@NoArgsConstructor 為類提供一個無參構造 @NoArgsConstructor 效果
@EqualsAndHashCode 為類提供 equals、canEqual 以及hashcode 方法 @EqualsAndHashCode 效果
@toString 為類提供 toString 方法 @toString 效果
@Getter 類或者屬性 為類的所有屬性或單個屬性提供 Getter 方法 @Getter 效果
@Setter 類或者屬性 為類的所有屬性或單個屬性提供 Setter 方法 @Setter 效果
@NonNull 屬性 為屬性提供非空檢查,如果為空則丟擲空指標異常 @NonNull 效果
@RequiredArgsConstructor 使用 帶@NonNull 或 final 修飾的屬性來構造類的構造方法 @RequiredArgsConstructor 效果

在 MyBatis 的使用中靈活搭配 Lombok 的各種註解能夠很大程度上簡化程式碼,提高開發效率,關於更多 Lombok 外掛的使用可參見其官網:https://projectlombok.org/

2. PageHelper外掛

2.1 PageHelper簡介

實際開發中遇到查詢資料庫表給前端返回資訊時,常需要對查詢結果進行分頁,使用 PageHelper 外掛能夠方便快捷地實現分頁要求。 PageHelper 是開源的分頁外掛,支援任何單表或多表的分頁。在實際開發中,推薦使用 maven 新增依賴的方式引入外掛:

<dependency>
  <groupId>com.github.pagehelper</groupId>
  <artifactId>pagehelper</artifactId>
  <version>5.2.0</version>
</dependency>

2.2 PageHelper的使用

2.2.1 新增PageHelper的配置

在 MyBatis 框架的配置檔案中提供了 plugins 標籤用於配置 MyBatis 外掛,因此在使用 PageHelper 時需要把外掛的相關配置寫到 MyBatis 的配置檔案 mybatis-config.xml 中,如下所示:

<plugins>
  <!-- com.github.pagehelper為PageHelper類所在包名 -->
  <plugin interceptor="com.github.pagehelper.PageInterceptor">
    <property name="param1" value="value1"/>
  </plugin>
 </plugins>

這裡需要注意的是:在 MyBatis 配置檔案中,各個標籤的順序有嚴格的要求,務必在正確的位置新增 PageHelper 的配置。相應的標籤順序見下方:

properties, settings, typeAliases, typeHandlers, objectFactory, objectWrapperFactory, plugins, environments, databaseIdProvided, mappers

2.2.2 PageHelper的兩種使用方式

在新增了 PageHelper 的配置後,就可以在實際開發中利用該外掛來實現分頁。還是採用之前的學生表案例來編寫相應的測試方法,如下所示:

public class StudentTest {

  private InputStream in;
  private SqlSession sqlSession;

  @Before
  public void init() throws IOException {
    // 讀取MyBatis的配置檔案
    in = Resources.getResourceAsStream("mybatis-config.xml");
    // 建立SqlSessionFactory的構建者物件
    SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
    // 使用builder建立SqlSessionFactory物件
    SqlSessionFactory factory = builder.build(in);
    // 使用factory建立sqlSession物件並設定自動提交事務
    sqlSession = factory.openSession(true);
  }

  @Test
  public void getAllStudents() {
    // 定義分頁相關引數
    int pageNum = 1, pageSize = 3;
    StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);
    // 使用分頁外掛
    PageHelper.startPage(pageNum, pageSize);
    List<Student> students = studentMapper.findAll();
    // 輸出結果
    System.out.println(students);
  }

  @After
  public void close() throws IOException {
    // 關閉資源
    sqlSession.close();
    in.close();
  }
}

可以採用 PageHelper.startPage(int pageNum, int pageSize) 來快速實現分頁操作,其中 int pageNum 用於指定當前的頁碼,而 int pageSize 用於指定每一頁展示的記錄數。這裡需要注意的是:只有緊跟在 PageHelper.startPage 方法後的第一個 Mybatis 的查詢(Select)方法會被分頁。因此在呼叫 PageHelper.startPage 方法後需要緊跟 Mapper 介面中定義的查詢方法,否則分頁外掛將失效。對於上面的測試方法,在執行後得到如下結果:

https://i.iter01.com/images/342f7111becd00c3c83f7a5606d316251778519bbbf73256c4f5b85b68b2af7e.png

通過列印的日誌發現原本 SELECT * FROM student 的 SQL 語句在配置了 PageHelper 外掛後會在語句末尾加入 LIMIT 的分頁操作,同時傳入指定的 pageSize 引數。在最後的查詢結果輸出中也可以看出實際的總記錄數為 total = 4 ,而查詢結果只顯示了第一頁的三條記錄,成功實現了對查詢結果分頁的操作。

上面介紹的 PageHelper.startPage() 方法最大的侷限在於只能對緊跟在其後的 MyBatis 的查詢操作的結果進行分頁。然而,在實際的後端開發中經常需要對多表進行查詢並對結果進行聚合,然後給前端傳一個結果集合,這種時候如何實現分頁操作呢?在這種情況下,仍然可以利用 PageHelper 外掛進行手工分頁,定義用於分頁請求的 pageRequest() 方法以及相應的測試類,如下所示:

/**
 * 分頁請求
 * @param pageNum 指定頁碼
 * @param pageSize 指定每頁的記錄數
 * @param list 待分頁的集合
 * @param <T> 集合型別
 * @return 分頁後的集合
 */
private <T> List<T> pageRequest(int pageNum, int pageSize, List<T> list){
  // 根據pageNum和pageSize構建Page類
  Page<T> page = new Page<T>(pageNum, pageSize);
  // 設定page物件的總記錄數屬性
  page.setTotal(list.size());
  // 計算分頁的開始和結束索引
  int startIndex = (pageNum - 1) * pageSize;
  int endIndex = Math.min(startIndex + pageSize, list.size());
  // 從待分頁的集合獲取需要展示的內容新增到page物件
  page.addAll(list.subList(startIndex, endIndex));
  // 返回分頁後的集合
  return page.getResult();
}

@Test
public void testPage() {
  // 定義分頁相關引數
  int pageNum = 2, pageSize = 3;
  // 準備List<Student>集合
  List<Student> students = new ArrayList<Student>();
  students.add(new Student(1, "張A","男"));
  students.add(new Student(2, "張B","男"));
  students.add(new Student(3, "張C","男"));
  students.add(new Student(4, "張D","男"));
  students.add(new Student(5, "張E","男"));
  // 分頁
  List<Student> results = pageRequest(pageNum, pageSize, students);
  System.out.println(results);
}

通過構建上面的 pageRequest 方法,我們實現了一個簡單的手工分頁,通過呼叫該方法就能夠實現對已有集合的分頁,通過執行測試方法可以得到如下結果。

https://i.iter01.com/images/1c6a3d71c03dc81f363253785364955df16b2209bfdb6a7b9758a326735e29cb.png

從結果易知在面對已存在的 List 集合時,我們基於 PageHelper 外掛構建的 pageRequest 方法仍起到了分頁的作用。這裡指定的頁碼 pageNum = 2,pageSize = 3 ,待分頁的集合總記錄為五條,結果顯示了集合中的最後兩條記錄,分頁結果正確。

在本小節中介紹了 PageHelper.startPage 方法以及手工定義 pageRequest 方法的兩種基於 PageHelper 外掛的分頁方式,能夠根據不同的情況實現分頁需求。

3. MyBatis外掛的執行原理簡介

第一篇文章中已經對 MyBatis 框架的執行流程進行了講解,相信讀者都已知曉 MyBatis 是利用 XMLConfigBuilder 來對配置檔案進行解析的。而在前文提到外掛的配置是寫在 mybatis-config.xml 配置檔案中,因此去 XMLConfigBuilder 類中找解析 plugins 標籤的方法。

3.1 XMLConfigBuilder#pluginElement()方法

果不其然,可以找到解析 MyBatis 外掛的 XMLConfigBuilder#pluginElement() 方法,如下所示:

private void pluginElement(XNode parent) throws Exception {
  if (parent != null) {
    // 遍歷子節點
    for (XNode child : parent.getChildren()) {
      // 獲取interceptor標籤下攔截器的全限定類名
      String interceptor = child.getStringAttribute("interceptor");
      // 獲取子節點對應的屬性
      Properties properties = child.getChildrenAsProperties();
      // 通過上面獲取得攔截器的全限定類名構建一個攔截器例項
      Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
      // 將子節點對應的屬性設定到攔截器
      interceptorInstance.setProperties(properties);
      // 將該攔截器例項設定到Configuration物件
      configuration.addInterceptor(interceptorInstance);
    }
  }
}

3.2 Interceptor介面

找到上面方法中涉及到的 Interceptor 類對於的原始碼如下所示:

public interface Interceptor {

  Object intercept(Invocation invocation) throws Throwable;

  Object plugin(Object target);

  void setProperties(Properties properties);

}

可以看出這是一個介面,其中定義了 intercept()、plugin() 以及 setProperties() 三個方法:

  • intercept() 方法:覆蓋所攔截物件原有的方法,也是外掛的核心方法。其入參是 invocation,可以利用反射調原物件的方法;
  • plugin() 方法:入參 target 是被攔截的物件,該方法的作用是生成一個被攔截物件的代理例項;
  • setProperties() 方法:方便把 MyBatis 配置檔案中 plugin 標籤下的內容解析後設定到 Configuration 物件。

對於一個外掛來說,必須要先實現 Intercept 介面中的三個方法才能在 MyBatis 框架中進行配置並使用。

3.3 從PageHelper外掛原始碼來看外掛的實現流程

本節中會從 PageHelper 的原始碼來分析 MyBatis 外掛的實現流程,找到關鍵類 PageInterceptor 的原始碼如下:

// 壓制警告註解
@SuppressWarnings({"rawtypes", "unchecked"})
// 攔截器的註解
@Intercepts(
    {
				// 註冊攔截器簽名:指定需要被攔截的型別(type)、方法(method)和引數(args)需要被攔截
				// 只包含4個引數
        @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
				// 只包含6個引數
        @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
    }
)
public class PageInterceptor implements Interceptor {
// 快取count查詢的ms
  protected Cache<String, MappedStatement> msCountMap = null;
  private Dialect dialect;
  private String default_dialect_class = "com.github.pagehelper.PageHelper";
  private Field additionalParametersField;
  private String countSuffix = "_COUNT";

  @Override
  public Object intercept(Invocation invocation) throws Throwable {
    try {
      // 解析攔截到的引數
      Object[] args = invocation.getArgs();
      MappedStatement ms = (MappedStatement) args[0];
      Object parameter = args[1];
      RowBounds rowBounds = (RowBounds) args[2];
      ResultHandler resultHandler = (ResultHandler) args[3];
      Executor executor = (Executor) invocation.getTarget();
      CacheKey cacheKey;
      BoundSql boundSql;
      // 由於邏輯關係,只會進入一次
      if(args.length == 4){
        // 4個引數時
        boundSql = ms.getBoundSql(parameter);
        cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);
      } else {
        // 6個引數時
        cacheKey = (CacheKey) args[4];
        boundSql = (BoundSql) args[5];
      }
      List resultList;
      // 呼叫方法判斷是否需要進行分頁,如果不需要,直接返回結果
      if (!dialect.skip(ms, parameter, rowBounds)) {
        // 反射獲取動態引數
        String msId = ms.getId();
        Configuration configuration = ms.getConfiguration();
        Map<String, Object> additionalParameters = (Map<String, Object>) additionalParametersField.get(boundSql);
        // 判斷是否需要進行count查詢
        if (dialect.beforeCount(ms, parameter, rowBounds)) {
          String countMsId = msId + countSuffix;
          Long count;
          // 先判斷是否存在手寫的count查詢
          MappedStatement countMs = getExistedMappedStatement(configuration, countMsId);
          if(countMs != null){
            count = executeManualCount(executor, countMs, parameter, boundSql, resultHandler);
          } else {
            countMs = msCountMap.get(countMsId);
            // 自動建立
            if (countMs == null) {
              // 根據當前的ms建立一個返回值為Long型別的ms
              countMs = MSUtils.newCountMappedStatement(ms, countMsId);
              msCountMap.put(countMsId, countMs);
            }
            count = executeAutoCount(executor, countMs, parameter, boundSql, rowBounds, resultHandler);
          }
          // 處理查詢總數
          // 返回true時繼續分頁查詢,false時直接返回
          if (!dialect.afterCount(count, parameter, rowBounds)) {
            // 當查詢總數為0時,直接返回空的結果
            return dialect.afterPage(new ArrayList(), parameter, rowBounds);
          }
        }
        // 判斷是否需要進行分頁查詢
        if (dialect.beforePage(ms, parameter, rowBounds)) {
          // 生成分頁的快取key
          CacheKey pageKey = cacheKey;
          // 處理引數物件
          parameter = dialect.processParameterObject(ms, parameter, boundSql, pageKey);
          // 呼叫方言獲取分頁sql
          String pageSql = dialect.getPageSql(ms, boundSql, parameter, rowBounds, pageKey);
          BoundSql pageBoundSql = new BoundSql(configuration, pageSql, boundSql.getParameterMappings(), parameter);
          // 設定動態引數
          for (String key : additionalParameters.keySet()) {
            pageBoundSql.setAdditionalParameter(key, additionalParameters.get(key));
          }
          // 執行分頁查詢
          resultList = executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, pageKey, pageBoundSql);
        } else {
          // 不執行分頁的情況下,也不執行記憶體分頁
          resultList = executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, cacheKey, boundSql);
        }
      } else {
        // rowBounds用引數值,不使用分頁外掛處理時,仍然支援預設的記憶體分頁
        resultList = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
      }
      return dialect.afterPage(resultList, parameter, rowBounds);
    } finally {
      dialect.afterAll();
    }
  }

  // 省略……
	
  @Override
  public Object plugin(Object target) {
    return Plugin.wrap(target, this);
  }

  @Override
  public void setProperties(Properties properties) {
    // 快取count ms
    msCountMap = CacheFactory.createCache(properties.getProperty("msCountCache"), "ms", properties);
    String dialectClass = properties.getProperty("dialect");
    if (StringUtil.isEmpty(dialectClass)) {
      dialectClass = default_dialect_class;
    }
    try {
      Class<?> aClass = Class.forName(dialectClass);
      dialect = (Dialect) aClass.newInstance();
    } catch (Exception e) {
      throw new PageException(e);
    }
    dialect.setProperties(properties);

    String countSuffix = properties.getProperty("countSuffix");
    if (StringUtil.isNotEmpty(countSuffix)) {
      this.countSuffix = countSuffix;
    }

    try {
      // 反射獲取BoundSql中的additionalParameters屬性
      additionalParametersField = BoundSql.class.getDeclaredField("additionalParameters");
      additionalParametersField.setAccessible(true);
    } catch (NoSuchFieldException e) {
      throw new PageException(e);
    }
  }
}

簡單看完 PageHelper 外掛的實現程式碼之後,總結一下 MyBatis 框架中自定義外掛的步驟:

  • ✅確定要被攔截的簽名:根據 @Intercepts 以及 @Signature 註解指定需要攔截的引數✅;
  • ✅實現 Intercept 介面的 intercept()、plugin() 以及 setProperties() 方法✅。

4. 自定義一個MyPageHelper分頁外掛

本節中會借鑑 PageHelper 外掛的實現思路來實現自定義一個 MyPageHelper 外掛,以更好地理解 MyBatis 框架執行外掛的流程。

4.1 MyPage類

本小節中實現了自定義的 MyPage 類,這是一個分頁返回物件,封裝了分頁的相關資訊以及分頁的列表資料,如下所示:

@Getter
public class MyPage<E> extends ArrayList<E> {

  private static final long serialVersionUID = 2630741492557235098L;
  /**  指定頁碼,從1開始  **/
  @Setter
  private Integer pageNum;

  /**  指定每頁記錄數  **/
  @Setter
  private Integer pageSize;

  /**  起始行  **/
  @Setter
  private Integer startIndex;

  /**  末行  **/
  @Setter
  private Integer endIndex;

  /**  總記錄數  **/
  private Long total;

  /**  總頁數  **/
  @Setter
  private Integer pages;
	
  // 根據pageNum、pageSize以及total設定其它屬性
  public void setTotal(Long total) {
    this.total = total;
    this.pages = (int)(total / pageSize + (total % pageSize == 0 ? 0 : 1));
    if (pageNum > pages) {
      pageNum = pages;
    }
    this.startIndex = this.pageNum > 0 ? (this.pageNum - 1) * this.pageSize : 0;
    this.endIndex = this.startIndex + this.pageSize * (this.pageNum > 0 ? 1 : 0);
  }

  // 獲取分頁後的結果
  public List<E> getResults() {
    return this;
  }
}

4.2 MyPageHelper類

進一步定義一個 MyPageHelper 類來輔助分頁,該類的核心是利用 ThreadLocal 執行緒遍歷儲存分頁資訊,程式碼如下所示:

/**
 * 分頁幫助類
 * @author chenliang258
 * @date 2021/3/17 17:23
 */
@SuppressWarnings("rawtypes")
public class MyPageHelper {

  private static final ThreadLocal<MyPage> MY_PAGE_THREAD_LOCAL = new ThreadLocal<>();

  public static void setMyPageThreadLocal(MyPage myPage) {
    MY_PAGE_THREAD_LOCAL.set(myPage);
  }

  public static MyPage geyMyPageThreadLocal() {
    return MY_PAGE_THREAD_LOCAL.get();
  }

  public static void clearMyPageThreadLocal() {
    MY_PAGE_THREAD_LOCAL.remove();
  }

  public static void startPage(Integer pageNum, Integer pageSize) {
    MyPage myPage = new MyPage();
    myPage.setPageNum(pageNum);
    myPage.setPageSize(pageSize);
    setMyPageThreadLocal(myPage);
  }
}

4.3 MyPageInterceptor類

接下來就需要編寫 Interceptor 介面的實現類來實現相應方法,這裡定義了 MyPageInterceptor 類,程式碼如下:

/**
 * 分頁攔截器實現
 * @author chenliang258
 * @date 2021/3/17 17:30
 */
@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 {

  private Field field;

  @SuppressWarnings({"rawtypes", "unchecked"})
  @Override
  public Object intercept(Invocation invocation) throws Throwable {
    Executor executor = (Executor)invocation.getTarget();
    Object[] args = invocation.getArgs();
    MappedStatement ms = (MappedStatement)args[0];
    Object parameter = args[1];
    RowBounds rowBounds = (RowBounds)args[2];
    ResultHandler resultHandler = (ResultHandler)args[3];
    CacheKey cacheKey;
    BoundSql boundSql;
    // 4個引數
    if (args.length == 4) {
      boundSql = ms.getBoundSql(parameter);
      cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);
    }
    // 6個引數
    else {
      cacheKey = (CacheKey)args[4];
      boundSql = (BoundSql)args[5];
    }
    // 判斷是否需要分頁
    MyPage myPage = MyPageHelper.geyMyPageThreadLocal();
    // 不執行分頁
    if (myPage.getPageNum() <= 0) {
      return executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
    }
    // count查詢
    MappedStatement countMs = newCountMappedStatement(ms);
    String sql = boundSql.getSql();
    String countSql = "select count(1) from (" + sql + ") _count";
    BoundSql countBoundSql =
        new BoundSql(ms.getConfiguration(), countSql, boundSql.getParameterMappings(), parameter);
    Map<String, Object> additionalParameters = (Map<String, Object>) field.get(boundSql);
    for (Map.Entry<String, Object> additionalParameter : additionalParameters.entrySet()) {
      countBoundSql.setAdditionalParameter(additionalParameter.getKey(), additionalParameter.getValue());
    }
    CacheKey countCacheKey = executor.createCacheKey(countMs, parameter, rowBounds, countBoundSql);
    Object countResult = executor.query(countMs, parameter, RowBounds.DEFAULT, resultHandler, countCacheKey, countBoundSql);
    Long count = (Long)((List)countResult).get(0);
    myPage.setTotal(count);
    // 分頁查詢
    String pageSql = sql + " limit " + myPage.getStartIndex() + "," + myPage.getPageSize();
    BoundSql pageBoundSql = new BoundSql(ms.getConfiguration(), pageSql, boundSql.getParameterMappings(), parameter);
    for (Map.Entry<String, Object> additionalParameter : additionalParameters.entrySet()) {
      pageBoundSql.setAdditionalParameter(additionalParameter.getKey(), additionalParameter.getValue());
    }
    CacheKey pageCacheKey = executor.createCacheKey(ms, parameter, rowBounds, pageBoundSql);
    List listResult = executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, pageCacheKey, pageBoundSql);
    myPage.addAll(listResult);
    // 清空執行緒區域性變數分頁資訊
    MyPageHelper.clearMyPageThreadLocal();
    return myPage;
  }

  @Override
  public Object plugin(Object target) {
    return Plugin.wrap(target, this);
  }

  @Override
  public void setProperties(Properties properties) {
    try {
      field = BoundSql.class.getDeclaredField("additionalParameters");
      field.setAccessible(true);
    } catch (NoSuchFieldException | SecurityException e) {
      e.printStackTrace();
    }
  }

  /**
   * 建立count的MappedStatement
   *
   * @param ms 原始MappedStatement
   * @return 新的帶有分頁資訊的MappedStatement
   */
  private MappedStatement newCountMappedStatement(MappedStatement ms) {
    MappedStatement.Builder builder = new MappedStatement.Builder(ms.getConfiguration(), ms.getId() + "_count",
        ms.getSqlSource(), ms.getSqlCommandType());
    builder.resource(ms.getResource());
    builder.fetchSize(ms.getFetchSize());
    builder.statementType(ms.getStatementType());
    builder.keyGenerator(ms.getKeyGenerator());
    if (ms.getKeyProperties() != null && ms.getKeyProperties().length != 0) {
      StringBuilder keyProperties = new StringBuilder();
      for (String keyProperty : ms.getKeyProperties()) {
        keyProperties.append(keyProperty).append(",");
      }
      keyProperties.delete(keyProperties.length() - 1, keyProperties.length());
      builder.keyProperty(keyProperties.toString());
    }
    builder.timeout(ms.getTimeout());
    builder.parameterMap(ms.getParameterMap());
    // count查詢返回值int
    List<ResultMap> resultMaps = new ArrayList<>();
    ResultMap resultMap = new ResultMap.Builder(ms.getConfiguration(), ms.getId() + "_count", Long.class,
        new ArrayList<>(0)).build();
    resultMaps.add(resultMap);
    builder.resultMaps(resultMaps);
    builder.resultSetType(ms.getResultSetType());
    builder.cache(ms.getCache());
    builder.flushCacheRequired(ms.isFlushCacheRequired());
    builder.useCache(ms.isUseCache());

    return builder.build();
  }
}

4.4 MyPageHelper外掛測試

要測試自定義的 MyPageHelper 外掛,首先必須要在 mybatis-config.xml 配置檔案中新增自定義外掛的配置資訊,如下所示:

<plugins>
  <!-- 使用自定義外掛MyPageHelper -->
  <plugin interceptor="com.chiaki.mypagehelper.MyPageInterceptor" />
</plugins>

然後編寫 MyPageHelper 外掛的測試方法,如下所示:

@Test
public void testMyPageHelper() {
  Integer pageNum = 2, pageSize = 3;
  StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);
  MyPageHelper.startPage(pageNum, pageSize);
  List<Student> students = studentMapper.findAll();
  System.out.println(students);
}

執行測試方法後,其結果如下圖所示。可以看出在資料庫表中共有 4 條記錄,在設定了 pageNum = 2, pageSize = 3 引數後,查詢結果顯示的是第二頁的資料,僅此 1 條,結果符合預期,這也驗證了本節中自定義 MyPageHelper 分頁外掛的正確性。

https://i.iter01.com/images/57a1efd96359e55a2523691f227ad0a837de7ec374288687417fbf30a9c92376.png

總結

本文首先介紹了 MyBatis 框架使用中比較實用的 Lombok 以及 PageHelper 外掛,然後從 PageHelper 外掛出發簡單介紹了 MyBatis 框架中外掛的解析與執行流程,並在此基礎上實現了一個自定義的 MyPageHelper 分頁外掛。筆者認為在實際應用中不必重複造輪子,有好用的外掛直接使用就行,大大提高開發效率。但是從另外一個角度看,所謂知其然也要知其所以然,從原始碼去理解實現原理並能夠自己動手實現一遍對於個人的進步是非常有用的。本文中只是很簡略地介紹了下 MyBatis 的外掛解析與執行過程,實現的 MyPageHelper 外掛也處於模仿的層面。讀者感興趣的話可以自行去探究原始碼,能夠在模仿中創新是最好不過了!

參考資料

Lombok 官方社群:https://projectlombok.org/

PageHelper 官方社群:https://pagehelper.github.io/

MyBatis 官網:https://mybatis.org/mybatis-3/

MyBatis 原始碼倉庫:https://github.com/mybatis/mybatis-3

淺析MyBatis(一):由一個快速案例剖析MyBatis的整體架構與執行流程

淺析MyBatis(二):手寫一個自己的MyBatis簡單框架

???

覺得有用的話就點個推薦吧~

相關文章