网站首页 文章专栏 深入druid之sqlparse,源码解读之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目录下就是了,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语法,所以挺复杂的。这一块也是粗略的研究理解,如有错误,还请留言指出。
版权声明:本文由星尘阁原创出品,转载请注明出处!