ast解析mysql_Druid SQL 解析器的解析过程 - Go语言中文社区

ast解析mysql_Druid SQL 解析器的解析过程


这篇文尝试近距离地探究 Druid SQL 解析器如何工作。

Demo 代码

以这份代码为例

/**

*

*

* @author beanlam

* @date 2017年1月10日 下午11:06:26

* @version 1.0

*

*/

public class ParserMain {

public static void main(String[] args) {

String sql = "select * from user order by id";

// 新建 MySQL Parser

SQLStatementParser parser = new MySqlStatementParser(sql);

// 使用Parser解析生成AST,这里SQLStatement就是AST

SQLStatement statement = parser.parseStatement();

// 使用visitor来访问AST

MySqlSchemaStatVisitor visitor = new MySqlSchemaStatVisitor();

statement.accept(visitor);

System.out.println(visitor.getColumns());

System.out.println(visitor.getOrderByColumns());

}

}

一开始,需要初始化一个 Parser,在这里 SQLStatementParser 是一个父类,真正解析 SQL 语句的 Parser 实现是 MySqlStatementParser。

Parser 的解析结果是一个 SQLStatement,这是一个内部维护了树状逻辑结构的类。

词法分析

Druid 的代码里,代表语法分析和词法分析的类分别是 SQLParser 和 Lexer。并且, Parser 拥有一个 Lexer。

public class SQLParser {

protected final Lexer lexer;

protected String dbType;

public SQLParser(String sql, String dbType){

this(new Lexer(sql), dbType);

this.lexer.nextToken();

}

public SQLParser(String sql){

this(sql, null);

}

public SQLParser(Lexer lexer){

this(lexer, null);

}

public SQLParser(Lexer lexer, String dbType){

this.lexer = lexer;

this.dbType = dbType;

}

}

经过瘦身后的 Druid 代码,其 Lexer 只有两个,分别是 Lexer,以及它的子类 MySqlLexer

Lexer 作为词法分析器,必然拥有其词汇表,在Lexer里,以 Keywords 表示。

protected Keywords keywods = Keywords.DEFAULT_KEYWORDS;

Keywords 实际上是 key 为单词,value 为 Token 的字典型结构,其中 Token 是单词的类型,比如说,“select” 的 Token 类型就是 Select Token,而 “abc” 的 Token 类型,则是标识符,也表示为 Identifier Token。

而 MySqlLexer 类,除了沿用其父类的 Keywords 外,自己还有自己的 Keywords。可以理解为 Lexer 所维护的关键字集合,是通用的;而 MySqlLexer 除了有通用的关键字集合,也有属于 MySQL 数据库 SQL 方言的关键字集合。

Parser 是 Lexer 的使用者,站在 Parser 的角度看,它会怎么去使用 Lexer,或者说,Lexer 应该具备怎样的功能,才能满足 Parser 的使用需求。

Lexer 应该具备一个函数,能让使用者命令它解析一个单词,并且 Lexer 还必须提供一个函数,供使用者获取 Lexer 上一次解析到的单词以及单词的类型。

在 Lexer 中,nextToken() 这个方法提供了第一个需求,只要被调用,它就按顺序从 SQL 语句的开头到结尾,解析出下一个单词;token() 方法,则返回了上一次解析的单词的 Token 类型,如果 Token 类型是标识符(Identifier),Lexer 还提供了一个 stringVal() 方法,让使用者能拿到标识符的值。

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

Select 语法分析

有了 Lexer 这样的犀利工具,接下来就是 Parser 发挥的时候了,从 Demo 代码里可以看到,解析的开始,在于调用 parser.parseStatement() 方法。进到这个方法看看,发现清一色是形似如下格式的代码:

if (lexer.token() == Token.xxx) {

// 这里解析 xxx 类型

return;

}

if (lexer.token() == Token.aaa) {

// 这里解析 aaa 类型

return;

}

显然,如果是分析对 Select 类型的语句的解析,那么应该关注以下的代码:

if (lexer.token() == Token.SELECT) {

statementList.add(parseSelect());

continue;

}

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

@Override

public SQLStatement parseSelect() {

MySqlSelectParser selectParser = new MySqlSelectParser(this.exprParser);

SQLSelect select = selectParser.select();

if (selectParser.returningFlag) {

return selectParser.updateStmt;

}

return new SQLSelectStatement(select, JdbcConstants.MYSQL);

}

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

抛开解析的细节不谈,实际上我会非常关心这棵 AST 的层次结构。

Select 抽象语法树

打开 SQLSelectStatement 的代码,扫描它的子成员,便分析出这样的一棵语法树:

298987ac0f5ceae81d2f2ac1493d7500.png

这意味着,在 Druid 眼里,它是这样看待一条 Select 语句的所有成员部分的。

Visitor

从 demo 代码中可以看到,有了 AST 语法树后,则需要一个 visitor 来访问它

// 使用visitor来访问AST

MySqlSchemaStatVisitor visitor = new MySqlSchemaStatVisitor();

statement.accept(visitor);

System.out.println(visitor.getColumns());

System.out.println(visitor.getOrderByColumns());

statement 调用 accept 方法,以 visitor 作为参数,开始了访问之旅。在这里 statement 的实际类型是 SQLSelectStatement。

在 Druid 中,一条 SQL 语句中的元素,无论是高层次还是低层次的元素,都是一个 SQLObject,statement 是一种 SQLObject,表达式 expr 也是一种 SQLObject,函数、字段、条件等等,这些都是一种 SQLObject,SQLObject 是一个接口,accept 方法便是它定义的,目的是为了让访问者在访问 SQLObject 时,告知访问者一些事情,好让访问者在访问的过程中能够收集到关于该 SQLObject 的一些信息。

具体的 accept() 实现,在 SQLObjectImpl 这个类中,代码如下所示:

public final void accept(SQLASTVisitor visitor) {

if (visitor == null) {

throw new IllegalArgumentException();

}

visitor.preVisit(this);

accept0(visitor);

visitor.postVisit(this);

}

这是一个 final 方法,意味着所有的子类都要遵循这个模板,首先 accept 方法前和后,visitor 都会做一些工作。真正的访问流程定义在 accept0() 方法里,而它是一个抽象方法。

因此要知道 Druid 中是如何访问 AST 的,先拿 SQLSelectStatement 的 accept0() 方法来探探究竟。

protected void accept0(SQLASTVisitor visitor) {

if (visitor.visit(this)) {

acceptChild(visitor, this.select);

}

visitor.endVisit(this);

}

首先,使 visitor 访问自己,访问自己后,visitor 会决定是否还要访问自己的子元素。

打开 MySqlSchemaStateVisitor 的 visit 方法,可以看到,visitor 做了一些事,初始化了自己的 aliasMap,然后 return true,这意味着还要访问 SQLSelectStatement 的子节点。

public boolean visit(SQLSelectStatement x) {

setAliasMap();

return true;

}

接下来访问子元素

protected final void acceptChild(SQLASTVisitor visitor, SQLObject child) {

if (child == null) {

return;

}

child.accept(visitor);

}

由此可以看出,SQLObject 负责通知 visitor 要访问自己的哪些元素,而 visitor 则负责访问相应元素前,中,后三个过程的逻辑处理。

90f187a3bd790776fdf88f6cb8c0d0d2.png

版权声明:本文来源CSDN,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/weixin_31519139/article/details/113254736
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。
  • 发表于 2021-05-16 05:48:38
  • 阅读 ( 1064 )
  • 分类:数据库

0 条评论

请先 登录 后评论

官方社群

GO教程

猜你喜欢