出于好奇和研究需要,需要将传入的sql进行解析,获取出一些关键字段。

通过查阅一些资料后,知道了有以下几种方式可以进行SQL的解析。

  1. druid sql解析
  2. JavaCC,在Calcite中有使用到
  3. antlr

通过简单对比和了解后,打算用antlr来进行SQL的解析

antlr

其官网地址为:https://www.antlr.org/

介绍如下

ANTLR (ANother Tool for Language Recognition) is a powerful parser generator for reading, processing, executing, or translating structured text or binary files. It’s widely used to build languages, tools, and frameworks. From a grammar, ANTLR generates a parser that can build and walk parse trees.

总之就是:一个强大的语法解析工具。

其次antlr在很多开源的项目中都有用到它,比如sharding-jdbc、hive、presto等。

解析SQL

知道了它可以解析SQL之后,接下来就来定一个小目标,解析一段SQL快速体验一下。

解析之前,先定一个打算要解析的目标。

目标:

获取出一条SQL中的所有表名

生成解析的代码

antlr在解析前,需要定义一个语法规则文件,其中包含了对应的词法信息和对应的语法规则

由于每个数据库的sql可能存在某些语法上的差异,所对应的语法规则可能不一样。这里以mysql的语法规则为例,那么则需要提供一个mysql数据库对应的语法规则文件,此文件可以自己编写,不过更实际一点还是直接使用antlr官方的文件即可。

antlr4官方提供的常用语法文件地址为: https://github.com/antlr/grammars-v4

在其中找到mysql数据库对应的语法文件,https://github.com/antlr/grammars-v4/tree/master/sql/mysql/Positive-Technologies

其中MySqlLexer.g4打开一看,可以看出里面主要是mysql数据库中的关键字信息,MySqlParser.g4为语法文件。

有了语法规则文件后,就可以直接生成代码了。

其生成代码的方法主要有两种:

  1. 下载antlr的jar包,用命令方式生成代码,其步鄹可以直接看官方的首页中的快速开始可以看到;https://github.com/antlr/antlr4/blob/master/doc/getting-started.md
OS X
$ cd /usr/local/lib
$ sudo curl -O https://www.antlr.org/download/antlr-4.9.2-complete.jar
$ export CLASSPATH=".:/usr/local/lib/antlr-4.9.2-complete.jar:$CLASSPATH"
$ alias antlr4='java -jar /usr/local/lib/antlr-4.9.2-complete.jar'
$ alias grun='java org.antlr.v4.gui.TestRig'
  1. 使用插件进行生成,操作步鄹在官网中也有说明。详见:https://www.antlr.org/tools.html

按照操作步骤,插件安装到idea中,再配置好生成的配置
在这里插入图片描述

之后点击生成,antlr插件则会快速生成对应语法规则文件对应的解析代码了。

就像我这里的一样,随便找了一个测试项目,生成了对应的解析代码
在这里插入图片描述

解析sql

通过上面的步骤,antlr生成出了很多代码,其中有两个类就是可以用来遍历用的。

一个是Listener类,一个是Visitor类。其遍历的规则是设计模式中的监听器设计模式和访问者设计模式。

遍历前记得要在对应的项目中导入antlr所需的jar包

<dependency> 
  <groupId>org.antlr</groupId> 
  <artifactId>antlr4</artifactId> 
  <version>4.9.3</version> 
</dependency> 

之后便可以编写对应的解析代码。
为了让分析SQL更加方便,可以先输出SQL中的解析树,

import com.github.puhaiyang.k8sops.antlr.MySqlLexer;
import com.github.puhaiyang.k8sops.antlr.MySqlParser;
import com.github.puhaiyang.k8sops.antlr.MysqlQueryFieldListener;
import org.antlr.v4.runtime.CharStreams;
import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.Parser;
import org.antlr.v4.runtime.ParserRuleContext;
import org.antlr.v4.runtime.tree.ParseTree;
import org.antlr.v4.runtime.tree.ParseTreeWalker;
import org.antlr.v4.runtime.tree.Trees;

import java.util.Arrays;
import java.util.List;

public class MysqlParseTest {
    public static void main(String[] args) throws Exception {
        String sql = "SELECT t1.column1,t1.column2,t1.column3,t2.xy from tableC t1 left join tableA t2 on t1.id=t2.oid where t1.column1 = 1 and t2.yy=6";
        //词法分析器
        MySqlLexer mySqlLexer = new MySqlLexer(CharStreams.fromString(sql.toUpperCase()));
        //词法符号的缓冲区,用于存储词法分析器生成的词法符号
        CommonTokenStream commonTokenStream = new CommonTokenStream(mySqlLexer);
        //新建一个语法分析器,处理词法符号缓冲区内容
        MySqlParser mySqlParser = new MySqlParser(commonTokenStream);
        //获取出selectStatement
        ParseTree tree = mySqlParser.selectStatement();
        System.out.println(printSyntaxTree(mySqlParser,tree));
    }

    public static String printSyntaxTree(Parser parser, ParseTree root) {
        StringBuilder buf = new StringBuilder();
        recursive(root, buf, 0, Arrays.asList(parser.getRuleNames()));
        return buf.toString();
    }

    private static void recursive(ParseTree aRoot, StringBuilder buf, int offset, List<String> ruleNames) {
        for (int i = 0; i < offset; i++) {
            buf.append("  ");
        }
        buf.append(Trees.getNodeText(aRoot, ruleNames)).append("\n");
        if (aRoot instanceof ParserRuleContext) {
            ParserRuleContext prc = (ParserRuleContext) aRoot;
            if (prc.children != null) {
                for (ParseTree child : prc.children) {
                    recursive(child, buf, offset + 1, ruleNames);
                }
            }
        }
    }
}

通过调用上面的printSyntaxTree方法,可以让SQL的语法树更加结构化的方式输出,写代码时也更便于分析。

上面的代码运行后,其输出结果为:

sqlStatements
  sqlStatement
    dmlStatement
      selectStatement
        querySpecification
          SELECT
          selectElements
            selectElement
              fullColumnName
                uid
                  simpleId
                    T1
                dottedId
                  .COLUMN1
            ,
            selectElement
              fullColumnName
                uid
                  simpleId
                    T1
                dottedId
                  .COLUMN2
            ,
            selectElement
              fullColumnName
                uid
                  simpleId
                    T1
                dottedId
                  .COLUMN3
            ,
            selectElement
              fullColumnName
                uid
                  simpleId
                    T2
                dottedId
                  .XY
          fromClause
            FROM
            tableSources
              tableSource
                tableSourceItem
                  tableName
                    fullId
                      uid
                        simpleId
                          TABLEC
                  uid
                    simpleId
                      T1
                joinPart
                  LEFT
                  JOIN
                  tableSourceItem
                    tableName
                      fullId
                        uid
                          simpleId
                            TABLEA
                    uid
                      simpleId
                        T2
                  ON
                  expression
                    predicate
                      predicate
                        expressionAtom
                          fullColumnName
                            uid
                              simpleId
                                T1
                            dottedId
                              .ID
                      comparisonOperator
                        =
                      predicate
                        expressionAtom
                          fullColumnName
                            uid
                              simpleId
                                T2
                            dottedId
                              .OID
            WHERE
            expression
              expression
                predicate
                  predicate
                    expressionAtom
                      fullColumnName
                        uid
                          simpleId
                            T1
                        dottedId
                          .COLUMN1
                  comparisonOperator
                    =
                  predicate
                    expressionAtom
                      constant
                        decimalLiteral
                          1
              logicalOperator
                AND
              expression
                predicate
                  predicate
                    expressionAtom
                      fullColumnName
                        uid
                          simpleId
                            T2
                        dottedId
                          .YY
                  comparisonOperator
                    =
                  predicate
                    expressionAtom
                      constant
                        decimalLiteral
                          6

FROM,T2.XY,T1.COLUMN3,T1.COLUMN2,T1.COLUMN1

之后选择一种合适的遍历方式编写解析代码即可,如这里需要获取SQL语句中的表名,则可以新建一个Listener,让其继承生成的ParserBaseListener即可。

然后再重写ParserBaseListener中需要进行记录的方法。

比如要获取一个SQL语句中的表名,可以这样写一下:

public class GetTableNamesListener extends MySqlParserBaseListener {
    private final Set<String> tableNameSet = new HashSet<String>();

    @Override
    public void enterTableSources(MySqlParser.TableSourcesContext ctx) {
        List<MySqlParser.TableSourceContext> tableSourceContexts = ctx.getRuleContexts(MySqlParser.TableSourceContext.class);
        for (MySqlParser.TableSourceContext tableSource : tableSourceContexts) {
            //通过tableSourceItems获取表名
            getTableNameByTableSourceItems(tableSource.getRuleContexts(MySqlParser.TableSourceItemContext.class));
            //获取join部分
            List<MySqlParser.OuterJoinContext> joinContexts = tableSource.getRuleContexts(MySqlParser.OuterJoinContext.class);
            for (MySqlParser.OuterJoinContext joinContext : joinContexts) {
                List<MySqlParser.TableSourceItemContext> tableSourceItemContexts = joinContext.getRuleContexts(MySqlParser.TableSourceItemContext.class);
                getTableNameByTableSourceItems(tableSourceItemContexts);
            }
        }
    }

    private void getTableNameByTableSourceItems(List<MySqlParser.TableSourceItemContext> tableSourceItems) {
        for (MySqlParser.TableSourceItemContext tableSourceItem : tableSourceItems) {
            List<MySqlParser.TableNameContext> tableNameContexts = tableSourceItem.getRuleContexts(MySqlParser.TableNameContext.class);
            for (MySqlParser.TableNameContext tableNameContext : tableNameContexts) {
                tableNameSet.add(tableNameContext.getText());
            }
        }
    }

    public Set<String> getTableNameSet() {
        return tableNameSet;
    }
}

然后调用:

    public static void main(String[] args) {
        String sql = "SELECT t1.column1,t1.column2,t1.column3,t2.xy from tableC t1 left join tableA t2 on t1.id=t2.oid where t1.column1 = 1 and t2.yy=6";
        System.out.println(sql);
        MySqlLexer lexer = new MySqlLexer(CharStreams.fromString(sql.toUpperCase()));
        MySqlParser parser = new MySqlParser(new CommonTokenStream(lexer));
        //定义GetTableNamesListener
        GetTableNamesListener listener = new GetTableNamesListener();
        ParseTreeWalker.DEFAULT.walk(listener, parser.sqlStatements());
        Set<String> tableNameSet = listener.getTableNameSet();
        for (String tableName : tableNameSet) {
            System.out.println(tableName);
        }
    }

其输出结果如下:

SELECT t1.column1,t1.column2,t1.column3,t2.xy from tableC t1 left join tableA t2 on t1.id=t2.oid where t1.column1 = 1 and t2.yy=6
TABLEA
TABLEC
Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐