访问者模式及其在 Java Parser 中的应用

用户头像
maijun
关注
发布于: 2020 年 12 月 14 日

一、概述

访问者模式,是Java设计模式中广泛使用的一种设计模式,尤其是在AST的遍历中使用更加普遍。在静态代码分析中,有部分类型的缺陷,可以通过简单遍历AST结构完成,例如在finally中执行return操作,空的catch块等。很多静态代码分析工具,也就应用了这种设计模式,例如Java静态代码分析工具PMD,而且,很多AST中间表示,已经直接对访问者模式进行了支持,例如Eclipse JDT、JavaParser等。

本文将首先介绍访问者模式的概念和相关知识,然后通过JavaParser的源码分析,介绍访问者模式具体的应用。

二、访问者模式介绍

2.1 引例

首先我们通过一个案例来引入对访问者模式的介绍。

杭州是一个美丽的城市,每年会有大量的游客到杭州过来游玩,美丽的西湖和香火鼎盛的灵隐寺都是游客必往之地。而对于美丽的西湖,又有断桥残雪、三潭印月等西湖十景,灵隐寺景区还有济公殿、大雄宝殿等不同的建筑。

我们假定每个游客到达杭州,就会游玩上面所有的地方,我们就可以针对这位游客的这次游玩进行打包操作,不需要对游玩断桥残雪进行特殊操作。我们应该怎么办呢?

图2.1 杭州游览路线

首先,对于上面每个地方,每个人都可能有不同的访问行为,有游客到济公殿会看看墙上济公的故事,有的游客可能就对济公殿很熟悉,进去拜一下就出来了,有的到了断桥残雪感慨一句全是人头,也有人到了断桥残雪,就会会回味一下张祜的诗句。

其次,对于上面每个地方,对于不同的游客来说,不确定,所以,不管是谁,给他准备个入口进来总不会有问题。

结合上面分析,我们可以初步分析访问者模式是怎么一回事儿:在每个游客自己内部,把对每个景点想操作的行为定义出来,然后在每个景点,准备个接口,把游客迎进来,最后呢,把这个游客迎进来之后呢,执行这个游客里面定义的,关于自己的行为

图2.2 访问者模式之游览杭州

抽象访问者(Visitor,游客)角色:声明访问者可以访问哪些元素(景点),具体到程序中就是visit方法中的参数定义哪些对象是可以被访问的。该操作接口的名字和参数标识了发送访问请求给具体访问者的具体元素角色。这样访问者就可以通过该元素角色的特定接口直接访问它;

具体访问者(Concrete Visitor,张三和李四)角色:实现抽象访问者所声明的方法接口,也就是抽象访问者所声明的各个访问操作。它影响到访问者访问到一个对象(景点)后该干什么,要做什么事情;

抽象节点(Node,景点)角色:声明接受哪一类访问者访问,程序上是通过accept方法中的参数来定义的,该方法接受一个访问者对象作为一个参数。并且声明了在该节点上可以进行的操作;

具体元素(Concrete Element,三潭印月、济公殿等)角色:实现抽象节点类所声明的accept方法,通常都是visitor.visit(this),基本上已经形成一种定式了,并实现该具体节点上可以进行的操作;

结构对象(Object  Structure)角色:可以遍历结构中的所有元素;可以提供一个高层次的接口让访问者对象可以访问每一个元素;可以设计成一个复合对象或者一个聚集,例如List,在我们实际静态代码分析中,是一个树形结构。

2.2示例代码

下面这部分贴出来上面示例的代码,为了方便对应,命名也都还采用了中文命名。

景点接口及其实现:

public interface 景点 {
public void accept(游客 visitor);
}
public class 杭州 implements 景点{
@Override
public void accept(游客 visitor) {
visitor.visit(this);
}
}
public class 西湖 implements 景点{
@Override
public void accept(游客 visitor) {
visitor.visit(this);
}
}
public class 断桥残雪 implements 景点{
@Override
public void accept(游客 visitor) {
visitor.visit(this);
}
}
public class 三潭印月 implements 景点{
@Override
public void accept(游客 visitor) {
visitor.visit(this);
}
}
public class 灵隐寺 implements 景点{
@Override
public void accept(游客 visitor) {
visitor.visit(this);
}
}
public class 大雄宝殿 implements 景点{
@Override
public void accept(游客 visitor) {
visitor.visit(this);
}
}
public class 济公殿 implements 景点{
@Override
public void accept(游客 visitor) {
visitor.visit(this);
}
}

访问者接口及其实现:

public interface 游客 {
public void visit(杭州 景点);
public void visit(西湖 景点);
public void visit(三潭印月 景点);
public void visit(断桥残雪 景点);
public void visit(灵隐寺 景点);
public void visit(大雄宝殿 景点);
public void visit(济公殿 景点);
}
public class 张三游客 implements 游客 {
@Override
public void visit(杭州 景点) {
System.out.println("张三来到杭州");
}
@Override
public void visit(西湖 景点) {
System.out.println("张三来到西湖");
}
@Override
public void visit(三潭印月 景点) {
System.out.println("张三来到三潭印月");
}
@Override
public void visit(断桥残雪 景点) {
System.out.println("张三来到断桥残雪,人真多");
}
@Override
public void visit(灵隐寺 景点) {
System.out.println("张三来到灵隐寺");
}
@Override
public void visit(大雄宝殿 景点) {
System.out.println("张三来到大雄宝殿");
}
@Override
public void visit(济公殿 景点) {
System.out.println("张三来到济公殿");
}
}
public class 李四游客 implements 游客 {
@Override
public void visit(杭州 景点) {
System.out.println("李四来到杭州");
}
@Override
public void visit(西湖 景点) {
System.out.println("李四来到西湖");
}
@Override
public void visit(三潭印月 景点) {
System.out.println("李四来到三潭印月");
}
@Override
public void visit(断桥残雪 景点) {
System.out.println("李四来到断桥残雪,哈哈哈");
}
@Override
public void visit(灵隐寺 景点) {
System.out.println("李四来到灵隐寺");
}
@Override
public void visit(大雄宝殿 景点) {
System.out.println("李四来到大雄宝殿");
}
@Override
public void visit(济公殿 景点) {
System.out.println("李四来到济公殿");
}
}

景点结构实现:

import java.util.ArrayList;
import java.util.List;
public class 景点结构 {
// 树形结构更能说明静态代码分析AST问题,也更能模拟该场景
// 这里图省事儿,就拿一个List表示了
List<景点> nodes = new ArrayList<>();
public void visitAll(游客 visitor) {
for(景点 node : nodes) {
node.accept(visitor);
}
}
public void add(景点 node) {
nodes.add(node);
}
}

示例代码入口:

public class Client {
public static void main(String[] args) {
景点结构 structure = new 景点结构();
structure.add(new 杭州());
structure.add(new 西湖());
structure.add(new 三潭印月());
structure.add(new 断桥残雪());
structure.add(new 灵隐寺());
structure.add(new 大雄宝殿());
structure.add(new 济公殿());
游客 zhangsanVisitor = new 张三游客();
structure.visitAll(zhangsanVisitor);
游客 lisiVisitor = new 李四游客();
structure.visitAll(lisiVisitor);
}
}



三、基于包装器的访问者模式访问优化

上面介绍访问者模式时,我们构造了一个List类型的景点结构,然后在访问时,我们自己在景点结构中执行了遍历操作,遍历所有的节点,然后分别执行节点的accept方法,接受访客的访问。那么在结构很复杂的场景下,我们还适合这样遍历吗?下面,我们将切到我们主要想介绍的编程语言的AST遍历上,来讨论这个问题。

因为编程语言的语法结构,语法是固定的,但是,基于这些语法,却可以有非常多的实现,if是一个Statement,包含有condition(条件)和实现的body,而body又可以有非常多的Statement组成,当然就可以包含if语句。虽然当我们将一个文件,比如Java文件的AST完整构造出来,那么它的AST就已经确定了,然后我们就可以在AST中,使用visitAll来递归遍历AST中所有的节点,包括每个节点的子节点,但是却非常麻烦。

在这里,我们来介绍一种基于包装器的优化访问者模式的访问的方法,这种方法,也是JavaParser中采用的。

首先,我们再拿前面的游客来杭州游玩来举例子,游客可以自己过来玩,当然就要自己设计游览路线,所有的事情都要自己做,但是同样,可以跟团来旅游啊!跟团过来旅游,就相当于创建了一个包装器,旅行团将整个旅游过程打包了,游客需要跟团走,然后在每个景点可以自己游玩。比如首先旅游团把游客带到断桥残雪,然后游客发出“全是人头”的感慨,然后再听导游介绍断桥残雪的典故,然后,有旅游团将你带到下一个景点(例如三潭音乐),整个过程,游客只需要对自己行为进行定义,然后就是听旅游团安排。

其中:

旅游团:就相当于一个包装类,对旅游的路线进行包装,也可以定义自己的旅游的行为,例如某些介绍,一般只做路径遍历(旅游路线)的规划;

游客:在包装器模式下,游客在实现好自己的visit行为之后,需要调用旅游团(即父类)的visit的行为,所以,一般我们写好visit方法,通常还要调用super.visit方法。

这里包装器,只是一种简单的包装器模式,如果想详细了解包装器模式的使用,可以自己搜索相关资料。

四、JavaParser中访问者模式应用

这里,我们通过实现一个检测空的catch块的功能,简单介绍如何自己定义访问器,并且介绍其中访问者模式和包装器模式的应用。在JavaParser中,Catch块类型为CatchClause。

首先,在VoidVisitor 接口中,定义了一个访问访问CatchClause的接口:

void visit(CatchClause n, A arg);

其中,A为泛型,表示可以传入的数据类型,我们简单示例,实现时,A就为Object。

然后,在包装器类中,定义了VoidVisitorAdapter中,定义了一个实现:

public void visit(final CatchClause n, final A arg) {
n.getBody().accept(this, arg);
n.getParameter().accept(this, arg);
n.getComment().ifPresent((l) -> {
l.accept(this, arg);
});
}

如上,在包装器类中,对CatchClause依次获取所有的子节点,然后使用放弃访问器进行遍历(实际上,就是定义了遍历AST节点的路径,AST是一种树状结构,依次访问每个节点的子节点,可以最终完成对整棵树的遍历)。

我们自己实现一个访问器:

import com.github.javaparser.ast.stmt.CatchClause;
import com.github.javaparser.ast.stmt.Statement;
import com.github.javaparser.ast.visitor.VoidVisitorAdapter;
import java.util.List;
public class EmptyCatchBlockRule extends VoidVisitorAdapter<Object> {
@Override
public void visit(final CatchClause n, final Object arg) {
List<Statement> statements = n.getBody().getStatements();
if (statements == null || statements.size() == 0) {
System.out.println("empty catch block found at line: " + n.getBegin().get().line);
}
super.visit(n, arg);
}
}

如上,因为我们只关心空的Catch块的问题,因此我们将只对visit CatchClause类型节点的实现,当当前访问器访问到我们自己没有实现的节点时,将直接调用包装器中的实现,而包装器实现的,就是获取所有的子节点来遍历,直到再次找到CatchClause,来执行自己写的访问器实现。

当然,如第三部分的介绍,最后的super.visit(n, arg); 语句是不可缺少的,因为Catch块中,仍然可以包含Catch块,如果我们在这部分没有调父类的visit方法,将导致当前Catch块中的子Catch块无法遍历到。

最后,我们写一个入口类,对此进行检查:

import com.github.javaparser.JavaParser;
import com.github.javaparser.ParseResult;
import com.github.javaparser.ast.CompilationUnit;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.nio.charset.Charset;
public class Main {
public static void main(String[] args) throws FileNotFoundException {
String sourcePath = "D:\\workspace\\idea\\test\\scs-java\\src\\main\\java\\zmj\\cert\\analyze\\rule\\Test.java";
FileInputStream in = new FileInputStream(sourcePath);
ParseResult<CompilationUnit> result = new JavaParser().parse(in, Charset.defaultCharset());
if (result.getResult().isPresent()) {
result.getResult().get().accept(new EmptyCatchBlockRule(), null);
}
}
}



这里,我们只需要对测试的代码(Test.java)整体生成AST,然后调用accept方法,就可以完成对整棵树的遍历(遍历操作在包装器类中实现),上面代码执行完,结果如下:

empty catch block found at line: 8

五、总结

本文中,介绍了如下内容:

(1) 介绍了访问者模式的基本概念和思路,并结合案例,详细介绍了访问者模式的使用方法;

(2) 介绍了如何通过访问者模式,结合使用简单包装器模式,简化复杂元素结构的遍历;

(3) 介绍了JavaParser中,访问者模式的基本应用;

(4) 介绍了一个简单的Java缺陷的检查的代码,实际上,Java静态代码分析工具PMD,就是基于该思路进行的开发。

本文主要目的是介绍访问者模式在JavaParser中的应用,或者更进一步,介绍访问者模式如果在基于AST遍历的静态代码分析工具中的缺陷检查上的应用(只要是能够生成AST的语言,都可以有类似实现)。

 

发布于: 2020 年 12 月 14 日阅读数: 30
用户头像

maijun

关注

还未添加个人签名 2019.09.20 加入

还未添加个人简介

评论

发布
暂无评论
访问者模式及其在Java Parser中的应用