1. 写作由来
Apache Shardingsphere 是一款国产精品中间件。我对这款中间件关注了很久了,刚好最近也业务中需要用到 Antlr4 来对语言进行解析,作为学习资料,我仔细的研究了 ShardingSphere 的解析器部分源码。通过对其源码的阅读和仿写,我获得了丰富的 Antlr4 使用经验,同时,我也尝试着向官方贡献了自己写的一点解析代码,并被官方采纳,也是感到无比的高兴。
2. 以 Oracle 的 DropTable 功能为例分析
SharingSphere 在其 Github 上提供了 Oracle 的 Antlr4 的 g4 文件,这个文件中支持了 DDL 的大部分功能,但是鉴于 Oracle 本身的功能复杂性,还是有一些功能没有提供的,但是也不影响学习。至于如何用 g4 文件生成代码,这不是本文的目的,有机会另开一篇来写。这里就默认生成了所有需要的 AST 和访问器代码。如下图:
Antlr4 会自动生成这些代码,注意其中的 OracleStatementBaseVisitor.java,我们所有的操作都要通过继承它来实现。换句话说,我们需要自己编写遍历 AST 的逻辑。首先来看看 DropTable 功能在 g4 文件中的定义:
dropTable
: DROP TABLE tableName (CASCADE CONSTRAINTS)? (PURGE)?
;
tableName
: (owner DOT_)? name
;
owner
: identifier
;
name
: identifier
;
identifier
: IDENTIFIER_ | unreservedWord
;
复制代码
再看测试看看这样一句 SQL 会被解析成一棵什么样形态的树:
-- 删除用户tom下的jerry表
drop table tom.jerry;
复制代码
从上图中可以看出,我们只需要去访问 tableName 这个节点下的所有节点就可以拿到所有的有用信息。解析 SQL 第一步,我们需要对模型进行抽象。ShardingSphere 的策略是所有的对象都实现自一个叫做 ASTNode 的接口:
public interface ASTNode {}
复制代码
接下来继续观察上面的树,我们可以发现 owner 和 name 都是一个叫做 identifier 的东西,这个 identifier 实际上是一个字符串或者是一个用引号包裹起来的字符串。那么为了保存它,需要编写一个类来保存,为了简单,我们就不考虑保存引号了:
public class IdentifierValue implements ASTNode {
private final String name;
public IdentifierValue(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
复制代码
这个基本的问题解决之后,就可以开始着手解决如何保存 name 和 owner 的问题了,还是观察上图,发现这两个 token 都是 identifier,因此基本上可以不用做额外操作。接下来看看最关键的节点 tableName,它包括了两个元素 owner 和 name,其中 owner 是可选的元素,在知道了这一点之后,我们设计这样一个类:
public class TableName implements ASTNode {
private IdentifierValue owner;
private final IdentifierValue name;
public TableName(IdentifierValue name) {
this.name = name;
}
public void setOwner(IdentifierValue owner) {
this.owner = owner;
}
public Optional<IdentifierValue> getOwner() {
return Optional.ofNullable(owner);
}
public IdentifierValue getName() {
return name;
}
}
复制代码
最后,还需要对语句本身进行抽象:
public class DropTableStatement implements ASTNode {
private final TableName tableName;
public DropTableStatement(TableName tableName) {
this.tableName = tableName;
}
public TableName getTableName() {
return tableName;
}
@Override
public String toString() {
return "DropTableStatement{" +
"tableName=" + tableName +
'}';
}
}
复制代码
这样,我们也就具备了将这个语句中所有的元素保存成模型的可能了(这里我忽略了(CASCADE CONSTRAINTS)? (PURGE)?后缀,因为这不过是一个简单的判断)。要将语句转换成模型,需要编写实现 visitor 逻辑,对这棵树的每一个节点进行遍历操作:
public class OracleDDLStatementVisitor extends OracleStatementBaseVisitor<ASTNode> {
@Override
public ASTNode visitDropTable(OracleStatementParser.DropTableContext ctx) {
return new DropTableStatement((TableName) visit(ctx.tableName()));
}
@Override
public ASTNode visitTableName(OracleStatementParser.TableNameContext ctx) {
TableName result = new TableName((IdentifierValue) visit(ctx.name()));
if (Objects.nonNull(ctx.owner())) {
result.setOwner((IdentifierValue) visit(ctx.owner()));
}
return result;
}
@Override
public ASTNode visitIdentifier(OracleStatementParser.IdentifierContext ctx) {
return new IdentifierValue(ctx.IDENTIFIER_().getText());
}
}
复制代码
由于语句比较简单,只需要遍历三个结点就可以了,这就有了上面的逻辑。经过下面这段代码的测试,我的代码是没有问题的。
评论