使用antlr4解析clickhouse sql获取表名
ANTLR是用java实现的一个语言识别器,它通过定义一个要识别目标语言的结构文件(DSL),然后用它的工具将该文件生成为可解析目标语言的java代码,利用生成的代码就可对目标语言进行解析。这种方式避免了自己编写java代码去解析文本,可利用ANTLR解析JSON,HTML,XML,EDIFACT,或自定义的报文格式。最出名的Spark计算引擎2.x就是用它来解析SQL的。
本文尝试使用antlr解析clickhouse,从而提取出查询sql中的表名,供数据安全项目鉴权。
参考文章 https://www.jianshu.com/p/df256de737b4 ,此文章是使用antlr解析mysql语句,基本步骤如下:
1)准备相应的DSL文件,对应sql即为语法、词法定义文件;clickhouse的文件从以下链接获取:
https://fossies.org/linux/ClickHouse/utils/antlr/ClickHouseLexer.g4
https://fossies.org/linux/ClickHouse/utils/antlr/ClickHouseParser.g4
3)将antlr-4.5.3-complete.jar、语法词法文件放在一个目录中,执行如下命令即可生成java文件
java -jar ./antlr-4.5.3-complete.jar -Dlanguage=Java -listener -visitor -o ./ck-parser -package x.x.x.x.x ClickHouseParser.g4 ClickHouseLexer.g4
4) 拷贝生成的java包到自己的工程中,即可使用,注意生成的java文件里会有一些编译不过去的地方,如下图所示,生成的是c++语法的set变量及对应的操作方法,手工将其改为java的语法即可
5) 客户端使用
a、参考前同事的文章 https://www.jianshu.com/p/edd2d3eac1f0 ,自定义Listener,监听并记录下所有的表名,代码如下,分别重写enterTableIdentifier和enterAlias事件的原因是因为在enterTableIdentifier事件中监听到的表包含了表别名,客户端调用该类的getRealDataBaseTablenameAndOper方法即可获取所有真实表名。
public class CustomClickHouseParserBaseListener extends ClickHouseParserBaseListener {
private Map<String, Set<String>> dataBaseTablenameAndOper = new HashMap<>();//用来保存表与操作的对应关系
private Map<String, Set<String>> dataBaseTablenameAliasAndOper = new HashMap<>();//用来保存表别名与操作的对应关系
public Map<String, Set<String>> getDataBaseTablenameAndOper() {
return dataBaseTablenameAndOper;
public Map<String, Set<String>> getDataBaseTablenameAliasAndOper() {
return dataBaseTablenameAliasAndOper;
public Map<String, Set<String>> getRealDataBaseTablenameAndOper() {
for (String key : dataBaseTablenameAliasAndOper.keySet()) {
if (dataBaseTablenameAndOper.containsKey(key)) {
dataBaseTablenameAndOper.remove(key);
return dataBaseTablenameAndOper;
@Override
public void enterQuery(ClickHouseParser.QueryContext ctx) {
ParseTreeWalker queryWalker = new ParseTreeWalker();
queryWalker.walk(new ClickHouseParserBaseListener() {
public void enterTableIdentifier(ClickHouseParser.TableIdentifierContext tctx) {
if(tctx!=null) {
String table = tctx.getText()/*.toLowerCase()*/;
Set<String> oper;
if (dataBaseTablenameAndOper.containsKey(table)) {
oper = dataBaseTablenameAndOper.get(table);
} else {
oper = new HashSet<>();
oper.add("SELECT");
dataBaseTablenameAndOper.put(table, oper);
}, ctx);
queryWalker.walk(new ClickHouseParserBaseListener() {
public void enterAlias(ClickHouseParser.AliasContext tctx) {
if(tctx!=null) {
String table = tctx.getText()/*.toLowerCase()*/;
Set<String> oper;
if (dataBaseTablenameAliasAndOper.containsKey(table)) {
oper = dataBaseTablenameAliasAndOper.get(table);
} else {
oper = new HashSet<>();
oper.add("SELECT");
dataBaseTablenameAliasAndOper.put(table, oper);
}, ctx);
b、客户端代码使用上面的监听类,代码如下:
public class ClickhouseSqlUtil {
public static Map<String, Set<String>> getDataBaseTablenameAndOper(String sql){
ClickHouseLexer lexer = new ClickHouseLexer(new ANTLRInputStream(sql));
CommonTokenStream tokenStream = new CommonTokenStream(lexer);
ClickHouseParser parser = new ClickHouseParser(tokenStream);
ParseTreeWalker walker = new ParseTreeWalker();
CustomClickHouseParserBaseListener clickhouseBaseBaseListener = new CustomClickHouseParserBaseListener();
walker.walk(clickhouseBaseBaseListener, parser.queryStmt());
return clickhouseBaseBaseListener.getRealDataBaseTablenameAndOper();