数据库中间件 Sharding-JDBC 源码分析 —— SQL 解析(一)之语法解析
🙂🙂🙂关注微信公众号:【芋艿的后端小屋】有福利:
RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表 RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址 您对于源码的疑问每条留言都将得到认真回复。甚至不知道如何读源码也可以请教噢! 新的源码解析文章实时收到通知。每周更新一篇左右。
- 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 篇相对简短的文章,让大家能够相对轻松愉快的去理解:
- 词法解析
- 插入 SQL 解析
- 查询 SQL 解析
- 更新 SQL 解析
- 删除 SQL 解析
SQL 解析引擎在
parsing
包下,如上图所见包含两大组件:- Lexer:词法解析器。
- Parser:SQL解析器。
两者都是解析器,区别在于 Lexer 只做词法的解析,不关注上下文,将字符串拆解成 N 个词法。而 Parser 在 Lexer 的基础上,还需要理解 SQL 。打个比方:
SQL :SELECT * FROM t_user
Lexer:[SELECT][*][FROM][t_user]
Parser:这是一条[SELECT]查询表为[t_user],并且返回[*]所有字段的 SQL。
🙂不完全懂?没关系,本文的主角是 Lexer,我们通过源码一点一点理解。一共 1400 行左右代码左右,还包含注释等等,实际更少噢。
2. Lexer 词法解析器
Lexer 原理:顺序顺序顺序 解析 SQL,将字符串拆解成 N 个词法。
核心代码如下:
// Lexer.java
publicclassLexer{
/**
* 输出字符串
* 比如:SQL
*/
@Getter
privatefinalString input;
/**
* 词法标记字典
*/
privatefinalDictionary dictionary;
/**
* 解析到 SQL 的 offset
*/
privateint offset;
/**
* 当前 词法标记
*/
@Getter
privateToken currentToken;
/**
* 分析下一个词法标记.
*
* @see #currentToken
* @see #offset
*/
publicfinalvoid nextToken(){
skipIgnoredToken();
if(isVariableBegin()){// 变量
currentToken =newTokenizer(input, dictionary, offset).scanVariable();
}elseif(isNCharBegin()){// N\
currentToken =newTokenizer(input, dictionary,++offset).scanChars();
}elseif(isIdentifierBegin()){// Keyword + Literals.IDENTIFIER
currentToken =newTokenizer(input, dictionary, offset).scanIdentifier();
}elseif(isHexDecimalBegin()){// 十六进制
currentToken =newTokenizer(input, dictionary, offset).scanHexDecimal();
}elseif(isNumberBegin()){// 数字(整数+浮点数)
currentToken =newTokenizer(input, dictionary, offset).scanNumber();
}elseif(isSymbolBegin()){// 符号
currentToken =newTokenizer(input, dictionary, offset).scanSymbol();
}elseif(isCharsBegin()){// 字符串,例如:"abc"
currentToken =newTokenizer(input, dictionary, offset).scanChars();
}elseif(isEnd()){// 结束
currentToken =newToken(Assist.END,"", offset);
}else{// 分析错误,无符合条件的词法标记
currentToken =newToken(Assist.ERROR,"", offset);
}
offset = currentToken.getEndPosition();
// System.out.println("| " + currentToken.getLiterals() + " | " + currentToken.getType() + " | " + currentToken.getEndPosition() + " |");
}
/**
* 跳过忽略的词法标记
* 1. 空格
* 2. SQL Hint
* 3. SQL 注释
*/
privatevoid skipIgnoredToken(){
// 空格
offset =newTokenizer(input, dictionary, offset).skipWhitespace();
// SQL Hint
while(isHintBegin()){
offset =newTokenizer(input, dictionary, offset).skipHint();
offset =newTokenizer(input, dictionary, offset).skipWhitespace();
}
// SQL 注释
while(isCommentBegin()){
offset =newTokenizer(input, dictionary, offset).skipComment();
offset =newTokenizer(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 |
torderitem | 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。‼️此处可以考虑做个优化,不需要每次都 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 )。核心代码如下:
// MySQLLexer.java
publicfinalclassMySQLLexerextendsLexer{
/**
* 字典
*/
privatestaticDictionary dictionary =newDictionary(MySQLKeyword.values());
publicMySQLLexer(finalString input){
super(input, dictionary);
}
}
// Dictionary.java
publicfinalclassDictionary{
/**
* 词法关键词Map
*/
privatefinalMap<String,Keyword> tokens =newHashMap<>(1024);
publicDictionary(finalKeyword... dialectKeywords){
fill(dialectKeywords);
}
/**
* 装上默认词法关键词 + 方言词法关键词
* 不同的数据库有相同的默认词法关键词,有有不同的方言关键词
*
* @param dialectKeywords 方言词法关键词
*/
privatevoid fill(finalKeyword... dialectKeywords){
for(DefaultKeyword each :DefaultKeyword.values()){
tokens.put(each.name(), each);
}
for(Keyword each : dialectKeywords){
tokens.put(each.toString(), each);
}
}
}
Keyword 与 Literals.IDENTIFIER 是一起解析的,我们放在 Literals.IDENTIFIER 处一起分析。
3.2 Literals 词法字面量标记
Literals 词法字面量标记,一共分成 6 种:
- IDENTIFIER :词法关键词
- VARIABLE :变量
- CHARS :字符串
- HEX :十六进制
- INT :整数
- FLOAT :浮点数
3.2.1 Literals.IDENTIFIER 词法关键词
词法关键词。例如:表名,查询字段 等等。
解析 Literals.IDENTIFIER 与 Keyword 核心代码如下:
// Lexer.java
privateboolean isIdentifierBegin(){
return isIdentifierBegin(getCurrentChar(0));
}
privateboolean isIdentifierBegin(finalchar ch){
returnCharType.isAlphabet(ch)||'`'== ch ||'_'== ch ||'$'== ch;
}
// Tokenizer.java
/**
* 扫描标识符.
*
* @return 标识符标记
*/
publicToken scanIdentifier(){
// `字段`,例如:SELECT `id` FROM t_user 中的 `id`
if('`'== charAt(offset)){
int length = getLengthUntilTerminatedChar('`');
returnnewToken(Literals.IDENTIFIER, input.substring(offset, offset + length), offset + length);
}
int length =0;
while(isIdentifierChar(charAt(offset + length))){
length++;
}
String literals = input.substring(offset, offset + length);
// 处理 order / group 作为表名
if(isAmbiguousIdentifier(literals)){
returnnewToken(processAmbiguousIdentifier(offset + length, literals), literals, offset + length);
}
// 从 词法关键词 查找是否是 Keyword,如果是,则返回 Keyword,否则返回 Literals.IDENTIFIER
returnnewToken(dictionary.findTokenType(literals,Literals.IDENTIFIER), literals, offset + length);
}
/**
* 计算到结束字符的长度
*
* @see #hasEscapeChar(char, int) 处理类似 SELECT a AS `b``c` FROM table。此处连续的 "``" 不是结尾,如果传递的是 "`" 会产生误判,所以加了这个判断
* @param terminatedChar 结束字符
* @return 长度
*/
privateint getLengthUntilTerminatedChar(finalchar terminatedChar){
int length =1;
while(terminatedChar != charAt(offset + length)|| hasEscapeChar(terminatedChar, offset + length)){
if(offset + length >= input.length()){
thrownewUnterminatedCharException(terminatedChar);
}
if(hasEscapeChar(terminatedChar, offset + length)){
length++;
}
length++;
}
return length +1;
}
/**
* 是否是 Escape 字符
*
* @param charIdentifier 字符
* @param offset 位置
* @return 是否
*/
privateboolean hasEscapeChar(finalchar charIdentifier,finalint offset){
return charIdentifier == charAt(offset)&& charIdentifier == charAt(offset +1);
}
privateboolean isIdentifierChar(finalchar ch){
returnCharType.isAlphabet(ch)||CharType.isDigital(ch)||'_'== ch ||'$'== ch ||'#'== ch;
}
/**
* 是否是引起歧义的标识符
* 例如 "SELECT * FROM group",此时 "group" 代表的是表名,而非词法关键词
*
* @param literals 标识符
* @return 是否
*/
privateboolean isAmbiguousIdentifier(finalString literals){
returnDefaultKeyword.ORDER.name().equalsIgnoreCase(literals)||DefaultKeyword.GROUP.name().equalsIgnoreCase(literals);
}
/**
* 获取引起歧义的标识符对应的词法标记类型
*
* @param offset 位置
* @param literals 标识符
* @return 词法标记类型
*/
privateTokenType processAmbiguousIdentifier(finalint offset,finalString literals){
int i =0;
while(CharType.isWhitespace(charAt(offset + i))){
i++;
}
if(DefaultKeyword.BY.name().equalsIgnoreCase(String.valueOf(newchar[]{charAt(offset + i), charAt(offset + i +1)}))){
return dictionary.findTokenType(literals);
}
returnLiterals.IDENTIFIER;
}
3.2.2 Literals.VARIABLE 变量
变量。例如:
SELECT@@VERSION
。解析核心代码如下:
// Lexer.java
/**
* 是否是 变量
* MySQL 与 SQL Server 支持
*
* @see Tokenizer#scanVariable()
* @return 是否
*/
protectedboolean isVariableBegin(){
returnfalse;
}
// Tokenizer.java
/**
* 扫描变量.
* 在 MySQL 里,@代表用户变量;@@代表系统变量。
* 在 SQLServer 里,有 @@。
*
* @return 变量标记
*/
publicToken scanVariable(){
int length =1;
if('@'== charAt(offset +1)){
length++;
}
while(isVariableChar(charAt(offset + length))){
length++;
}
returnnewToken(Literals.VARIABLE, input.substring(offset, offset + length), offset + length);
}
3.2.3 Literals.CHARS 字符串
字符串。例如:
SELECT"123"
。解析核心代码如下:
// Lexer.java
/**
* 是否 N\
* 目前 SQLServer 独有:在 SQL Server 中處理 Unicode 字串常數時,必需為所有的 Unicode 字串加上前置詞 N
*
* @see Tokenizer#scanChars()
* @return 是否
*/
privateboolean isNCharBegin(){
return isSupportNChars()&&'N'== getCurrentChar(0)&&'\''== getCurrentChar(1);
}
privateboolean isCharsBegin(){
return'\''== getCurrentChar(0)||'\"'== getCurrentChar(0);
}
// Tokenizer.java
/**
* 扫描字符串.
*
* @return 字符串标记
*/
publicToken scanChars(){
return scanChars(charAt(offset));
}
privateToken scanChars(finalchar terminatedChar){
int length = getLengthUntilTerminatedChar(terminatedChar);
returnnewToken(Literals.CHARS, input.substring(offset +1, offset + length -1), offset + length);
}
3.2.4 Literals.HEX 十六进制
// Lexer.java
/**
* 是否是 十六进制
*
* @see Tokenizer#scanHexDecimal()
* @return 是否
*/
privateboolean isHexDecimalBegin(){
return'0'== getCurrentChar(0)&&'x'== getCurrentChar(1);
}
// Tokenizer.java
/**
* 扫描十六进制数.
*
* @return 十六进制数标记
*/
publicToken scanHexDecimal(){
int length = HEX_BEGIN_SYMBOL_LENGTH;
// 负数
if('-'== charAt(offset + length)){
length++;
}
while(isHex(charAt(offset + length))){
length++;
}
returnnewToken(Literals.HEX, input.substring(offset, offset + length), offset + length);
}
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"(科学计数法)。解析核心代码如下:
// Lexer.java
/**
* 是否是 数字
* '-' 需要特殊处理。".2" 被处理成省略0的小数,"-.2" 不能被处理成省略的小数,否则会出问题。
* 例如说,"SELECT a-.2" 处理的结果是 "SELECT" / "a" / "-" / ".2"
*
* @return 是否
*/
privateboolean isNumberBegin(){
returnCharType.isDigital(getCurrentChar(0))// 数字
||('.'== getCurrentChar(0)&&CharType.isDigital(getCurrentChar(1))&&!isIdentifierBegin(getCurrentChar(-1))// 浮点数
||('-'== getCurrentChar(0)&&('.'== getCurrentChar(0)||CharType.isDigital(getCurrentChar(1)))));// 负数
}
// Tokenizer.java
/**
* 扫描数字.
* 解析数字的结果会有两种:整数 和 浮点数.
*
* @return 数字标记
*/
publicToken scanNumber(){
int length =0;
// 负数
if('-'== charAt(offset + length)){
length++;
}
// 浮点数
length += getDigitalLength(offset + length);
boolean isFloat =false;
if('.'== charAt(offset + length)){
isFloat =true;
length++;
length += getDigitalLength(offset + length);
}
// 科学计数表示,例如:SELECT 7.823E5
if(isScientificNotation(offset + length)){
isFloat =true;
length++;
if('+'== charAt(offset + length)||'-'== charAt(offset + length)){
length++;
}
length += getDigitalLength(offset + length);
}
// 浮点数,例如:SELECT 1.333F
if(isBinaryNumber(offset + length)){
isFloat =true;
length++;
}
returnnewToken(isFloat ?Literals.FLOAT :Literals.INT, input.substring(offset, offset + length), offset + length);
}
这里要特别注意下:"-"。在数字表达实例,可以判定为 负号 和 减号(不考虑科学计数法)。
- ".2" 解析结果是 ".2"
- "-.2" 解析结果不能是 "-.2",而是 "-" 和 ".2"。
3.3 Symbol 词法符号标记
词法符号标记。例如:"{", "}", ">=" 等等。
解析核心代码如下:
// Lexer.java
/**
* 是否是 符号
*
* @see Tokenizer#scanSymbol()
* @return 是否
*/
privateboolean isSymbolBegin(){
returnCharType.isSymbol(getCurrentChar(0));
}
// CharType.java
/**
* 判断是否为符号.
*
* @param ch 待判断的字符
* @return 是否为符号
*/
publicstaticboolean isSymbol(finalchar 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 符号标记
*/
publicToken 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);
}
returnnewToken(symbol, literals, offset + length);
}
3.4 Assist 词法辅助标记
Assist 词法辅助标记,一共分成 2 种:
- END :分析结束
- ERROR :分析错误。
4. 彩蛋
老铁,是不是比想象中简单一些?!继续加油写 Parser 相关的文章!来一波微信公众号关注吧。
Sharding-JDBC 正在收集使用公司名单:传送门。🙂 你的登记,会让更多人参与和使用 Sharding-JDBC。Sharding-JDBC 也会因此,能够覆盖更广的场景。登记吧,少年!
我创建了一个微信群【源码圈】,希望和大家分享交流读源码的经验。
读源码先难后易,掌握方法后,可以做更有深度的学习。
而且掌握方法并不难噢。
加群方式:微信公众号发送关键字【qun】。
最新评论
推荐文章
作者最新文章
你可能感兴趣的文章
Copyright Disclaimer: The copyright of contents (including texts, images, videos and audios) posted above belong to the User who shared or the third-party website which the User shared from. If you found your copyright have been infringed, please send a DMCA takedown notice to [email protected]. For more detail of the source, please click on the button "Read Original Post" below. For other communications, please send to [email protected].
版权声明:以上内容为用户推荐收藏至CareerEngine平台,其内容(含文字、图片、视频、音频等)及知识版权均属用户或用户转发自的第三方网站,如涉嫌侵权,请通知[email protected]进行信息删除。如需查看信息来源,请点击“查看原文”。如需洽谈其它事宜,请联系[email protected]。
版权声明:以上内容为用户推荐收藏至CareerEngine平台,其内容(含文字、图片、视频、音频等)及知识版权均属用户或用户转发自的第三方网站,如涉嫌侵权,请通知[email protected]进行信息删除。如需查看信息来源,请点击“查看原文”。如需洽谈其它事宜,请联系[email protected]。