Shading-jdbc原始碼分析-sql詞法解析

selrain_公眾號也叫selrain發表於2019-03-04

前言

前有芋艿大佬已經發過相關分析的文章,自己覺的原始碼總歸要看一下,然後看了就要記錄下來(記性很差...),所以就有了這篇文章(以後還要繼續更?) ,希望我們都能在看過文章後能夠有不一樣的收穫。

宣告:本文基於1.5.M1版本

相關的UML類圖

TokenType

Shading-jdbc原始碼分析-sql詞法解析

Shading-jdbc原始碼分析-sql詞法解析

Shading-jdbc原始碼分析-sql詞法解析

解析:

首先我們來看下解析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 \t  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();
  1. 跳過空格
  2. 跳過以/*!開頭的(Mysql是這樣)的字元,對於不同資料庫。isHintBegin實現了不同的處理
  3. 跳過註釋
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
  1. 掃描識別符號
  2. 迴圈判斷當前的識別符號是不是字元,直到不是字元
  3. 擷取這個字串
  4. 判斷是否是雙關詞彙(group、order)
  5. 如果4符合,則進一步做特殊處理
  6. 構造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(),方法差不多,區別就是詞法的型別不一樣,走的判斷可能邏輯會不同,後面有興趣的可以自己跟著程式碼去看看。

Shading-jdbc原始碼分析-sql詞法解析

最後

小尾巴走一波,歡迎關注我的公眾號,不定期分享程式設計、投資、生活方面的感悟:)

Shading-jdbc原始碼分析-sql詞法解析

相關文章