数据库中间件 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。


数据库中间件 Sharding-JDBC 源码分析 —— SQL 解析之词法解析_第1张图片

子 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 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 解析,尽请关注!

你可能感兴趣的:(数据库中间件 Sharding-JDBC 源码分析 —— SQL 解析之词法解析)