mybatis抽取出的工具-(一)通用標記解析器(即拿即用)

在深入理解 mybatis 原理過程中, 我不單單是想理解整個 mybatis 是怎麼運行的, 我還想從這個過程中提取出一些對自己有益的編程方法, 編程思想, 註釋, 以及一些實用工具類。

1. 簡介

1.1 mybatis-config.xml 中使用

mybatis-config.xml 文件中, 我們常常看到類似的配置

<properties>
    <property name="driver" value="com.mysql.jdbc.Driver" />
    <property name="url" value="jdbc:mysql://localhost:3306/mybatis" />
    <property name="username" value="root" />
    <property name="password" value="aaabbb" />
</properties>
<environments default="development">
    <environment id="development">
        <transactionManager type="JDBC">
            <property name="" value=""/>
        </transactionManager>
        <dataSource type="POOLED">
            <property name="driver" value="${driver}"/>
            <property name="url" value="${url}"/>
            <!--填寫你的數據庫用戶名-->
            <property name="username" value="${username}"/>
            <!--填寫你的數據庫密碼-->
            <property name="password" value="${password}"/>
        </dataSource>
    </environment>
</environments>

將一些屬性放置在 properties標籤下的子標籤中, 後續在配置文件中就可以使用 ${key} 的形式將 value 取出。

也可以將屬性配置在外部文件中,將外部文件的相對路徑告知解析器即可:

<properties resource="jdbc.properties"></properties>

1.2 xxxMapper.xml 中使用

當然, 在 xxxMapper.xml 中, 我們寫 SQL 語句時也會用到, 如

    select
    <include refid="Base_Column_List" />
    from student
    where student_id=#{student_id, jdbcType=INTEGER}

#{student_id, jdbcType=INTEGER} 替換爲傳入的參數。

2. 原理

在 mybatis 中, 處理這個過程的就是 GenericTokenParser 類。

2.1 GenericTokenParser 成員變量

GenericTokenParser 類有三個成員變量

// 開始標記
private final String openToken;
// 結束標記
private final String closeToken;
// 表處理器
private final TokenHandler handler;

舉個例子

解析以上配置中的 ${driver}, 那麼這幾個成員變量

openToken="${";
closeToken="}";

handler則是一個 TokenHandler 接口

public interface TokenHandler {
  String handleToken(String content);
}

在實際的過程中, 我們需要自己定義的處理器, 該處理器實現 TokenHandler 即可, 後面的例子中會有示例。

2.2 GenericTokenParser 構造函數

構造函數很簡單, 就是給幾個成員變量賦值即可。

public GenericTokenParser(String openToken, String closeToken, TokenHandler handler) {
    this.openToken = openToken;
    this.closeToken = closeToken;
    this.handler = handler;
}

2.3 解析過程

2.3.1 整體流程

大的流程如下:

解析過程

整體上來講, 就是找到這個需要處理的表達式, 將表達式的內容替換爲處理器處理後的內容, 最後返回最終的字符串。

2.3.2 流程詳解

先看代碼以及我給的註釋

public String parse(String text) {
    if (text == null || text.isEmpty()) {
        return "";
    }
    // 從第0位開始, 查找開始標記的下標
    int start = text.indexOf(openToken, 0);
    if (start == -1) { // 找不到則返回原參數
        return text;
    }
    char[] src = text.toCharArray();
    // offset用來記錄builder變量讀取到了哪
    int offset = 0;
    // builder 是最終返回的字符串
    final StringBuilder builder = new StringBuilder();
    // expression 是每一次找到的表達式, 要傳入處理器中進行處理
    StringBuilder expression = null;
    while (start > -1) {
        if (start > 0 && src[start - 1] == '\\') {
            // 開始標記是轉義的, 則去除轉義字符'\'
            builder.append(src, offset, start - offset - 1).append(openToken);
            offset = start + openToken.length();
        } else {
            // 此分支是找到了結束標記, 要找到結束標記
            if (expression == null) {
                expression = new StringBuilder();
            } else {
                expression.setLength(0);
            }
            // 將開始標記前的字符串都添加到 builder 中 
            builder.append(src, offset, start - offset);
            // 計算新的 offset
            offset = start + openToken.length();
            
            // 從此處開始查找結束的標記
            int end = text.indexOf(closeToken, offset);
            while (end > -1) {
                if (end > offset && src[end - 1] == '\\') {
                    // 此結束標記是轉義的
                    expression.append(src, offset, end - offset - 1).append(closeToken);
                    offset = end + closeToken.length();
                    end = text.indexOf(closeToken, offset);
                } else {
                    expression.append(src, offset, end - offset);
                    offset = end + closeToken.length();
                    break;
                }
            }
            if (end == -1) {
                // 找不到結束標記了
                builder.append(src, start, src.length - start);
                offset = src.length;
            } else {
                // 找到了結束的標記, 則放入處理器進行處理
                builder.append(handler.handleToken(expression.toString()));
                offset = end + closeToken.length();
            }
        }
        // 因爲字符串中可能有很多表達式需要解析, 因此開始下一個表達式的查找
        start = text.indexOf(openToken, offset);
    }
    // 最後一次未找到開始標記, 則將 offset 後的字符串添加到 builder 中
    if (offset < src.length) {
        builder.append(src, offset, src.length - offset);
    }
    return builder.toString();
}

如果你看代碼看明白了, 就不用看下面的詳細過程了

第一步:就是參數的非空處理

參數爲空或 “” , 則返回 “”。

if (text == null || text.isEmpty()) {
    return "";
}

第二步:查找開始標記,聲明變量

    // 從第0位開始, 查找開始標記的下標
    int start = text.indexOf(openToken, 0);
    if (start == -1) { // 找不到則返回原參數
        return text;
    }
    char[] src = text.toCharArray();
    // offset用來記錄 builder 變量讀取到的位置
    int offset = 0;
    // builder 是最終返回的字符串
    final StringBuilder builder = new StringBuilder();
    // expression 是每一次找到的表達式, 要傳入處理器中進行處理
    StringBuilder expression = null;

如果傳入的字符串中沒有要處理的開始標記, 那麼直接就返回了, 不需要進行一堆變量的聲明。

如果找到了, 則進行變量的聲明。

第三步: 循環查找標記並進行處理

在本例子中, 假設開始標記爲 ${, 結束標記爲 }

首先, 先查找開始標記符

只有找到了開始標記符才需要去找對應的結束標記符, 不然單獨找到結束標記符沒有意義。

我們找到了 ${, 有兩種情況:

  1. ${ 是開始標記符
  2. ${ 就是我們本身想要的字符

如何區分這兩種情況呢, 正常的情況得到的是情況1, 如果想得到情況2, 在該解析器中, 是需要加入轉義符號\\

即我們在最終的字符中想要得到 ${字符, 則應該這樣寫 \\${

對於情況2, 該解析器中是這樣子處理的

 builder.append(src, offset, start - offset - 1).append(openToken);
 offset = start + openToken.length();

把轉移符去掉, 將字符串添加到 builder 中。 記錄解析到的位置 offset。 繼續查找下一個開始標記。

對於情況1,

接着, 查找結束標記符

我們找到了 }, 有兩種情況:

  1. } 是開始標記符
  2. } 就是我們本身想要的字符

那麼, 如同前面, 情況2也需要轉移標記才能區分。情況2處理

 // 此結束標記是轉義的
expression.append(src, offset, end - offset - 1).append(closeToken);
offset = end + closeToken.length();
end = text.indexOf(closeToken, offset);

把轉移符去掉, 將字符串添加到 builder 中。 記錄解析到的位置 offset。 繼續查找下一個結束標記,直到找到的是情況1, 則跳出循環。

對於情況1, 得到${}之間的表字符串作爲 expression, 繼續往下

處理器處理

 // 找到了結束的標記, 則放入處理器進行處理
builder.append(handler.handleToken(expression.toString()));
offset = end + closeToken.length();

最後, 只要還沒找到最後, 則繼續查找下一個開始的標記

start = text.indexOf(openToken, offset);

3. 測試

  @Test
  public void simpleTest() {
    GenericTokenParser parser = new GenericTokenParser("${", "}", new VariableTokenHandler(new HashMap<String, String>() {
      {
        put("driver", "com.mysql.jdbc.Driver");
        put("url", "jdbc:mysql://localhost:3306/mybatis");
        put("username", "root");
        put("password", "aaabbb");

      }
    }));

    // 測試單個解析
    assertEquals("com.mysql.jdbc.Driver", parser.parse("${driver}"));
    // 多個一起測試
    assertEquals("驅動=com.mysql.jdbc.Driver,地址=jdbc:mysql://localhost:3306/mybatis,用戶名=root",
            parser.parse("驅動=${driver},地址=${url},用戶名=${username}"));
  }

4 代碼

如需要代碼, 請訪問我的Github

如有問題, 請跟我交流。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章