🙂🙂🙂关注微信公众号:【芋艿的后端小屋】有福利:
  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. publicclassLexer{
  3. /**
  4.     * 输出字符串
  5.     * 比如:SQL
  6.     */
  7. @Getter
  8. privatefinalString input;
  9. /**
  10.     * 词法标记字典
  11.     */
  12. privatefinalDictionary dictionary;
  13. /**
  14.     * 解析到 SQL 的 offset
  15.     */
  16. privateint offset;
  17. /**
  18.     * 当前 词法标记
  19.     */
  20. @Getter
  21. privateToken currentToken;
  22. /**
  23.     * 分析下一个词法标记.
  24.     *
  25.     * @see #currentToken
  26.     * @see #offset
  27.     */
  28. publicfinalvoid nextToken(){
  29.        skipIgnoredToken();
  30. if(isVariableBegin()){// 变量
  31.            currentToken =newTokenizer(input, dictionary, offset).scanVariable();
  32. }elseif(isNCharBegin()){// N\
  33.            currentToken =newTokenizer(input, dictionary,++offset).scanChars();
  34. }elseif(isIdentifierBegin()){// Keyword + Literals.IDENTIFIER
  35.            currentToken =newTokenizer(input, dictionary, offset).scanIdentifier();
  36. }elseif(isHexDecimalBegin()){// 十六进制
  37.            currentToken =newTokenizer(input, dictionary, offset).scanHexDecimal();
  38. }elseif(isNumberBegin()){// 数字(整数+浮点数)
  39.            currentToken =newTokenizer(input, dictionary, offset).scanNumber();
  40. }elseif(isSymbolBegin()){// 符号
  41.            currentToken =newTokenizer(input, dictionary, offset).scanSymbol();
  42. }elseif(isCharsBegin()){// 字符串,例如:"abc"
  43.            currentToken =newTokenizer(input, dictionary, offset).scanChars();
  44. }elseif(isEnd()){// 结束
  45.            currentToken =newToken(Assist.END,"", offset);
  46. }else{// 分析错误,无符合条件的词法标记
  47.            currentToken =newToken(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. privatevoid skipIgnoredToken(){
  59. // 空格
  60.        offset =newTokenizer(input, dictionary, offset).skipWhitespace();
  61. // SQL Hint
  62. while(isHintBegin()){
  63.            offset =newTokenizer(input, dictionary, offset).skipHint();
  64.            offset =newTokenizer(input, dictionary, offset).skipWhitespace();
  65. }
  66. // SQL 注释
  67. while(isCommentBegin()){
  68.            offset =newTokenizer(input, dictionary, offset).skipComment();
  69.            offset =newTokenizer(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. publicfinalclassMySQLLexerextendsLexer{
  3. /**
  4.     * 字典
  5.     */
  6. privatestaticDictionary dictionary =newDictionary(MySQLKeyword.values());
  7. publicMySQLLexer(finalString input){
  8. super(input, dictionary);
  9. }
  10. }
  11. // Dictionary.java
  12. publicfinalclassDictionary{
  13. /**
  14.     * 词法关键词Map
  15.     */
  16. privatefinalMap<String,Keyword> tokens =newHashMap<>(1024);
  17. publicDictionary(finalKeyword... dialectKeywords){
  18.        fill(dialectKeywords);
  19. }
  20. /**
  21.     * 装上默认词法关键词 + 方言词法关键词
  22.     * 不同的数据库有相同的默认词法关键词,有有不同的方言关键词
  23.     *
  24.     * @param dialectKeywords 方言词法关键词
  25.     */
  26. privatevoid fill(finalKeyword... 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. privateboolean isIdentifierBegin(){
  3. return isIdentifierBegin(getCurrentChar(0));
  4. }
  5. privateboolean isIdentifierBegin(finalchar ch){
  6. returnCharType.isAlphabet(ch)||'`'== ch ||'_'== ch ||'$'== ch;
  7. }
  8. // Tokenizer.java
  9. /**
  10. * 扫描标识符.
  11. *
  12. * @return 标识符标记
  13. */
  14. publicToken scanIdentifier(){
  15. // `字段`,例如:SELECT `id` FROM t_user 中的 `id`
  16. if('`'== charAt(offset)){
  17. int length = getLengthUntilTerminatedChar('`');
  18. returnnewToken(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. returnnewToken(processAmbiguousIdentifier(offset + length, literals), literals, offset + length);
  28. }
  29. // 从 词法关键词 查找是否是 Keyword,如果是,则返回 Keyword,否则返回 Literals.IDENTIFIER
  30. returnnewToken(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. privateint getLengthUntilTerminatedChar(finalchar terminatedChar){
  40. int length =1;
  41. while(terminatedChar != charAt(offset + length)|| hasEscapeChar(terminatedChar, offset + length)){
  42. if(offset + length >= input.length()){
  43. thrownewUnterminatedCharException(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. privateboolean hasEscapeChar(finalchar charIdentifier,finalint offset){
  60. return charIdentifier == charAt(offset)&& charIdentifier == charAt(offset +1);
  61. }
  62. privateboolean isIdentifierChar(finalchar ch){
  63. returnCharType.isAlphabet(ch)||CharType.isDigital(ch)||'_'== ch ||'$'== ch ||'#'== ch;
  64. }
  65. /**
  66. * 是否是引起歧义的标识符
  67. * 例如 "SELECT * FROM group",此时 "group" 代表的是表名,而非词法关键词
  68. *
  69. * @param literals 标识符
  70. * @return 是否
  71. */
  72. privateboolean isAmbiguousIdentifier(finalString literals){
  73. returnDefaultKeyword.ORDER.name().equalsIgnoreCase(literals)||DefaultKeyword.GROUP.name().equalsIgnoreCase(literals);
  74. }
  75. /**
  76. * 获取引起歧义的标识符对应的词法标记类型
  77. *
  78. * @param offset 位置
  79. * @param literals 标识符
  80. * @return 词法标记类型
  81. */
  82. privateTokenType processAmbiguousIdentifier(finalint offset,finalString literals){
  83. int i =0;
  84. while(CharType.isWhitespace(charAt(offset + i))){
  85.       i++;
  86. }
  87. if(DefaultKeyword.BY.name().equalsIgnoreCase(String.valueOf(newchar[]{charAt(offset + i), charAt(offset + i +1)}))){
  88. return dictionary.findTokenType(literals);
  89. }
  90. returnLiterals.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. protectedboolean isVariableBegin(){
  10. returnfalse;
  11. }
  12. // Tokenizer.java
  13. /**
  14. * 扫描变量.
  15. * 在 MySQL 里,@代表用户变量;@@代表系统变量。
  16. * 在 SQLServer 里,有 @@。
  17. *
  18. * @return 变量标记
  19. */
  20. publicToken scanVariable(){
  21. int length =1;
  22. if('@'== charAt(offset +1)){
  23.       length++;
  24. }
  25. while(isVariableChar(charAt(offset + length))){
  26.       length++;
  27. }
  28. returnnewToken(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. privateboolean isNCharBegin(){
  10. return isSupportNChars()&&'N'== getCurrentChar(0)&&'\''== getCurrentChar(1);
  11. }
  12. privateboolean isCharsBegin(){
  13. return'\''== getCurrentChar(0)||'\"'== getCurrentChar(0);
  14. }
  15. // Tokenizer.java
  16. /**
  17. * 扫描字符串.
  18. *
  19. * @return 字符串标记
  20. */
  21. publicToken scanChars(){
  22. return scanChars(charAt(offset));
  23. }
  24. privateToken scanChars(finalchar terminatedChar){
  25. int length = getLengthUntilTerminatedChar(terminatedChar);
  26. returnnewToken(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. privateboolean isHexDecimalBegin(){
  9. return'0'== getCurrentChar(0)&&'x'== getCurrentChar(1);
  10. }
  11. // Tokenizer.java
  12. /**
  13. * 扫描十六进制数.
  14. *
  15. * @return 十六进制数标记
  16. */
  17. publicToken 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. returnnewToken(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. privateboolean isNumberBegin(){
  10. returnCharType.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. publicToken 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. returnnewToken(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. privateboolean isSymbolBegin(){
  9. returnCharType.isSymbol(getCurrentChar(0));
  10. }
  11. // CharType.java
  12. /**
  13. * 判断是否为符号.
  14. *
  15. * @param ch 待判断的字符
  16. * @return 是否为符号
  17. */
  18. publicstaticboolean isSymbol(finalchar 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. publicToken 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. returnnewToken(symbol, literals, offset + length);
  40. }

3.4 Assist 词法辅助标记

Assist 词法辅助标记,一共分成 2 种:
  • END :分析结束
  • ERROR :分析错误。

4. 彩蛋

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

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

我创建了一个微信群【源码圈】,希望和大家分享交流读源码的经验。

读源码先难后易,掌握方法后,可以做更有深度的学习。

而且掌握方法并不难噢。

加群方式:微信公众号发送关键字【qun】。
继续阅读
阅读原文