网站首页 文章专栏 深入druid之sqlparse,源码解读之Parser
如图可以看出通过SQLUtils工具类入口,将sql解析为ast语法树,通过visitor访问。完整的流程大致如下:
通过上一篇的demo,我们知道,如下即可得到一个语法树,上篇链接:深入druid之sqlparse,sql语法是如何被解析的
String dbType = JdbcConstants.MYSQL; List statementList = SQLUtils.parseStatements(sql, dbType);
对于一个SQL请求,根据其指定的sqlType,需要先新建一个SQLStatementParser。那么这个初始化过程究竟是怎样的呢?涉及到哪些类?通过类图工具,大致如下:
我们一层一层分析,sql的解析分为语句解析(MySqlStatementParser) -> 表达式解析(MySqlExprParser) -> 词法解析(MySqlLexer)。
1. MySqlStatementParser 继承自 SQLStatementParser,继承自 SQLParser。
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
SQLExprParser:
- AGGREGATE_FUNCTIONS:一些统计函数的关键词
- aggregateFunctions:保存统计函数的关键词
MySqlSQLExprParser:
- AGGREGATE_FUNCTIONS:针对MySql统计函数的关键词
最后是词法解析的结构:
- 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的
其实就是根据类型,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,初始化第一个单词的属性
如此便初始化完了一个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这块的源码,我们下篇博客再继续分析。
版权声明:本文由星尘阁原创出品,转载请注明出处!