數(shù)據(jù)庫中間件 Sharding-JDBC 源碼分析 —— SQL 解析之詞法解析

1. 概述

SQL 解析引擎,數(shù)據(jù)庫中間件必備的功能和流程筝野。Sharding-JDBC 在 1.5.0.M1 正式發(fā)布時(shí)晌姚,將 SQL 解析引擎從 Druid 替換成了自研的粤剧。新引擎僅解析分片上下文歇竟,對(duì)于 SQL 采用"半理解"理念,進(jìn)一步提升性能和兼容性抵恋,同時(shí)降低了代碼復(fù)雜度焕议。

SQL 解析引擎有兩大組件:

  1. Lexer:詞法解析器。
  2. Parser:SQL解析器弧关。

兩者都是解析器盅安,區(qū)別在于 Lexer 只做詞法的解析唤锉,不關(guān)注上下文,將字符串拆解成 N 個(gè)詞法别瞭。而 Parser 在 Lexer 的基礎(chǔ)上窿祥,還需要理解 SQL 。打個(gè)比方:

SQL :SELECT * FROM t_user
Lexer :[SELECT] [ * ] [FROM] [t_user]
Parser:這是一條 [SELECT] 查詢表為 [t_user] 蝙寨,并且返回 [ * ] 所有字段的 SQL晒衩。

2. Lexer 詞法解析器

Lexer 原理:順序解析 SQL,將字符串拆解成 N 個(gè)詞法墙歪。
核心代碼如下:

/**
 * Lexical analysis.
 * 
 * @author zhangliang 
 */
@RequiredArgsConstructor
public class Lexer {
    
    @Getter
    private final String input;
    
    private final Dictionary dictionary;
    
    private int offset;
    
    @Getter
    private Token currentToken;
    
    /**
     * Analyse next token.
     */
    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 {
            throw new SQLParsingException(this, Assist.ERROR);
        }
        offset = currentToken.getEndPosition();
    }
    
    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();
        }
    }
   
}

通過nextToken()方法听系,不斷解析出Token(詞法標(biāo)記)。我們來執(zhí)行一次虹菲,看看 SQL 會(huì)被拆解成哪些Token靠胜。

SQL :SELECT i.* FROM t_order o JOIN t_order_item i ON o.order_id=i.order_id WHERE o.user_id=? AND o.order_id=?

literals TokenType類 TokenType值 endPosition
SELECT DefaultKeyword SELECT 6
i Literals IDENTIFIER 8
. Symbol DOT 9
* Symbol STAR 10
FROM DefaultKeyword FROM 15
t_order Literals IDENTIFIER 23
o Literals IDENTIFIER 25
JOIN DefaultKeyword JOIN 30
t_order_item Literals IDENTIFIER 43
i Literals IDENTIFIER 45
ON DefaultKeyword ON 48
o Literals IDENTIFIER 50
. Symbol DOT 51
order_id Literals IDENTIFIER 59
= Symbol EQ 60
i Literals IDENTIFIER 61
. Symbol DOT 62
order_id Literals IDENTIFIER 70
WHERE DefaultKeyword WHERE 76
o Literals IDENTIFIER 78
. Symbol DOT 79
user_id Literals IDENTIFIER 86
= Symbol EQ 87
? Symbol QUESTION 88
AND DefaultKeyword AND 92
o Literals IDENTIFIER 94
. Symbol DOT 95
order_id Literals IDENTIFIER 103
= Symbol EQ 104
? Symbol QUESTION 105
Assist END 105

眼尖的同學(xué)可能看到了 Tokenizer。對(duì)的毕源,它是 Lexer 的好基佬浪漠,負(fù)責(zé)分詞。

我們來總結(jié)下霎褐,Lexer#nextToken()方法里郑藏,使用 skipIgnoredToken() 方法跳過忽略的 Token(如空格、注釋)瘩欺,通過 isXXXX()方法判斷好下一個(gè) Token 的類型后必盖,交給 Tokenizer 進(jìn)行分詞返回 Token。

由于不同數(shù)據(jù)庫遵守 SQL 規(guī)范略有不同俱饿,所以不同的數(shù)據(jù)庫對(duì)應(yīng)不同的 Lexer歌粥。



子 Lexer 通過重寫方法實(shí)現(xiàn)自己獨(dú)有的 SQL 語法。

3. Token 詞法標(biāo)記

Token中一共有三個(gè)屬性:

  • TokenType type :詞法標(biāo)記類型
  • String literals :詞法字面量標(biāo)記
  • int endPosition : literals 在 SQL 里的結(jié)束位置

TokenType詞法標(biāo)記類型拍埠,一共分成 4 個(gè)大類:

  • Keyword :詞法關(guān)鍵詞
  • Literals :詞法字面量標(biāo)記
  • Symbol :詞法符號(hào)標(biāo)記
  • Assist :詞法輔助標(biāo)記

3.1 Keyword 詞法關(guān)鍵詞

不同數(shù)據(jù)庫有自己獨(dú)有的詞法關(guān)鍵詞失驶,例如 MySQL 熟知的分頁 Limit。

我們以 MySQL 舉個(gè)例子枣购,當(dāng)創(chuàng)建 MySQLLexer 時(shí)嬉探,會(huì)加載 DefaultKeyword 和 MySQLKeyword。核心代碼如下:

    // MySQLLexer.java
    public final class MySQLLexer extends Lexer {
        /**
         * 字典
         */
        private static Dictionary dictionary = new Dictionary(MySQLKeyword.values());

        public MySQLLexer(final String input) {
            super(input, dictionary);
        }
    }

    // Dictionary.java
    public final class Dictionary {
        /**
         * 詞法關(guān)鍵詞Map
         */
        private final Map<String, Keyword> tokens = new HashMap<>(1024);

        public Dictionary(final Keyword... dialectKeywords) {
            fill(dialectKeywords);
        }

        /**
         * 裝上默認(rèn)詞法關(guān)鍵詞 + 方言詞法關(guān)鍵詞
         * 不同的數(shù)據(jù)庫有相同的默認(rèn)詞法關(guān)鍵詞棉圈,有不同的方言關(guān)鍵詞
         *
         * @param dialectKeywords 方言詞法關(guān)鍵詞
         */
        private void fill(final Keyword... dialectKeywords) {
            for (DefaultKeyword each : DefaultKeyword.values()) {
                tokens.put(each.name(), each);
            }
            for (Keyword each : dialectKeywords) {
                tokens.put(each.toString(), each);
            }
        }
    }

3.2 Literals 詞法字面量標(biāo)記

Literals 詞法字面量標(biāo)記涩堤,一共分成 6 種:

  • IDENTIFIER :詞法關(guān)鍵詞
    例如:表名,查詢字段 等等分瘾。

  • VARIABLE :變量
    例如: SELECT @@VERSION 胎围。在 MySQL 里,@代表用戶變量,@@代表系統(tǒng)變量白魂。

  • CHARS :字符串
    例如: SELECT "123" 汽纤。

  • HEX :十六進(jìn)制
    以“0x”開頭的數(shù)據(jù)。

  • INT :整數(shù)
    例如: SELECT * FROM t_user WHERE id = 1福荸。

  • FLOAT :浮點(diǎn)數(shù)
    例如: SELECT * FROM t_user WHERE id = 1.0蕴坪。

3.3 Symbol 詞法符號(hào)標(biāo)記

詞法符號(hào)標(biāo)記。例如:"{", "}", ">=" 等等敬锐。
解析核心代碼如下:

    // Lexer.java
    /**
    * 是否是 符號(hào)
    *
    * @see Tokenizer#scanSymbol()
    * @return 是否
    */
    private boolean isSymbolBegin() {
       return CharType.isSymbol(getCurrentChar(0));
    }

    // CharType.java
    /**
    * 判斷是否為符號(hào).
    *
    * @param ch 待判斷的字符
    * @return 是否為符號(hào)
    */
    public static boolean isSymbol(final char ch) {

       return '(' == ch || ')' == ch || '[' == ch || ']' == ch || '{' == ch || '}' == ch || '+' == ch || '-' == ch || '*' == ch || '/' == ch || '%' == ch || '^' == ch || '=' == ch
               || '>' == ch || '<' == ch || '~' == ch || '!' == ch || '?' == ch || '&' == ch || '|' == ch || '.' == ch || ':' == ch || '#' == ch || ',' == ch || ';' == ch;
    }

    // Tokenizer.java
    /**
    * 掃描符號(hào).
    *
    * @return 符號(hào)標(biāo)記
    */
    public Token scanSymbol() {

       int length = 0;

       while (CharType.isSymbol(charAt(offset + length))) {
           length++;
       }
       String literals = input.substring(offset, offset + length);
       // 倒序遍歷辞嗡,查詢符合條件的 符號(hào)。例如 literals = ";;"滞造,會(huì)是拆分成兩個(gè) ";"续室。如果基于正序,literals = "<="谒养,會(huì)被解析成 "<" + "="挺狰。
       Symbol symbol;
       while (null == (symbol = Symbol.literalsOf(literals))) {
           literals = input.substring(offset, offset + --length);
       }
       return new Token(symbol, literals, offset + length);
    }

3.4 Assist 詞法輔助標(biāo)記

Assist 詞法輔助標(biāo)記,一共分成 2 種:

  • END :分析結(jié)束
  • ERROR :分析錯(cuò)誤买窟。

4. 結(jié)束

Lexer 詞法解析已經(jīng)講解完畢丰泊,下一節(jié)我們將討論 SQL 解析,盡請(qǐng)關(guān)注始绍!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末瞳购,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子亏推,更是在濱河造成了極大的恐慌学赛,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,576評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件吞杭,死亡現(xiàn)場離奇詭異盏浇,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)芽狗,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,515評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門绢掰,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人童擎,你說我怎么就攤上這事滴劲。” “怎么了顾复?”我有些...
    開封第一講書人閱讀 168,017評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵班挖,是天一觀的道長。 經(jīng)常有香客問我捕透,道長聪姿,這世上最難降的妖魔是什么碴萧? 我笑而不...
    開封第一講書人閱讀 59,626評(píng)論 1 296
  • 正文 為了忘掉前任乙嘀,我火速辦了婚禮末购,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘虎谢。我一直安慰自己盟榴,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,625評(píng)論 6 397
  • 文/花漫 我一把揭開白布婴噩。 她就那樣靜靜地躺著擎场,像睡著了一般。 火紅的嫁衣襯著肌膚如雪几莽。 梳的紋絲不亂的頭發(fā)上迅办,一...
    開封第一講書人閱讀 52,255評(píng)論 1 308
  • 那天,我揣著相機(jī)與錄音章蚣,去河邊找鬼站欺。 笑死,一個(gè)胖子當(dāng)著我的面吹牛纤垂,可吹牛的內(nèi)容都是我干的矾策。 我是一名探鬼主播,決...
    沈念sama閱讀 40,825評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼峭沦,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼贾虽!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起吼鱼,我...
    開封第一講書人閱讀 39,729評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤蓬豁,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后菇肃,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體庆尘,經(jīng)...
    沈念sama閱讀 46,271評(píng)論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,363評(píng)論 3 340
  • 正文 我和宋清朗相戀三年巷送,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了驶忌。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,498評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡笑跛,死狀恐怖付魔,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情飞蹂,我是刑警寧澤几苍,帶...
    沈念sama閱讀 36,183評(píng)論 5 350
  • 正文 年R本政府宣布,位于F島的核電站陈哑,受9級(jí)特大地震影響妻坝,放射性物質(zhì)發(fā)生泄漏伸眶。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,867評(píng)論 3 333
  • 文/蒙蒙 一刽宪、第九天 我趴在偏房一處隱蔽的房頂上張望厘贼。 院中可真熱鬧,春花似錦圣拄、人聲如沸嘴秸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,338評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽岳掐。三九已至,卻和暖如春饭耳,著一層夾襖步出監(jiān)牢的瞬間串述,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,458評(píng)論 1 272
  • 我被黑心中介騙來泰國打工寞肖, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留纲酗,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,906評(píng)論 3 376
  • 正文 我出身青樓逝淹,卻偏偏與公主長得像耕姊,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子栅葡,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,507評(píng)論 2 359