网站首页 文章专栏 深入druid之sqlparse,源码解读之Parser
深入druid之sqlparse,源码解读之Parser

一. sqlparser在druid中的位置,以及目录结构

1636474905(1).jpg


如图可以看出通过SQLUtils工具类入口,将sql解析为ast语法树,通过visitor访问。完整的流程大致如下:


1636474979(1).jpg


二. 解析一个mysql涉及的类图

通过上一篇的demo,我们知道,如下即可得到一个语法树,上篇链接:深入druid之sqlparse,sql语法是如何被解析的

 String dbType = JdbcConstants.MYSQL;
 List statementList = SQLUtils.parseStatements(sql, dbType);


对于一个SQL请求,根据其指定的sqlType,需要先新建一个SQLStatementParser。那么这个初始化过程究竟是怎样的呢?涉及到哪些类?通过类图工具,大致如下:


c5a454b1-06bf-4837-8474-ca610fa76a6c.png


三. sql解析过程

我们一层一层分析,sql的解析分为语句解析(MySqlStatementParser) -> 表达式解析(MySqlExprParser) -> 词法解析(MySqlLexer)。

1. MySqlStatementParser 继承自 SQLStatementParser,继承自 SQLParser。


777.png


SQLParser属于顶层,是一个解析类,包含三个属性


 - DbType:DbType也就是数据库类型,这里我们就先只研究mysql的

 - errorEndPos:解析出错sql的位置 

-  Lexer:Lexer就是词法解析,其作用就是分析sql中的每个单次的含义,是否是语法关键字,以及当前解析的位置。


SQLStatementParser继承自SQLParser,字段属性如下:


 - exprParser:表达式解析类

 - parseValuesSize:记录解析结果集大小

 - keepComments:是否保留注释

 - parseCompleteValues:是否全部解析完成


MySqlStatementParser


 - 静态关键词:比如auto increment,collate等,对于DDL语句或者DCL语句

 - exprParser:针对MySQL语句的parser


然后是表达式的结构,MysqlExprParser继承自SQLExprParser


888.png


SQLExprParser:

    - AGGREGATE_FUNCTIONS:一些统计函数的关键词

    - aggregateFunctions:保存统计函数的关键词

MySqlSQLExprParser:

    - AGGREGATE_FUNCTIONS:针对MySql统计函数的关键词


最后是词法解析的结构:


999.png


     - text:保存目前的整个SQL语句

     - pos:当前处理位置

     - mark:当前处理词的开始位置

     - ch:当前处理字符

     - buf:当前缓存的处理词

     - bufPos:用于取出词的标记,当前从text中取出的词应该为从mark位置开始,mark+bufPos结束的词

     - token:当前位于的关键词

     - stringVal:当前处理词

     - comments:注释

     - skipComment:是否跳过注释

     - savePoint:保存点

     - varIndex:针对?表达式

     - lines:总行数

     - digits:数字ASCII码

     - EOF:是否结尾

     - keepComments:是否保留注释

     - endOfComment:是否注释结尾

     - line:当前处理行数

     - commentHandler:注释处理器

     - allowComment:是否允许注释

     - KeyWords:所有关键词集合


如此,一个MySqlStatementParser语法解析器便包含了表达式的解析器,以及词法的解析器,我们看下从头是怎么解析的。

入口:

// 使用调用SQLUtils,就直接生成一个语法树了
 String dbType = JdbcConstants.MYSQL;
 List statementList = SQLUtils.parseStatements(sql, dbType);

// 实际调用这个方法
public static List parseStatements(String sql, DbType dbType, SQLParserFeature... features) {
    // 此处生成一个语法解析器,实际就是一个MySqlStatementParser
    SQLStatementParser parser = SQLParserUtils.createSQLStatementParser(sql, dbType, features);
    List stmtList = new ArrayList();
    // 语法解析器调用解析函数,返回语法树列表
    parser.parseStatementList(stmtList, -1, null);
    if (parser.getLexer().token() != Token.EOF) {
        throw new ParserException("syntax error : " + sql);
    }
    return stmtList;
}

我们看下createSQLStatementParser这个方法是怎么生成一个MySqlStatementParser的


1636471332(1).jpg


其实就是根据类型,new一个对象,在MySqlStatementParser的构造函数中,进行了一系列的初始化动作,再去new一个MySqlExprParser表达式解析器,new MySqlExprParser的时候又会new 一个MySqlLexer,套娃一样。

Lexer类作为词法分析,它拥有一个词汇表以 Keywords 表示。

protected Keywords keywords = Keywords.DEFAULT_KEYWORDS;

Keywords 实际上是 key 为单词,value 为 Token 的字典型结构,其中 Token 是单词的类型,为枚举类型,包含了所有的基础语法关键字,而 MySqlLexer 类,除了沿用其父类的 Keywords 外,自己还有自己的 Keywords。可以理解为 Lexer 所维护的关键字集合,是通用的;而 MySqlLexer 除了有通用的关键字集合,也有属于 MySQL 数据库 SQL 方言的关键字集合。同理其他数据库类型也有自己的语法关键字。

在new一个MySqlLexer时,会加载其关键词,同时将sql原始值保存下来,当前处理位置置为0。


同理,且在new MySqlExprParser表达式解析器的时候会初始化自己的关键表达式,且会根据sql的前6个字符,判断是 select,insert,还是update,初始化第一个单词的属性


2.jpg


如此便初始化完了一个MySqlStatementParser。


初始化完成后,就要对sql进行解析了,通过parseStatementList方法进行解析。

parser.parseStatementList(stmtList, -1, null);
public void parseStatementList(List statementList, int max, SQLObject parent) {
    if ("select @@session.tx_read_only".equals(lexer.text)
            && lexer.token == Token.SELECT) {
        SQLSelect select = new SQLSelect();
        MySqlSelectQueryBlock queryBlock = new MySqlSelectQueryBlock();
        queryBlock.addSelectItem(new SQLPropertyExpr(new SQLVariantRefExpr("@@session"), "tx_read_only"));
        select.setQuery(queryBlock);

        SQLSelectStatement stmt = new SQLSelectStatement(select);
        statementList.add(stmt);

        lexer.reset(29, '\u001A', Token.EOF);
        return;
    }

    boolean semi = false;
    for (int i = 0;;i++) {
        if (max != -1) {
            if (statementList.size() >= max) {
                return;
            }
        }
        while (lexer.token == MULTI_LINE_COMMENT || lexer.token == LINE_COMMENT) {
            lexer.nextToken();
        }

        switch (lexer.token) {
            case EOF:
            case END:
            case UNTIL:
            case ELSE:
            case WHEN:
                if (lexer.isKeepComments() && lexer.hasComment() && statementList.size() > 0) {
                    SQLStatement stmt = statementList.get(statementList.size() - 1);
                    stmt.addAfterComment(lexer.readAndResetComments());
                }
                return;
            case SEMI: {
                int line0 = lexer.getLine();
                char ch = lexer.ch;
                lexer.nextToken();
                int line1 = lexer.getLine();

                if(statementList.size() > 0) {
                    SQLStatement lastStmt = statementList.get(statementList.size() - 1);
                    lastStmt.setAfterSemi(true);

                    if (lexer.isKeepComments()) {
                        if (ch == '\n'
                                && lexer.getComments() != null
                                && !lexer.getComments().isEmpty()
                                && !(lastStmt instanceof SQLSetStatement)
                        ) {
                            lexer.getComments().add(0, new String("\n"));
                        }

                        if (line1 - line0  0
                        && statementList.get(statementList.size() - i) instanceof MySqlHintStatement) {
                    hintStatement = (MySqlHintStatement) statementList.get(statementList.size() - i);
                } else if (i > 0 && dbType != DbType.odps && !semi) {
                    throw new ParserException("syntax error. " + lexer.info());
                }
                SQLStatement stmt = parseSelect();
                stmt.setParent(parent);
                if (hintStatement != null && stmt instanceof SQLStatementImpl) {
                    SQLStatementImpl stmtImpl = (SQLStatementImpl) stmt;
                    List hints = stmtImpl.getHeadHintsDirect();
                    if (hints == null) {
                        stmtImpl.setHeadHints(hintStatement.getHints());
                    } else {
                        hints.addAll(hintStatement.getHints());
                    }
                    statementList.set(statementList.size() - 1, stmt);
                } else {
                    statementList.add(stmt);
                }
                semi = false;
                continue;
            }
            
            ...忽略大量代码
         }
       }

这个方法太长了,大概逻辑就是起一个死循环,在循环里面,遍历MySqlLexer的单词的token属性,根据token属性,构建对应的SQLStatement,并加入入参的stmtList,通过lexer.nextToken()方法获取生成下一个单词,遇到空格,回车等直接跳过,直到结尾token类型为EOF或者其他方式退出循环,如此便完成了语法树。我们选取关键的几个方法看下。


lexer.nextToken();
public final void nextToken() {
    startPos = pos;
    bufPos = 0;
    if (comments != null && comments.size() > 0) {
        comments = null;
    }

    this.lines = 0;
    int startLine = line;

    for (;;) {
        if (isWhitespace(ch)) {
            if (ch == '\n') {
                line++;

                lines = line - startLine;
            }

            ch = charAt(++pos);
            startPos = pos;
            continue;
        }

        if (ch == '$' && isVaraintChar(charAt(pos + 1))) {
            scanVariable();
            return;
        }

        if (isFirstIdentifierChar(ch)) {
            if (ch == '(') {
                scanChar();
                token = LPAREN;
                return;
            } else if (ch == ')') {
                scanChar();
                token = RPAREN;
                return;
            }

            if (ch == 'N' || ch == 'n') {
                if (charAt(pos + 1) == '\'') {
                    ++pos;
                    ch = '\'';
                    scanString();
                    token = Token.LITERAL_NCHARS;
                    return;
                }
            }

            if (ch == '—' && charAt(pos + 1) == '—' && charAt(pos + 2) == '\n') {
                pos += 3;
                ch = charAt(pos);
                continue;
            }

            scanIdentifier();
            return;
        }
        ...

走进 Lexer 的 nextToken() 方法,可以发现它的代码充斥着 if 语句和 switch 语句,因为解析单词的时候,是一个字符一个字符地解析,这就意味着,这个方法每次扫描一个字符,都必须判断单词是否结束,应该用什么方式来验证这个单词等等。这个过程,就是一个 状态机 运作的过程,每解析到一个字符,都要判断当前的状态,以决定应该进入下一个什么状态。

回到我们的MySqlStatementParser,也就是使用上面词法器的token类型,判断类型,形似如下格式的代码:

case WITH: {
    SQLStatement stmt = parseWith();
    stmt.setParent(parent);
    statementList.add(stmt);
    continue;
}

我们找个select的看下

case SELECT: {
    MySqlHintStatement hintStatement = null;
    if (i == 1
            && statementList.size() > 0
            && statementList.get(statementList.size() - i) instanceof MySqlHintStatement) {
        hintStatement = (MySqlHintStatement) statementList.get(statementList.size() - i);
    } else if (i > 0 && dbType != DbType.odps && !semi) {
        throw new ParserException("syntax error. " + lexer.info());
    }
    SQLStatement stmt = parseSelect();
    stmt.setParent(parent);
    if (hintStatement != null && stmt instanceof SQLStatementImpl) {
        SQLStatementImpl stmtImpl = (SQLStatementImpl) stmt;
        List hints = stmtImpl.getHeadHintsDirect();
        if (hints == null) {
            stmtImpl.setHeadHints(hintStatement.getHints());
        } else {
            hints.addAll(hintStatement.getHints());
        }
        statementList.set(statementList.size() - 1, stmt);
    } else {
        statementList.add(stmt);
    }
    semi = false;
    continue;
}

核心逻辑在这一句:

SQLStatement stmt = parseSelect();

MySqlStatementParser 重载了它的父类的这个方法,因此这个方法实际上的实现细节是这样的:

public SQLStatement parseSelect() {
    SQLSelectParser selectParser = createSQLSelectParser();
    SQLSelect select = selectParser.select();
    return new SQLSelectStatement(select,getDbType());
}

初始化一个针对 MySQL Select 语句的 Parser,然后调用 select() 方法进行解析,把返回结果 SQLSelect 放到 SQLSelectStatement 里,而这个 SQLSelectStatement ,便是我最关心的 AST 抽象语法树,SQLSelect 是它的第一个子节点。我们看下selectParser.select()方法做了什么。

public SQLSelect select() {
    SQLSelect select = new SQLSelect();

    if (lexer.token == Token.WITH) {
        SQLWithSubqueryClause with = this.parseWith();
        select.setWithSubQuery(with);
    }

    SQLSelectQuery query = query(select, true);
    select.setQuery(query);

    SQLOrderBy orderBy = this.parseOrderBy();

    if (query instanceof SQLSelectQueryBlock) {
        SQLSelectQueryBlock queryBlock = (SQLSelectQueryBlock) query;

        if (queryBlock.getOrderBy() == null) {
            queryBlock.setOrderBy(orderBy);
            if (lexer.token == Token.LIMIT) {
                SQLLimit limit = this.exprParser.parseLimit();
                queryBlock.setLimit(limit);
            }
        } else {
            select.setOrderBy(orderBy);
            if (lexer.token == Token.LIMIT) {
                SQLLimit limit = this.exprParser.parseLimit();
                select.setLimit(limit);
            }
        }

        if (orderBy != null) {
            parseFetchClause(queryBlock);
        }
    } else {
        select.setOrderBy(orderBy);
    }

    if (lexer.token == Token.LIMIT) {
        SQLLimit limit = this.exprParser.parseLimit();
        select.setLimit(limit);
    }

    while (lexer.token == Token.HINT) {
        this.exprParser.parseHints(select.getHints());
    }

    return select;
}

new了一个SQLSelect,并通过query()方法解析语法,返回一个SQLSelectQuery对象,这个对象也就是上篇博客分析中语法树的顶层。然后再通过各种各样的解析生成SQLLimit,SQLOrderBy等其他属性。

这个中间逻辑相当复杂,暂时就不研究了。


如此便从头到尾,从一个sql,初始化一个 MySqlStatementParser 语法分析器,内套一个 MySqlExprParser表达式解析器,再内套一个MySqlLexer,通过lexer不断生成词语,通过MySqlExprParser分析表达式语法,MySqlStatementParser分析关键字,就慢慢生成了一个语法树对象。


有了这颗语法树,便可以通过visitor进行分析,读取我们想要的数据了,visitor这块的源码,我们下篇博客再继续分析。







版权声明:本文由星尘阁原创出品,转载请注明出处!

本文链接:http://www.52xingchen.cn/detail/89




赞助本站,网站的发展离不开你们的支持!
来说两句吧
大侠留个名吧,或者可以使用QQ登录。
: 您已登陆!可以继续留言。
最新评论