一、JDT AST介绍

1.概念

抽象语法树(Abstract Syntax Tree, AST)使用树形结构来表示源代码的抽象语法结构,树上的每一个节点都对应源代码中的一种结构。

2.举例感受抽象语法树

①运算表达式

表达式:1+2*(3-4)+5

抽象语法树:

如何实现java抽象语法树 java抽象语法树ast_语法树

②代码语句块

如何实现java抽象语法树 java抽象语法树ast_子类_02


抽象语法树:

如何实现java抽象语法树 java抽象语法树ast_语法树_03

③Java文件

如何实现java抽象语法树 java抽象语法树ast_访问者_04


如何实现java抽象语法树 java抽象语法树ast_语法树_05

3.AST中3个关键类

①org.eclipse.jdt.core.dom.AST

作用:AST的工厂类,用于创建表示各种语法结构的节点。一般用于创建AST。

②org.eclipse.jdt.core.dom.ASTNode及其子类(AST节点类)

作用:用于表示Java中的所有语法结构。每个具体的子类,都代表着一种Java语法结构。某一个节点源代码具体到底是什么类型的节点,可以通过ASTView来查看。

  1. 安装eclipse astview插件。
  2. 分析java demo代码,观察语法树的各个节点和层级关系。
  3. ASTNode的主要子类:



    Expression系列如下:
  4. 编译单元作为根节点的一般树状节点模型(叶子节点基本为名称、操作符和字面量):

③org.eclipse.jdt.core.dom.ASTVisitor(ASTVisitor类)

如何获取具体ASTNode节点中的数据呢?

  1. 使用具体ASTNode的特性属性、特性方法获取。
  2. ASTVisitor:AST访问者类获取。介绍如下:
    它针对不同类型的节点提供了一系列重载的visit()和endvisit()方法,意思就是访问到该类型的节点时执行visit()方法,访问完该类型节点后执行endvisit()方法。其中,visit()方法需返回boolean类型的返回值,表示是否继续访问子节点。
    这些方法的具体实现由ASTVisitor的子类负责,如果不需要对所访问到的节点做处理,则无需在ASTVisitor的子类中覆盖这些方法。
    Eclipse AST访问节点这一部分的设计采用了访问者模式,不同类型的节点是待访问的具体元素,ASTNode充当抽象元素角色,ASTVisitor充当抽象访问者,而我们自己写的ASTVisitor的子类充当具体访问者,而程序代码就是对象结构,包含了不同种类的节点供访问者来访问。
    在使用 Eclipse AST访问节点时需要先声明一个类继承自ASTVisitor,即增加具体访问者类,并覆盖相应的方法,编写需执行的操作,实例化这个访问者类后调用ASTNode的accept()方法,将ASTVisitor作为参数传入,就可以执行访问了,此处对应了访问者模式的“双重分派”机制。
    测试代码:
public class Demo1 {

	/*
	 * 字段1、字段2
	 */
	private String field1 = "Hello World", field2;

	/**
	 * 
	 * @param d1 被除数
	 * @param d2 除数
	 * @return
	 */
	@RequestMapping("/divide")
	public Double divide(Double d1, Double d2) {
		Double result = null;
		if (d2 == null) {
			result = (double) 0;
		} else {
			result = d1 / d2;
			for (int i = 0; i < 10; i++) {
				result /= d2;
			}
		}
		return result;
	}

	// 加法
	public void plus(int a, int b) {
		System.out.println(a + b);
	}

	/*
	 * 字段3
	 */
	private String field3;

}

ASTVisitor子类:

public class DeclarationSeriesVisitor extends ASTVisitor {

	@Override
	public boolean visit(FieldDeclaration node) {
		List fragments = node.fragments();
		for (Object obj : fragments) {
			VariableDeclarationFragment v = (VariableDeclarationFragment) obj;
			System.out.println("Field:" + v.getName());
		}
		return true;
	}

	@Override
	public boolean visit(MethodDeclaration node) {
		System.out.println("Method:" + node.getName());
		return true;
	}

	@Override
	public boolean visit(TypeDeclaration node) {
		System.out.println("Class:" + node.getName());
		return true;
	}

}

客户端类:

public class App {

	public static void main(String[] args) {
		String javaFilePath = "F:\\workspace\\codeview-demo\\src\\main\\java\\com\\heshj\\codeview_demo\\Demo1.java";
		CompilationUnit compilationUnit = JdtAstUtil.getCompilationUnit(javaFilePath);

		DeclarationSeriesVisitor declarationSeriesVisitor = new DeclarationSeriesVisitor();
		compilationUnit.accept(declarationSeriesVisitor);
	}

}

debug跟一遍代码,感受一下这个具体代码树。

4.使用AST解析代码视图,需要注意的问题

①个人理解

ast使用树状结构存储,采用组合模式分析。我们试想,如果组合模式中的各个节点,都拥有相同的接口操作的话,操作将变得简单,在进行树遍历的时候,甚至可以不用在意具体类的实现。但是分析语法树是一件很严谨的事,由于Java语法很多,因此各个具体节点类都存在或多或少的差异。ast牺牲了通用性,采用各个具体子类具体分析,从而能够看出分析的复杂性。
优点:
1.ast采用ASTVisitor访问者模式来获取想要的任意节点信息;
2.其内部各个具体子节点类,已经封装好遍历下属节点的操作。我们无需自己去遍历树节点,自己遍历也遍历不完。
3 建议获取数据都采用访问者的方式获取,充分利用ast各个节点封装的解析。
缺点:
1.跨文件:ast只能分析彼此独立的java文件(编译单元),以编译单元为根节点的树形节点之间的关联,ast没有提供支持。举个例子,A文件中调用B文件的某个方法,那么在解析A文件这个编译单元时,访问到该调用方法表达式节点的时候,无法指向B文件对应的哪个申明方法节点。因此跨文件调用方法、获取属性字段等,将受到限制。
2.运行时:继承、接口实现、抽象的方法调用、属性继承等操作,无法找到对应的具体类。因为ast是静态代码扫描,无法获取到运行时的具体类型,这是一个局限。

针对上述两个不足,我们应该如何处理呢?