网站首页 文章专栏 深入druid之sqlparse,源码解读之visitor
深入druid之sqlparse,源码解读之visitor

一. visitor的作用,分类

第一篇博客我们写的demo将一个sql解析成语法树,并通过SchemaStatVisitor拿到了sql的表名信息,字段信息,也可以动态改写sql,移除,增加条件等。

前文博客链接:深入druid之sqlparse,sql语法是如何被解析的

Visitor是遍历AST的手段,是处理AST最方便的模式,Visitor是一个接口,有缺省什么都没做的实现VistorAdapter。我们可以实现不同的Visitor来满足不同的需求,Druid内置提供了如下Visitor:

    - OutputVisitor用来把AST输出为字符串

    WallVisitor 来分析SQL语意来防御SQL注入攻击

    ParameterizedOutputVisitor用来合并未参数化的SQL进行统计

    EvalVisitor 用来对SQL表达式求值

    ExportParameterVisitor用来提取SQL中的变量参数

    SchemaStatVisitor 用来统计SQL中使用的表、字段、过滤条件、排序表达式、分组表达式

    SQL格式化 Druid内置了基于语义的SQL格式化功能


而且如果我们想要实现自己的逻辑,还可以自定义visitor,每种方言的Visitor都有一个缺省的VisitorAdapter,mysql等其他数据类型也都是这么实现的。


二. visitor的位置

1636552667(1).jpg


在visitor目录下就是了,SQLASTVisitor作为顶层接口,定义了大量的default接口,其他的visitor都是通过实现它来处理的。

public interface SQLASTVisitor {

    default void endVisit(SQLAllColumnExpr x) {
    }

    default void endVisit(SQLBetweenExpr x) {
    }

    default void endVisit(SQLBinaryOpExpr x) {
    }

    default void endVisit(SQLCaseExpr x) {
    }

    default void endVisit(SQLCaseExpr.Item x) {
    }

    default void endVisit(SQLCaseStatement x) {
    }

    default void endVisit(SQLCaseStatement.Item x) {
    }

    default void endVisit(SQLCharExpr x) {
    }

    default void endVisit(SQLIdentifierExpr x) {
    }

    default void endVisit(SQLInListExpr x) {
    }

    default void endVisit(SQLIntegerExpr x) {
    }

    default void endVisit(SQLSmallIntExpr x) {
    }

    default void endVisit(SQLBigIntExpr x) {
    }

    default void endVisit(SQLTinyIntExpr x) {
    }

    default void endVisit(SQLExistsExpr x) {
    }

    default void endVisit(SQLNCharExpr x) {
    }

    default void endVisit(SQLNotExpr x) {
    }

    default void endVisit(SQLNullExpr x) {
    }
    
    ...
}

怎么使用visitor呢?

MySqlSchemaStatVisitor mysqlVisitor = new MySqlSchemaStatVisitor();
statement.accept(mysqlVisitor);

System.out.println("使用visitor数据表:" + mysqlVisitor.getTables());
System.out.println("使用visitor字段:" + mysqlVisitor.getColumns());
System.out.println("使用visitor条件:" + mysqlVisitor.getConditions());
System.out.println("使用visitor分组:" + mysqlVisitor.getGroupByColumns());
System.out.println("使用visitor排序:" + mysqlVisitor.getOrderByColumns());

创建一个mysqlVisitor,将之前生成的语法树accept这个visitor,在这里 statement 的实际类型是 SQLSelectStatement,然后就可以使用这个visitor获取期望的信息了。

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

我们看下accept的实现,其定义是在SQLObject接口中,在SQLObjectImpl抽象类中实现:

// 定义
public interface SQLObject {
    void accept(SQLASTVisitor var1);
    ... 
}

// 实现
public abstract class SQLObjectImpl implements SQLObject {
    protected SQLObject parent;
    protected Map attributes;
    protected SQLCommentHint hint;
    protected int sourceLine;
    protected int sourceColumn;

    public SQLObjectImpl() {
    }

    public final void accept(SQLASTVisitor visitor) {
        if (visitor == null) {
            throw new IllegalArgumentException();
        } else {
            visitor.preVisit(this);
            this.accept0(visitor);
            visitor.postVisit(this);
        }
    }
    
    protected abstract void accept0(SQLASTVisitor var1);
    ...
}

这是一个 final 方法,意味着所有的子类都要遵循这个模板,首先 accept 方法前和后,visitor 都会做一些工作(preVisit(this),postVisit(this))。真正的访问流程定义在 accept0() 方法里,而它是一个抽象方法。前面我们说了,无论是高层次还是低层次的元素,都是一个 SQLObject,而每个SQLObject都有 accept0 方法接受一个visitor,去根据自己的特性遍历自己,每个属性又可以再次通过接受的vistior遍历自己的子元素,每个子元素也是一个SQLObject,也是通过accept0方法进行处理。

所以总的来说,SQLObject 是负责通知 visitor 要访问自己的哪些元素,使用 元素.accept(visitor)去递归访问,accept方法则会回到上面代码,访问前置 -> 访问自己accept0(visitor) -> 访问后置,accept0是实际处理的地方

visitor 则通过visitor.visit(this)负责访问相应元素,并返回bool类型结果表示是否继续访问,大概就是按照dfs,前序遍历的方式的遍历自己的属性。最后通过visitor.endVisit(this);来终止递归。

总结下就是,accept是负责通知哪些元素需要被访问,只关注是否需要被访问,是用来控制递归循环的。visitor.visit()则是具体的访问逻辑,不同的visitor处理不同的类型逻辑是不一样的,所以我们可以自定义visitor,实现不同类型节点作为入参的visit方法,那么用这个visitor去访问这个类型的节点时,就会走我们的逻辑。如:

public class ExportTableAliasVisitor extends MySqlASTVisitorAdapter {
    private Map aliasMap = new HashMap();
    public boolean visit(SQLExprTableSource x) {
        String alias = x.getAlias();
        aliasMap.put(alias, x);
        return true;
    }

    public Map getAliasMap() {
        return aliasMap;
    }
}


// 使用自定义visitor访问
ExportTableAliasVisitor visitor = new ExportTableAliasVisitor();
statement.accept(visitor);

SQLTableSource tableSource = visitor.getAliasMap().get("a");
System.out.println("别名为a的数据表:" + tableSource);

这里我们继承MySqlASTVisitorAdapter,重写visit(SQLExprTableSource x)方法,那么这个我们自己定义的visitor在使用时,就会走这个逻辑。


在看源码过程中,针对不同类型的节点都有不同的逻辑,而且要结合sql语法,所以挺复杂的。这一块也是粗略的研究理解,如有错误,还请留言指出。





版权声明:本文由星尘阁原创出品,转载请注明出处!

本文链接:http://www.52xingchen.cn/detail/91




赞助本站,网站的发展离不开你们的支持!
来说两句吧
大侠留个名吧,或者可以使用QQ登录。
: 您已登陆!可以继续留言。
最新评论