Shading-jdbc源码分析-sql词法解析

前言

前有芋艿大佬已经发过相关分析的文章,自己觉的源码总归要看一下,然后看了就要记录下来(记性很差...),所以就有了这篇文章(以后还要继续更😄)
,希望我们都能在看过文章后能够有不一样的收获。

声明:本文基于1.5.M1版本

相关的UML类图

TokenType

解析:

首先我们来看下解析sql的过程中用到的类做一个解释:

  • TokenType:衍生了多个子类,用来标记sql拆分过程中,每个被拆分的词的类型(比如select属于KeyWord,";"属于Symbol)
  • Lexer:sql具体的解析类,通过调用nextToken()方法分析sql每个词的类型;
  • Tokenizer:具体的标记类,标记具体的词,配合Lexer的nextToken()方法使用
  • Token:标记后的结果,type:具体的词类型、literals:具体的词、endPosition:这个词在sql中的最后位置(index)
@Test
    public void assertNextTokenForOrderBy() {
        Lexer lexer = new Lexer("SELECT * FROM ORDER  ORDER \t  BY XX DESC", dictionary);
        //lexer.nextToken();
        LexerAssert.assertNextToken(lexer, DefaultKeyword.SELECT, "SELECT");
        //lexer.nextToken();
        LexerAssert.assertNextToken(lexer, Symbol.STAR, "*");
        //lexer.nextToken();
        LexerAssert.assertNextToken(lexer, DefaultKeyword.FROM, "FROM");
        //lexer.nextToken();
        LexerAssert.assertNextToken(lexer, Literals.IDENTIFIER, "ORDER");
        //lexer.nextToken();
        LexerAssert.assertNextToken(lexer, DefaultKeyword.ORDER, "ORDER");
        //lexer.nextToken();
        LexerAssert.assertNextToken(lexer, DefaultKeyword.BY, "BY");
        //lexer.nextToken();
        LexerAssert.assertNextToken(lexer, Literals.IDENTIFIER, "XX");
        //lexer.nextToken();
        LexerAssert.assertNextToken(lexer, DefaultKeyword.DESC, "DESC");
        //lexer.nextToken();
        LexerAssert.assertNextToken(lexer, Assist.END, "");
    }

上面是项目中的一段测试用例,我们以这个用例来分析。

  • 第一次调用nextToken()
/**
     * 分析下一个词法标记.
     */
    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 {
            currentToken = new Token(Assist.ERROR, "", offset);
        }
        offset = currentToken.getEndPosition();
    }
  • 先走skipIgnoredToken();
  1. 跳过空格
  2. 跳过以/*!开头的(Mysql是这样)的字符,对于不同数据库。isHintBegin实现了不同的处理
  3. 跳过注释
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();
        }
    }

这里我们以跳过空格为例来展开说明:

从传入的offset标志位开始,循环判断sql语句中对应位置的字符是不是空格,直到不是空格就退出,返回最新位置的offset

     /**
     * 跳过空格. 
     * 
     * @return 跳过空格后的偏移量
     */
    public int skipWhitespace() {
        int length = 0;
        while (CharType.isWhitespace(charAt(offset + length))) {
            length++;
        }
        return offset + length;
    }
    
    private char charAt(final int index) {
        return index >= input.length() ? (char) CharType.EOI : input.charAt(index);
    }
    /**
     * 判断是否为空格.
     * 
     * @param ch 待判断的字符
     * @return 是否为空格
     */
    public static boolean isWhitespace(final char ch) {
        return ch <= 32 && EOI != ch || 160 == ch || ch >= 0x7F && ch <= 0xA0;
    }
  • 第二步 从最新位置的offset开始,继续判断是否是变量,这里以mysql为例,开始的单词是‘SELECT’,所以进入第三步
  /**
    这是mysql的实现
  **/
@Override
    protected boolean isVariableBegin() {
        return '@' == getCurrentChar(0);
    }
  • 第三步 判断是否是NChar,false,进入第四步
private boolean isNCharBegin() {
        return isSupportNChars() && 'N' == getCurrentChar(0) && '\'' == getCurrentChar(1);
    }
  • 第四步 判断是否是标识符 true
  1. 扫描标识符
  2. 循环判断当前的标识符是不是字符,直到不是字符
  3. 截取这个字符串
  4. 判断是否是双关词汇(group、order)
  5. 如果4符合,则进一步做特殊处理
  6. 构造Token返回
private boolean isIdentifierBegin() {
        return isIdentifierBegin(getCurrentChar(0));
    }
 private boolean isIdentifierBegin(final char ch) {
        return CharType.isAlphabet(ch) || '`' == ch || '_' == ch || '$' == ch;
    }
   /**
     * 判断是否为字母.
     *
     * @param ch 待判断的字符
     * @return 是否为字母
     */
    public static boolean isAlphabet(final char ch) {
        return ch >= 'A' && ch <= 'Z' || ch >= 'a' && ch <= 'z';
    }   
    
   /**
     * 扫描标识符.
     *
     * @return 标识符标记
     */
    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);
        if (isAmbiguousIdentifier(literals)) {
            return new Token(processAmbiguousIdentifier(offset + length, literals), literals, offset + length);
        }
        return new Token(dictionary.findTokenType(literals, Literals.IDENTIFIER), literals, offset + length);
    }
  • 返回最终的Token,赋值给currentToken,更新offset,此时的Token内容如下。第一个 “SELECT” 就解析出来了,后面的单词继续调用nextToken(),方法差不多,区别就是词法的类型不一样,走的判断可能逻辑会不同,后面有兴趣的可以自己跟着代码去看看。

最后

小尾巴走一波,欢迎关注我的公众号,不定期分享编程方面的小技巧:)

posted @ 2018-10-26 22:57  selrain  阅读(615)  评论(0编辑  收藏  举报