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, 在該解析器中, 是需要加入轉義符號\\
。
即我們在最終的字元中想要得到 ${
字元, 則應該這樣寫 \\${
。
對於情況2, 該解析器中是這樣子處理的
builder.append(src, offset, start - offset - 1).append(openToken);
offset = start + openToken.length();
把轉移符去掉, 將字串新增到 builder 中。 記錄解析到的位置 offset。 繼續查詢下一個開始標記。
對於情況1,
接著, 查詢結束標記符
我們找到了 }
, 有兩種情況:
- } 是開始標記符
- } 就是我們本身想要的字元
那麼, 如同前面, 情況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 程式碼
如有問題, 請跟我交流。