数据库中间件 Sharding-JDBC 源码分析 —— SQL 解析之词法解析

1. 概述

SQL 解析引擎,数据库中间件必备的功能和流程。Sharding-JDBC 在 1.5.0.M1 正式发布时,将 SQL 解析引擎从 Druid 替换成了自研的。新引擎仅解析分片上下文,对于 SQL 采用"半理解"理念,进一步提升性能和兼容性,同时降低了代码复杂度。

SQL 解析引擎有两大组件:

  1. Lexer:词法解析器。
  2. Parser:SQL解析器。

两者都是解析器,区别在于 Lexer 只做词法的解析,不关注上下文,将字符串拆解成 N 个词法。而 Parser 在 Lexer 的基础上,还需要理解 SQL 。打个比方:

SQL :SELECT * FROM t_user
Lexer :[SELECT] [ * ] [FROM] [t_user]
Parser:这是一条 [SELECT] 查询表为 [t_user] ,并且返回 [ * ] 所有字段的 SQL。

2. Lexer 词法解析器

Lexer 原理:顺序解析 SQL,将字符串拆解成 N 个词法。
核心代码如下:

/**
 * 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(词法标记)。我们来执行一次,看看 SQL 会被拆解成哪些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

眼尖的同学可能看到了 Tokenizer。对的,它是 Lexer 的好基佬,负责分词。

我们来总结下,Lexer#nextToken()方法里,使用 skipIgnoredToken() 方法跳过忽略的 Token(如空格、注释),通过 isXXXX()方法判断好下一个 Token 的类型后,交给 Tokenizer 进行分词返回 Token。

由于不同数据库遵守 SQL 规范略有不同,所以不同的数据库对应不同的 Lexer。



子 Lexer 通过重写方法实现自己独有的 SQL 语法。

3. Token 词法标记

Token中一共有三个属性:

  • TokenType type :词法标记类型
  • String literals :词法字面量标记
  • int endPosition : literals 在 SQL 里的结束位置

TokenType词法标记类型,一共分成 4 个大类:

  • Keyword :词法关键词
  • Literals :词法字面量标记
  • Symbol :词法符号标记
  • Assist :词法辅助标记

3.1 Keyword 词法关键词

不同数据库有自己独有的词法关键词,例如 MySQL 熟知的分页 Limit。

我们以 MySQL 举个例子,当创建 MySQLLexer 时,会加载 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 {
        /**
         * 词法关键词Map
         */
        private final Map<String, Keyword> tokens = new HashMap<>(1024);

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

        /**
         * 装上默认词法关键词 + 方言词法关键词
         * 不同的数据库有相同的默认词法关键词,有不同的方言关键词
         *
         * @param dialectKeywords 方言词法关键词
         */
        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 词法字面量标记

Literals 词法字面量标记,一共分成 6 种:

  • IDENTIFIER :词法关键词
    例如:表名,查询字段 等等。

  • VARIABLE :变量
    例如: SELECT @@VERSION 。在 MySQL 里,@代表用户变量,@@代表系统变量。

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

  • HEX :十六进制
    以“0x”开头的数据。

  • INT :整数
    例如: SELECT * FROM t_user WHERE id = 1。

  • FLOAT :浮点数
    例如: SELECT * FROM t_user WHERE id = 1.0。

3.3 Symbol 词法符号标记

词法符号标记。例如:"{", "}", ">=" 等等。
解析核心代码如下:

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

    // CharType.java
    /**
    * 判断是否为符号.
    *
    * @param ch 待判断的字符
    * @return 是否为符号
    */
    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
    /**
    * 扫描符号.
    *
    * @return 符号标记
    */
    public Token scanSymbol() {

       int length = 0;

       while (CharType.isSymbol(charAt(offset + length))) {
           length++;
       }
       String literals = input.substring(offset, offset + length);
       // 倒序遍历,查询符合条件的 符号。例如 literals = ";;",会是拆分成两个 ";"。如果基于正序,literals = "<=",会被解析成 "<" + "="。
       Symbol symbol;
       while (null == (symbol = Symbol.literalsOf(literals))) {
           literals = input.substring(offset, offset + --length);
       }
       return new Token(symbol, literals, offset + length);
    }

3.4 Assist 词法辅助标记

Assist 词法辅助标记,一共分成 2 种:

  • END :分析结束
  • ERROR :分析错误。

4. 结束

Lexer 词法解析已经讲解完毕,下一节我们将讨论 SQL 解析,尽请关注!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342