Sharding-JDBC Lexer源码分析

Posted by RussXia on August 6, 2018

Sharding-JDBC Lexer源码分析

Lexer中的Toekn

Lexer在Sharding-JDBC中的主要作用是顺序解析sql,将字符串解析成一个个Token(包含词法类型、词法字面量、sql字符串中结束位置等信息)。

@RequiredArgsConstructor
@Getter
public final class Token {
    //词法类型
    private final TokenType type;
    //字面量
    private final String literals;
    //在sql字符串中的偏离位置
    private final int endPosition;
}

其中,词法类型主要分为以下几种:

  • DefaultKeyword-关键字类型:SQL中一些有特殊意义的关键字,e.g.:SCHEMA、TABLE、CREATE、SELECT。
  • Literals-字面量:包含INT, FLOAT, HEX, CHARS, IDENTIFIER, VARIABLE等类型。
  • Symbol-符号类型:各类符号,e.g.:’+’、’;’、’(‘、’«‘等等
  • Assit-辅助类型:目前之定义了END, ERROR两种类型

下面是一段Sharding-JDBC中的一段JUnit测试方法。包括了我们上面提到的几种类型

private void assertNextTokenForNumber(final String expectedNumber, final TokenType expectedTokenType) {
        Lexer lexer = new Lexer(String.format("SELECT * FROM XXX_TABLE WHERE XX=%s AND YY=%s", expectedNumber, expectedNumber), dictionary);
        LexerAssert.assertNextToken(lexer, DefaultKeyword.SELECT, "SELECT");
        LexerAssert.assertNextToken(lexer, Symbol.STAR, "*");
        LexerAssert.assertNextToken(lexer, DefaultKeyword.FROM, "FROM");
        LexerAssert.assertNextToken(lexer, Literals.IDENTIFIER, "XXX_TABLE");
        LexerAssert.assertNextToken(lexer, DefaultKeyword.WHERE, "WHERE");
        LexerAssert.assertNextToken(lexer, Literals.IDENTIFIER, "XX");
        LexerAssert.assertNextToken(lexer, Symbol.EQ, "=");
        LexerAssert.assertNextToken(lexer, expectedTokenType, expectedNumber);
        LexerAssert.assertNextToken(lexer, DefaultKeyword.AND, "AND");
        LexerAssert.assertNextToken(lexer, Literals.IDENTIFIER, "YY");
        LexerAssert.assertNextToken(lexer, Symbol.EQ, "=");
        LexerAssert.assertNextToken(lexer, expectedTokenType, expectedNumber);
        LexerAssert.assertNextToken(lexer, Assist.END, "");
    }

Sharding-JDBC是支持多种数据库的,在词法上不同数据库的词法略有不同。例如describe是mysql支持,Oracle不支持的关键字。 Sharding-JDBC针对不同的数据库,有自己一套dialect的KeyWord。 .KeyWord继承结构

同理,针对不同的数据库,Sharding-JDBC使用的词法解析器也是不同的,如MySQL的词法解析器:MySQLLexer,Oracle的词法解析器OracleLexer。在创建MySQL时,会加载DefaultKeyWord和MySQLKeyword。

Dictionary的构造方法


public Dictionary(final Keyword... dialectKeywords) {
    fill(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);
    }
}

MySQLLexer

public final class MySQLLexer extends Lexer {
    //加载mysql的关键字
    private static Dictionary dictionary = new Dictionary(MySQLKeyword.values());

    public MySQLLexer(final String input) {
        super(input, dictionary);
    }

    @Override
    protected boolean isHintBegin() {
        return '/' == getCurrentChar(0) && '*' == getCurrentChar(1) && '!' == getCurrentChar(2);
    }

    @Override
    protected boolean isCommentBegin() {
        return '#' == getCurrentChar(0) || super.isCommentBegin();
    }

    @Override
    protected boolean isVariableBegin() {
        return '@' == getCurrentChar(0);
    }
}

Lexer解析SQL

Lexer类中解析的核心方法是io.shardingsphere.core.parsing.lexer.Lexer#nextToken。

@RequiredArgsConstructor
public class Lexer {

    //输入的sql
    @Getter
    private final String input;

    //词典,里面包含了各种关键字
    private final Dictionary dictionary;

    //当前解析位置的偏移量
    private int offset;

    //当前解析出的Token
    @Getter
    private Token currentToken;

    /**
     * Analyse next token.
     */
    public final void nextToken() {
        //部分内容跳过解析
        skipIgnoredToken();
        if (isVariableBegin()) {    //解析变量
            currentToken = new Tokenizer(input, dictionary, offset).scanVariable();
        } else if (isNCharBegin()) {    //nchar、nvarchar类型解析
            currentToken = new Tokenizer(input, dictionary, ++offset).scanChars();
        } else if (isIdentifierBegin()) {   //标识符解析(包含关键字)
            currentToken = new Tokenizer(input, dictionary, offset).scanIdentifier();
        } else if (isHexDecimalBegin()) {   //16进制数字解析
            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();
    }

    /**
     * 跳过忽略的词法标记
     * 1. 空格
     * 2. SQL Hint
     * 3. SQL 注释
     */
    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();
        }
    }
    ..省略..
    ..省略..
    ..省略..
}

各类解析大同小异,下面以解析标识符为例。

/**
    * scan identifier.
    *
    * @return identifier token
    */
public Token scanIdentifier() {
    //如果是以`开头的,肯定是一个字面量
    if ('`' == charAt(offset)) {
        int length = getLengthUntilTerminatedChar('`');
        return new Token(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)) {
        return new Token(processAmbiguousIdentifier(offset + length, literals), literals, offset + length);
    }
    //是否是关键字,如果不是,返回字面量类型。
    return new Token(dictionary.findTokenType(literals, Literals.IDENTIFIER), literals, offset + length);
}

//关键字如果包含字面量,返回关键字,否则返回给定的默认类型defaultTokenType。
TokenType findTokenType(final String literals, final TokenType defaultTokenType) {
    String key = null == literals ? null : literals.toUpperCase();
    return tokens.containsKey(key) ? tokens.get(key) : defaultTokenType;
}

Sharding-JDBC是如何使用Lexer的

Sharding-JDBC通过简单工程模式,通过LexerEngineFactory.newInstance方法,产生一个携带特定语法解析的LexerEngine。

public static LexerEngine newInstance(final DatabaseType dbType, final String sql) {
    switch (dbType) {
        case H2:
        case MySQL:
            return new LexerEngine(new MySQLLexer(sql));
        case Oracle:
            return new LexerEngine(new OracleLexer(sql));
        case SQLServer:
            return new LexerEngine(new SQLServerLexer(sql));
        case PostgreSQL:
            return new LexerEngine(new PostgreSQLLexer(sql));
        default:
            throw new UnsupportedOperationException(String.format("Cannot support database [%s].", dbType));
    }
}