查看原文
其他

数据库中间件 Sharding-JDBC 源码分析 —— SQL 解析(一)之语法解析

2017-07-23 王文斌(芋艿) 芋道源码

🙂🙂🙂关注微信公众号:【芋艿的后端小屋】有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表

  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址

  3. 您对于源码的疑问每条留言将得到认真回复。甚至不知道如何读源码也可以请教噢!

  4. 新的源码解析文章实时收到通知。每周更新一篇左右


  • 1. 概述

  • 2. Lexer 词法解析器

  • 3. Token 词法标记

    • 3.2.1 Literals.IDENTIFIER 词法关键词

    • 3.2.2 Literals.VARIABLE 变量

    • 3.2.3 Literals.CHARS 字符串

    • 3.2.4 Literals.HEX 十六进制

    • 3.2.5 Literals.INT 整数

    • 3.2.6 Literals.FLOAT 浮点数

    • 3.1 DefaultKeyword 词法关键词

    • 3.2 Literals 词法字面量标记

    • 3.3 Symbol 词法符号标记

    • 3.4 Assist 词法辅助标记

  • 4. 彩蛋


1. 概述

SQL 解析引擎,数据库中间件必备的功能和流程。Sharding-JDBC 在 1.5.0.M1 正式发布时,将 SQL 解析引擎从 Druid 替换成了自研的。新引擎仅解析分片上下文,对于 SQL 采用"半理解"理念,进一步提升性能和兼容性,同时降低了代码复杂度(不理解没关系,我们后续会更新文章解释该优点)。 国内另一款数据库中间件 MyCAT SQL 解析引擎也是 Druid,目前也在开发属于自己的 SQL 解析引擎。

可能有同学看到SQL 解析会被吓到,请淡定,耐心往下看。《SQL 解析》内容我们会分成 5 篇相对简短的文章,让大家能够相对轻松愉快的去理解:

  1. 词法解析

  2. 插入 SQL 解析

  3. 查询 SQL 解析

  4. 更新 SQL 解析

  5. 删除 SQL 解析


SQL 解析引擎parsing 包下,如上图所见包含两大组件:

  1. Lexer:词法解析器。

  2. Parser:SQL解析器。

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

  1. SQL :SELECT * FROM t_user  

  2. Lexer :[SELECT] [ * ] [FROM] [t_user]  

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

🙂不完全懂?没关系,本文的主角是 Lexer,我们通过源码一点一点理解。一共 1400 行左右代码左右,还包含注释等等,实际更少噢。

2. Lexer 词法解析器

Lexer 原理顺序顺序顺序 解析 SQL,将字符串拆解成 N 个词法。

核心代码如下:

  1. // Lexer.java

  2. public class Lexer {

  3.    /**

  4.     * 输出字符串

  5.     * 比如:SQL

  6.     */

  7.    @Getter

  8.    private final String input;

  9.    /**

  10.     * 词法标记字典

  11.     */

  12.    private final Dictionary dictionary;

  13.    /**

  14.     * 解析到 SQL 的 offset

  15.     */

  16.    private int offset;

  17.    /**

  18.     * 当前 词法标记

  19.     */

  20.    @Getter

  21.    private Token currentToken;

  22.    /**

  23.     * 分析下一个词法标记.

  24.     *

  25.     * @see #currentToken

  26.     * @see #offset

  27.     */

  28.    public final void nextToken() {

  29.        skipIgnoredToken();

  30.        if (isVariableBegin()) { // 变量

  31.            currentToken = new Tokenizer(input, dictionary, offset).scanVariable();

  32.        } else if (isNCharBegin()) { // N\

  33.            currentToken = new Tokenizer(input, dictionary, ++offset).scanChars();

  34.        } else if (isIdentifierBegin()) { // Keyword + Literals.IDENTIFIER

  35.            currentToken = new Tokenizer(input, dictionary, offset).scanIdentifier();

  36.        } else if (isHexDecimalBegin()) { // 十六进制

  37.            currentToken = new Tokenizer(input, dictionary, offset).scanHexDecimal();

  38.        } else if (isNumberBegin()) { // 数字(整数+浮点数)

  39.            currentToken = new Tokenizer(input, dictionary, offset).scanNumber();

  40.        } else if (isSymbolBegin()) { // 符号

  41.            currentToken = new Tokenizer(input, dictionary, offset).scanSymbol();

  42.        } else if (isCharsBegin()) { // 字符串,例如:"abc"

  43.            currentToken = new Tokenizer(input, dictionary, offset).scanChars();

  44.        } else if (isEnd()) { // 结束

  45.            currentToken = new Token(Assist.END, "", offset);

  46.        } else { // 分析错误,无符合条件的词法标记

  47.            currentToken = new Token(Assist.ERROR, "", offset);

  48.        }

  49.        offset = currentToken.getEndPosition();

  50.        // System.out.println("| " + currentToken.getLiterals() + " | " + currentToken.getType() + " | " + currentToken.getEndPosition() + " |");

  51.    }

  52.    /**

  53.     * 跳过忽略的词法标记

  54.     * 1. 空格

  55.     * 2. SQL Hint

  56.     * 3. SQL 注释

  57.     */

  58.    private void skipIgnoredToken() {

  59.        // 空格

  60.        offset = new Tokenizer(input, dictionary, offset).skipWhitespace();

  61.        // SQL Hint

  62.        while (isHintBegin()) {

  63.            offset = new Tokenizer(input, dictionary, offset).skipHint();

  64.            offset = new Tokenizer(input, dictionary, offset).skipWhitespace();

  65.        }

  66.        // SQL 注释

  67.        while (isCommentBegin()) {

  68.            offset = new Tokenizer(input, dictionary, offset).skipComment();

  69.            offset = new Tokenizer(input, dictionary, offset).skipWhitespace();

  70.        }

  71.    }

  72. }

通过 #nextToken() 方法,不断解析出 Token(词法标记)。我们来执行一次,看看 SQL 会被拆解成哪些 Token。

  1. 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=?

literalsTokenType类TokenType值endPosition
SELECTDefaultKeywordSELECT6
iLiteralsIDENTIFIER8
.SymbolDOT9
*SymbolSTAR10
FROMDefaultKeywordFROM15
t_orderLiteralsIDENTIFIER23
oLiteralsIDENTIFIER25
JOINDefaultKeywordJOIN30
torderitemLiteralsIDENTIFIER43
iLiteralsIDENTIFIER45
ONDefaultKeywordON48
oLiteralsIDENTIFIER50
.SymbolDOT51
order_idLiteralsIDENTIFIER59
=SymbolEQ60
iLiteralsIDENTIFIER61
.SymbolDOT62
order_idLiteralsIDENTIFIER70
WHEREDefaultKeywordWHERE76
oLiteralsIDENTIFIER78
.SymbolDOT79
user_idLiteralsIDENTIFIER86
=SymbolEQ87
?SymbolQUESTION88
ANDDefaultKeywordAND92
oLiteralsIDENTIFIER94
.SymbolDOT95
order_idLiteralsIDENTIFIER103
=SymbolEQ104
?SymbolQUESTION105

AssistEND105

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

我们来总结下, Lexer#nextToken() 方法里,使用 #skipIgnoredToken() 方法跳过忽略的 Token,通过 #isXXXX() 方法判断好下一个 Token 的类型后,交给 Tokenizer 进行分词返回 Token。‼️此处可以考虑做个优化,不需要每次都 newTokenizer(...) 出来,一个 Lexer 搭配一个 Tokenizer。


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

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

3. Token 词法标记

上文我们已经看过 Token 的例子,一共有 3 个属性:

  • TokenType type :词法标记类型

  • String literals :词法字面量标记

  • int endPosition : literals 在 SQL 里的结束位置

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

  • DefaultKeyword :词法关键词

  • Literals :词法字面量标记

  • Symbol :词法符号标记

  • Assist :词法辅助标记

3.1 DefaultKeyword 词法关键词

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

我们以 MySQL 举个例子,当创建 MySQLLexer 时,会加载 DefaultKeyword 和 MySQLKeyword( OracleLexer、PostgreSQLLexer、SQLServerLexer 同 MySQLLexer )。核心代码如下:

  1. // MySQLLexer.java

  2. public final class MySQLLexer extends Lexer {

  3.    /**

  4.     * 字典

  5.     */

  6.    private static Dictionary dictionary = new Dictionary(MySQLKeyword.values());

  7.    public MySQLLexer(final String input) {

  8.        super(input, dictionary);

  9.    }

  10. }

  11. // Dictionary.java

  12. public final class Dictionary {

  13.    /**

  14.     * 词法关键词Map

  15.     */

  16.    private final Map<String, Keyword> tokens = new HashMap<>(1024);

  17.    public Dictionary(final Keyword... dialectKeywords) {

  18.        fill(dialectKeywords);

  19.    }

  20.    /**

  21.     * 装上默认词法关键词 + 方言词法关键词

  22.     * 不同的数据库有相同的默认词法关键词,有有不同的方言关键词

  23.     *

  24.     * @param dialectKeywords 方言词法关键词

  25.     */

  26.    private void fill(final Keyword... dialectKeywords) {

  27.        for (DefaultKeyword each : DefaultKeyword.values()) {

  28.            tokens.put(each.name(), each);

  29.        }

  30.        for (Keyword each : dialectKeywords) {

  31.            tokens.put(each.toString(), each);

  32.        }

  33.    }

  34. }

Keyword 与 Literals.IDENTIFIER 是一起解析的,我们放在 Literals.IDENTIFIER 处一起分析。

3.2 Literals 词法字面量标记

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

  • IDENTIFIER :词法关键词

  • VARIABLE :变量

  • CHARS :字符串

  • HEX :十六进制

  • INT :整数

  • FLOAT :浮点数

3.2.1 Literals.IDENTIFIER 词法关键词

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

解析 Literals.IDENTIFIER 与 Keyword 核心代码如下:

  1. // Lexer.java

  2. private boolean isIdentifierBegin() {

  3.   return isIdentifierBegin(getCurrentChar(0));

  4. }

  5. private boolean isIdentifierBegin(final char ch) {

  6.   return CharType.isAlphabet(ch) || '`' == ch || '_' == ch || '$' == ch;

  7. }

  8. // Tokenizer.java

  9. /**

  10. * 扫描标识符.

  11. *

  12. * @return 标识符标记

  13. */

  14. public Token scanIdentifier() {

  15.   // `字段`,例如:SELECT `id` FROM t_user 中的 `id`

  16.   if ('`' == charAt(offset)) {

  17.       int length = getLengthUntilTerminatedChar('`');

  18.       return new Token(Literals.IDENTIFIER, input.substring(offset, offset + length), offset + length);

  19.   }

  20.   int length = 0;

  21.   while (isIdentifierChar(charAt(offset + length))) {

  22.       length++;

  23.   }

  24.   String literals = input.substring(offset, offset + length);

  25.   // 处理 order / group 作为表名

  26.   if (isAmbiguousIdentifier(literals)) {

  27.       return new Token(processAmbiguousIdentifier(offset + length, literals), literals, offset + length);

  28.   }

  29.   // 从 词法关键词 查找是否是 Keyword,如果是,则返回 Keyword,否则返回 Literals.IDENTIFIER

  30.   return new Token(dictionary.findTokenType(literals, Literals.IDENTIFIER), literals, offset + length);

  31. }

  32. /**

  33. * 计算到结束字符的长度

  34. *

  35. * @see #hasEscapeChar(char, int) 处理类似 SELECT a AS `b``c` FROM table。此处连续的 "``" 不是结尾,如果传递的是 "`" 会产生误判,所以加了这个判断

  36. * @param terminatedChar 结束字符

  37. * @return 长度

  38. */

  39. private int getLengthUntilTerminatedChar(final char terminatedChar) {

  40.   int length = 1;

  41.   while (terminatedChar != charAt(offset + length) || hasEscapeChar(terminatedChar, offset + length)) {

  42.       if (offset + length >= input.length()) {

  43.           throw new UnterminatedCharException(terminatedChar);

  44.       }

  45.       if (hasEscapeChar(terminatedChar, offset + length)) {

  46.           length++;

  47.       }

  48.       length++;

  49.   }

  50.   return length + 1;

  51. }

  52. /**

  53. * 是否是 Escape 字符

  54. *

  55. * @param charIdentifier 字符

  56. * @param offset 位置

  57. * @return 是否

  58. */

  59. private boolean hasEscapeChar(final char charIdentifier, final int offset) {

  60.   return charIdentifier == charAt(offset) && charIdentifier == charAt(offset + 1);

  61. }

  62. private boolean isIdentifierChar(final char ch) {

  63.   return CharType.isAlphabet(ch) || CharType.isDigital(ch) || '_' == ch || '$' == ch || '#' == ch;

  64. }

  65. /**

  66. * 是否是引起歧义的标识符

  67. * 例如 "SELECT * FROM group",此时 "group" 代表的是表名,而非词法关键词

  68. *

  69. * @param literals 标识符

  70. * @return 是否

  71. */

  72. private boolean isAmbiguousIdentifier(final String literals) {

  73.   return DefaultKeyword.ORDER.name().equalsIgnoreCase(literals) || DefaultKeyword.GROUP.name().equalsIgnoreCase(literals);

  74. }

  75. /**

  76. * 获取引起歧义的标识符对应的词法标记类型

  77. *

  78. * @param offset 位置

  79. * @param literals 标识符

  80. * @return 词法标记类型

  81. */

  82. private TokenType processAmbiguousIdentifier(final int offset, final String literals) {

  83.   int i = 0;

  84.   while (CharType.isWhitespace(charAt(offset + i))) {

  85.       i++;

  86.   }

  87.   if (DefaultKeyword.BY.name().equalsIgnoreCase(String.valueOf(new char[] {charAt(offset + i), charAt(offset + i + 1)}))) {

  88.       return dictionary.findTokenType(literals);

  89.   }

  90.   return Literals.IDENTIFIER;

  91. }

3.2.2 Literals.VARIABLE 变量

变量。例如: SELECT@@VERSION

解析核心代码如下:

  1. // Lexer.java

  2. /**

  3. * 是否是 变量

  4. * MySQL 与 SQL Server 支持

  5. *

  6. * @see Tokenizer#scanVariable()

  7. * @return 是否

  8. */

  9. protected boolean isVariableBegin() {

  10.   return false;

  11. }

  12. // Tokenizer.java

  13. /**

  14. * 扫描变量.

  15. * 在 MySQL 里,@代表用户变量;@@代表系统变量。

  16. * 在 SQLServer 里,有 @@。

  17. *

  18. * @return 变量标记

  19. */

  20. public Token scanVariable() {

  21.   int length = 1;

  22.   if ('@' == charAt(offset + 1)) {

  23.       length++;

  24.   }

  25.   while (isVariableChar(charAt(offset + length))) {

  26.       length++;

  27.   }

  28.   return new Token(Literals.VARIABLE, input.substring(offset, offset + length), offset + length);

  29. }

3.2.3 Literals.CHARS 字符串

字符串。例如: SELECT"123"

解析核心代码如下:

  1. // Lexer.java

  2. /**

  3. * 是否 N\

  4. * 目前 SQLServer 独有:在 SQL Server 中處理 Unicode 字串常數時,必需為所有的 Unicode 字串加上前置詞 N

  5. *

  6. * @see Tokenizer#scanChars()

  7. * @return 是否

  8. */

  9. private boolean isNCharBegin() {

  10.   return isSupportNChars() && 'N' == getCurrentChar(0) && '\'' == getCurrentChar(1);

  11. }

  12. private boolean isCharsBegin() {

  13.   return '\'' == getCurrentChar(0) || '\"' == getCurrentChar(0);

  14. }

  15. // Tokenizer.java

  16. /**

  17. * 扫描字符串.

  18. *

  19. * @return 字符串标记

  20. */

  21. public Token scanChars() {

  22.   return scanChars(charAt(offset));

  23. }

  24. private Token scanChars(final char terminatedChar) {

  25.   int length = getLengthUntilTerminatedChar(terminatedChar);

  26.   return new Token(Literals.CHARS, input.substring(offset + 1, offset + length - 1), offset + length);

  27. }

3.2.4 Literals.HEX 十六进制

  1. // Lexer.java

  2. /**

  3. * 是否是 十六进制

  4. *

  5. * @see Tokenizer#scanHexDecimal()

  6. * @return 是否

  7. */

  8. private boolean isHexDecimalBegin() {

  9.   return '0' == getCurrentChar(0) && 'x' == getCurrentChar(1);

  10. }

  11. // Tokenizer.java

  12. /**

  13. * 扫描十六进制数.

  14. *

  15. * @return 十六进制数标记

  16. */

  17. public Token scanHexDecimal() {

  18.   int length = HEX_BEGIN_SYMBOL_LENGTH;

  19.   // 负数

  20.   if ('-' == charAt(offset + length)) {

  21.       length++;

  22.   }

  23.   while (isHex(charAt(offset + length))) {

  24.       length++;

  25.   }

  26.   return new Token(Literals.HEX, input.substring(offset, offset + length), offset + length);

  27. }

3.2.5 Literals.INT 整数

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

Literals.INT 与 Literals.FLOAT 是一起解析的,我们放在 Literals.FLOAT 处一起分析。

3.2.6 Literals.FLOAT 浮点数

浮点数。例如: SELECT*FROM t_user WHERE id=1.0。 浮点数包含几种:"1.0","1.0F","7.823E5"(科学计数法)。

解析核心代码如下:

  1. // Lexer.java

  2. /**

  3. * 是否是 数字

  4. * '-' 需要特殊处理。".2" 被处理成省略0的小数,"-.2" 不能被处理成省略的小数,否则会出问题。

  5. * 例如说,"SELECT a-.2" 处理的结果是 "SELECT" / "a" / "-" / ".2"

  6. *

  7. * @return 是否

  8. */

  9. private boolean isNumberBegin() {

  10.   return CharType.isDigital(getCurrentChar(0)) // 数字

  11.           || ('.' == getCurrentChar(0) && CharType.isDigital(getCurrentChar(1)) && !isIdentifierBegin(getCurrentChar(-1)) // 浮点数

  12.           || ('-' == getCurrentChar(0) && ('.' == getCurrentChar(0) || CharType.isDigital(getCurrentChar(1))))); // 负数

  13. }

  14. // Tokenizer.java

  15. /**

  16. * 扫描数字.

  17. * 解析数字的结果会有两种:整数 和 浮点数.

  18. *

  19. * @return 数字标记

  20. */

  21. public Token scanNumber() {

  22.   int length = 0;

  23.   // 负数

  24.   if ('-' == charAt(offset + length)) {

  25.       length++;

  26.   }

  27.   // 浮点数

  28.   length += getDigitalLength(offset + length);

  29.   boolean isFloat = false;

  30.   if ('.' == charAt(offset + length)) {

  31.       isFloat = true;

  32.       length++;

  33.       length += getDigitalLength(offset + length);

  34.   }

  35.   // 科学计数表示,例如:SELECT 7.823E5

  36.   if (isScientificNotation(offset + length)) {

  37.       isFloat = true;

  38.       length++;

  39.       if ('+' == charAt(offset + length) || '-' == charAt(offset + length)) {

  40.           length++;

  41.       }

  42.       length += getDigitalLength(offset + length);

  43.   }

  44.   // 浮点数,例如:SELECT 1.333F

  45.   if (isBinaryNumber(offset + length)) {

  46.       isFloat = true;

  47.       length++;

  48.   }

  49.   return new Token(isFloat ? Literals.FLOAT : Literals.INT, input.substring(offset, offset + length), offset + length);

  50. }

这里要特别注意下:"-"。在数字表达实例,可以判定为 负号 和 减号(不考虑科学计数法)。

  • ".2" 解析结果是 ".2"

  • "-.2" 解析结果不能是 "-.2",而是 "-" 和 ".2"。

3.3 Symbol 词法符号标记

词法符号标记。例如:"{", "}", ">=" 等等。

解析核心代码如下:

  1. // Lexer.java

  2. /**

  3. * 是否是 符号

  4. *

  5. * @see Tokenizer#scanSymbol()

  6. * @return 是否

  7. */

  8. private boolean isSymbolBegin() {

  9.   return CharType.isSymbol(getCurrentChar(0));

  10. }

  11. // CharType.java

  12. /**

  13. * 判断是否为符号.

  14. *

  15. * @param ch 待判断的字符

  16. * @return 是否为符号

  17. */

  18. public static boolean isSymbol(final char ch) {

  19.   return '(' == ch || ')' == ch || '[' == ch || ']' == ch || '{' == ch || '}' == ch || '+' == ch || '-' == ch || '*' == ch || '/' == ch || '%' == ch || '^' == ch || '=' == ch

  20.           || '>' == ch || '<' == ch || '~' == ch || '!' == ch || '?' == ch || '&' == ch || '|' == ch || '.' == ch || ':' == ch || '#' == ch || ',' == ch || ';' == ch;

  21. }

  22. // Tokenizer.java

  23. /**

  24. * 扫描符号.

  25. *

  26. * @return 符号标记

  27. */

  28. public Token scanSymbol() {

  29.   int length = 0;

  30.   while (CharType.isSymbol(charAt(offset + length))) {

  31.       length++;

  32.   }

  33.   String literals = input.substring(offset, offset + length);

  34.   // 倒序遍历,查询符合条件的 符号。例如 literals = ";;",会是拆分成两个 ";"。如果基于正序,literals = "<=",会被解析成 "<" + "="。

  35.   Symbol symbol;

  36.   while (null == (symbol = Symbol.literalsOf(literals))) {

  37.       literals = input.substring(offset, offset + --length);

  38.   }

  39.   return new Token(symbol, literals, offset + length);

  40. }

3.4 Assist 词法辅助标记

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

  • END :分析结束

  • ERROR :分析错误。

4. 彩蛋

老铁,是不是比想象中简单一些?!继续加油写 Parser 相关的文章!来一波微信公众号关注吧。


Sharding-JDBC 正在收集使用公司名单:传送门。🙂 你的登记,会让更多人参与和使用 Sharding-JDBC。Sharding-JDBC 也会因此,能够覆盖更广的场景。登记吧,少年!


我创建了一个微信群【源码圈】,希望和大家分享交流读源码的经验。
读源码先难后易,掌握方法后,可以做更有深度的学习。
而且掌握方法并不难噢。
加群方式:微信公众号发送关键字【qun】。


    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存