前言
前有芋艿大佬已經發過相關分析的文章,自己覺的原始碼總歸要看一下,然後看了就要記錄下來(記性很差…),所以就有了這篇文章(以後還要繼續更?)
,希望我們都能在看過文章後能夠有不一樣的收穫。
宣告:本文基於1.5.M1版本
相關的UML類圖
解析:
首先我們來看下解析sql的過程中用到的類做一個解釋:
- TokenType:衍生了多個子類,用來標記sql拆分過程中,每個被拆分的詞的型別(比如select屬於KeyWord,”;”屬於Symbol)
- Lexer:sql具體的解析類,通過呼叫nextToken()方法分析sql每個詞的型別;
- Tokenizer:具體的標記類,標記具體的詞,配合Lexer的nextToken()方法使用
- Token:標記後的結果,type:具體的詞型別、literals:具體的詞、endPosition:這個詞在sql中的最後位置(index)
@Test
public void assertNextTokenForOrderBy() {
Lexer lexer = new Lexer("SELECT * FROM ORDER ORDER BY XX DESC", dictionary);
//lexer.nextToken();
LexerAssert.assertNextToken(lexer, DefaultKeyword.SELECT, "SELECT");
//lexer.nextToken();
LexerAssert.assertNextToken(lexer, Symbol.STAR, "*");
//lexer.nextToken();
LexerAssert.assertNextToken(lexer, DefaultKeyword.FROM, "FROM");
//lexer.nextToken();
LexerAssert.assertNextToken(lexer, Literals.IDENTIFIER, "ORDER");
//lexer.nextToken();
LexerAssert.assertNextToken(lexer, DefaultKeyword.ORDER, "ORDER");
//lexer.nextToken();
LexerAssert.assertNextToken(lexer, DefaultKeyword.BY, "BY");
//lexer.nextToken();
LexerAssert.assertNextToken(lexer, Literals.IDENTIFIER, "XX");
//lexer.nextToken();
LexerAssert.assertNextToken(lexer, DefaultKeyword.DESC, "DESC");
//lexer.nextToken();
LexerAssert.assertNextToken(lexer, Assist.END, "");
}
複製程式碼
上面是專案中的一段測試用例,我們以這個用例來分析。
- 第一次呼叫nextToken()
/**
* 分析下一個詞法標記.
*/
public final void nextToken() {
skipIgnoredToken();
if (isVariableBegin()) {
currentToken = new Tokenizer(input, dictionary, offset).scanVariable();
} else if (isNCharBegin()) {
currentToken = new Tokenizer(input, dictionary, ++offset).scanChars();
} else if (isIdentifierBegin()) {
currentToken = new Tokenizer(input, dictionary, offset).scanIdentifier();
} else if (isHexDecimalBegin()) {
currentToken = new Tokenizer(input, dictionary, offset).scanHexDecimal();
} else if (isNumberBegin()) {
currentToken = new Tokenizer(input, dictionary, offset).scanNumber();
} else if (isSymbolBegin()) {
currentToken = new Tokenizer(input, dictionary, offset).scanSymbol();
} else if (isCharsBegin()) {
currentToken = new Tokenizer(input, dictionary, offset).scanChars();
} else if (isEnd()) {
currentToken = new Token(Assist.END, "", offset);
} else {
currentToken = new Token(Assist.ERROR, "", offset);
}
offset = currentToken.getEndPosition();
}
複製程式碼
- 先走skipIgnoredToken();
- 跳過空格
- 跳過以/*!開頭的(Mysql是這樣)的字元,對於不同資料庫。isHintBegin實現了不同的處理
- 跳過註釋
private void skipIgnoredToken() {
offset = new Tokenizer(input, dictionary, offset).skipWhitespace();
while (isHintBegin()) {
offset = new Tokenizer(input, dictionary, offset).skipHint();
offset = new Tokenizer(input, dictionary, offset).skipWhitespace();
}
while (isCommentBegin()) {
offset = new Tokenizer(input, dictionary, offset).skipComment();
offset = new Tokenizer(input, dictionary, offset).skipWhitespace();
}
}
複製程式碼
這裡我們以跳過空格為例來展開說明:
從傳入的offset標誌位開始,迴圈判斷sql語句中對應位置的字元是不是空格,直到不是空格就退出,返回最新位置的offset
/**
* 跳過空格.
*
* @return 跳過空格後的偏移量
*/
public int skipWhitespace() {
int length = 0;
while (CharType.isWhitespace(charAt(offset + length))) {
length++;
}
return offset + length;
}
private char charAt(final int index) {
return index >= input.length() ? (char) CharType.EOI : input.charAt(index);
}
/**
* 判斷是否為空格.
*
* @param ch 待判斷的字元
* @return 是否為空格
*/
public static boolean isWhitespace(final char ch) {
return ch <= 32 && EOI != ch || 160 == ch || ch >= 0x7F && ch <= 0xA0;
}
複製程式碼
- 第二步 從最新位置的offset開始,繼續判斷是否是變數,這裡以mysql為例,開始的單詞是‘SELECT’,所以進入第三步
/**
這是mysql的實現
**/
@Override
protected boolean isVariableBegin() {
return `@` == getCurrentChar(0);
}
複製程式碼
- 第三步 判斷是否是NChar,false,進入第四步
private boolean isNCharBegin() {
return isSupportNChars() && `N` == getCurrentChar(0) && ``` == getCurrentChar(1);
}
複製程式碼
- 第四步 判斷是否是識別符號 true
- 掃描識別符號
- 迴圈判斷當前的識別符號是不是字元,直到不是字元
- 擷取這個字串
- 判斷是否是雙關詞彙(group、order)
- 如果4符合,則進一步做特殊處理
- 構造Token返回
private boolean isIdentifierBegin() {
return isIdentifierBegin(getCurrentChar(0));
}
private boolean isIdentifierBegin(final char ch) {
return CharType.isAlphabet(ch) || ``` == ch || `_` == ch || `$` == ch;
}
/**
* 判斷是否為字母.
*
* @param ch 待判斷的字元
* @return 是否為字母
*/
public static boolean isAlphabet(final char ch) {
return ch >= `A` && ch <= `Z` || ch >= `a` && ch <= `z`;
}
複製程式碼
/**
* 掃描識別符號.
*
* @return 識別符號標記
*/
public Token scanIdentifier() {
if (``` == charAt(offset)) {
int length = getLengthUntilTerminatedChar(```);
return new Token(Literals.IDENTIFIER, input.substring(offset, offset + length), offset + length);
}
int length = 0;
while (isIdentifierChar(charAt(offset + length))) {
length++;
}
String literals = input.substring(offset, offset + length);
if (isAmbiguousIdentifier(literals)) {
return new Token(processAmbiguousIdentifier(offset + length, literals), literals, offset + length);
}
return new Token(dictionary.findTokenType(literals, Literals.IDENTIFIER), literals, offset + length);
}
複製程式碼
- 返回最終的Token,賦值給currentToken,更新offset,此時的Token內容如下。第一個 “SELECT” 就解析出來了,後面的單詞繼續呼叫nextToken(),方法差不多,區別就是詞法的型別不一樣,走的判斷可能邏輯會不同,後面有興趣的可以自己跟著程式碼去看看。
最後
小尾巴走一波,歡迎關注我的公眾號,不定期分享程式設計、投資、生活方面的感悟:)