Eclipse JDT AST的基本使用

Eclipse JDT AST 是当前语法树工具中,目前我使用过的最稳定的工具,但是奈何当前的文档比较少,而AST是代码分析的基础,这篇文章主要是介绍JDT AST的核心类的使用。

Eclipse中的Eclipse JDT提供了一组访问和操作Java源代码的API,Eclipse AST是其中一个重要组成部分,它提供了AST、ASTParser、ASTNode、ASTVisitor等类,通过这些类可以获取、创建、访问和修改抽象语法树。

参考:

语法树分析器——ASTParser

在我们在将源代码转换成AST之前,需要使用 ASTParser。Eclipse AST提供了ASTParser类用于解析源代码,ASTParser有两种导入源代码的方式,一种是以Java Model的形式,另一种是以字符数组的形式。ASTParser的创建与使用如下:

ASTParser astParser = ASTParser.newParser(/*API Level*/);
astParser.setSource(/*Source Code*/);
复制代码

参数说明如下:

  • API Level:Java编程规范(Java Language Specification,简写为JLS),此参数为常量,例如AST.JLS3。
  • Source Code:方法setSource()针对不同形式的源代码作为参数而进行了重载,主要分类为字符数组形式(char[])和JavaModel形式(ICompilationUnit、IClassFile等)。

如果传入的字符数组不是完整的一个Java文件,而是一个表达式或语句,又怎么办呢?可以按照以下代码对ASTParser进行设置:

astParser.setKind(/*Kind of Construct*/);
复制代码

其中Kind of Construct是所需解析的代码的类型,包括:

  • K_COMPILATION_UNIT:编译单元,即一个Java文件
  • K_CLASS_BODY_DECLARATIONS:类的声明
  • K_EXPRESSION:单个表达式
  • K_STATEMENTS:语句块

创建并设定好ASTParser后,便可以开始源代码与AST的转换,代码如下:

CompilationUnit result = (CompilationUnit) (astParser.createAST(null));
复制代码

createAST()方法的参数类型为IProgressMonitor,用于对AST的转换进行监控,不需要的话就填个null即可。本代码示例是以待解析的源代码为一完整的Java文件(对应一个编译单元Compilation Unit)为前提的,所以在转换为AST后直接强制类型转换为CompilationUnit。CompilationUnit是ASTNode的子类,指的就是整个Java文件,也是AST的根节点。

AST根节点——CompilationUnit

获取 Compilation Unit 的代码

/**
 * get compilation unit of source code
 *
 * @param javaFilePath
 * @return CompilationUnit
 */
public static CompilationUnit getCompilationUnit(String javaFilePath) {

    byte[] input = null;
    try {
        BufferedInputStream bufferedInputStream = new BufferedInputStream(new FileInputStream(javaFilePath));
        input = new byte[bufferedInputStream.available()];
        bufferedInputStream.read(input);
        bufferedInputStream.close();
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    }

    /*
    创建抽象语法书解析器,按照Java语言规范Java SE 8 Edition (JLS8)解析,
    支持操作所有jdk8版本的Java代码操作
     */
    ASTParser astParser = ASTParser.newParser(AST.JLS8);

    /*
     Java编译参数,默认astKind类型为编译单元K_COMPILATION_UNIT,解析器将源码会解析为一个编译单元类型CompilationUnit
     K_COMPILATION_UNIT:包含所有的类型声明、import、package等等节点
     K_CLASS_BODY_DECLARATIONS:仅将源码的class body(含子节点)部分解析。不包含类型、import、package等信息
     K_STATEMENTS:源码将被解析为一个语句序列statements数组,package、import节点会被解析成assert语句,其余节点正常解析
     K_EXPRESSION:源码将被解析为一个表达式,如果将一个类的源码传入,返回的将会是一对解析失败的报错,类的import等节点均不是表达式
     */
    astParser.setKind(ASTParser.K_COMPILATION_UNIT);

    /*
    ASTParser绑定源码
     */
    astParser.setSource(new String(input).toCharArray());

    /*
    请求编译器为其创建的AST节点提供绑定信息。默认为false,如果设置为true,那么可以该去该语法树节点的全限定类名等信息。
     */
    astParser.setResolveBindings(true);

    // 如果不设置 setResolveBindings是无效的,四个属性配置其中给一个即可,如果不配置全部默认为null
    // 第1个参数 classpathEntries:当没有Java project时(直接解析某个文件),解析绑定信息使用的classpath,例如:String[] classpathEntries = new String[]{"D:\Applications\apache-maven-3.5.0\conf\repository\org\springframework\spring-beans\4.3.20.RELEASE\spring-beans-4.3.20.RELEASE.jar"};
    // 第2个参数 sourcepathEntries:当没有Java project时(直接解析某个文件),解析绑定信息使用的sourcepath,例如:String[] sourcepathEntries = new String[]{"D:\Applications\apache-maven-3.5.0\conf\repository\org\springframework\spring-beans\4.3.20.RELEASE\spring-beans-4.3.20.RELEASE-sources.jar"};
    // 第3个参数 sourcepathsEncodings:sourcepaths条目解析使用编码
    // 第4个参数 includeRunningVMBootclasspath:运行时VM的bootclasspath追加至给定的classpath
    astParser.setEnvironment(null, null, null, true);

    // 如果开启则允许编译器创建包含语法错误的语句,当解析时发现语法错误会保留statementsRecoveryData,
    // 用于语句恢复,获取恢复数据的方法不希望被客户端使用,仅供框架内部使用
    astParser.setStatementsRecovery(true);

    /*
    这个函数一定要调用,参数随便设置一个字符串都可以。不调用 在一些地方会出现resolveBinding()为空的情况。
    在使用setSource(char [])方法的时候,后面要加上setEnvironment和setUnitName方法。setEnvironment方法单独使用是可以加入具体参数,但是setUnitName方法也使用的时候,参数只能用(null, null, null, true)才跑的出结果。
     */
    astParser.setUnitName("example.java");

    /*
    createAST()方法的参数类型为IProgressMonitor,用于对AST的转换进行监控,不需要的话就填个null即可。
     */
    CompilationUnit compilationUnit = (CompilationUnit) (astParser.createAST(null));

    return compilationUnit;
}
复制代码

Eclipse AST 的核心类:ASTNode、ASTVisitor

  • ASTNode:AST节点类,所有的节点类型都是ASTNode的子类,至于某一个节点源代码具体到底是什么节点,可以通过ASTView来查看。
  • ASTVisitor:AST访问者类,它针对不同类型的节点提供了一系列重载的visit()和endvisit()方法,意思就是访问到该类型的节点时执行visit()方法,访问完该类型节点后执行endvisit()方法。其中,visit()方法需返回boolean类型的返回值,表示是否继续访问子节点。另外ASTVisitor还提供了preVisit()和postVisit()方法,参数类型为ASTNode,也就是说不论哪种类型的节点,访问前后都要分别执行preVisit()和postVisit()。这些方法的具体实现由ASTVisitor的子类负责,如果不需要对所访问到的节点做处理,则无需在ASTVisitor的子类中覆盖这些方法。

Eclipse AST访问节点这一部分的设计采用了访问者模式,不同类型的节点是待访问的具体元素,ASTNode充当抽象元素角色,ASTVisitor充当抽象访问者,而我们自己写的ASTVisitor的子类充当具体访问者,而程序代码就是对象结构,包含了不同种类的节点供访问者来访问。

在使用 Eclipse AST访问节点时需要先声明一个类继承自ASTVisitor,即增加具体访问者类,并覆盖相应的方法,编写需执行的操作,实例化这个访问者类后调用ASTNode的accept()方法,将ASTVisitor作为参数传入,就可以执行访问了,此处对应了访问者模式的“双重分派”机制,如果不熟悉的童鞋,可以先学习一下访问者模式。

下面给出一个示例,实现输出给定Java文件中所声明的类名、方法名和属性名,基本步骤如下:

(1) 通过ASTView确定类、方法和属性的声明对应的AST节点分别是TypeDeclaration、MethodDeclaration和FieldDeclaration,FieldDeclaration类比较特殊,因为一个FieldDeclaration下可能有多个同类型的属性被声明,形如“privateint a, b;”。

(2) 创建一个新的访问者子类,继承自ASTVisitor,并覆盖参数类型为TypeDeclaration、MethodDeclaration和FieldDeclaration的visit()方法,由于需要遍历所有节点,因此将返回值都设为true。

(3) TypeDeclaration、MethodDeclaration可以直接获得相关的名字并输出,而FieldDeclaration需要遍历它的子节点VariableDeclarationFragment,由于FieldDeclaration提供了返回所有VariableDeclarationFragment的方法,因此这里直接采用for循环遍历即可。

import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.FieldDeclaration;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.TypeDeclaration;
 
public class DemoVisitor extends ASTVisitor {
 
	@Override
	public boolean visit(FieldDeclaration node) {
		for (Object obj: node.fragments()) {
			VariableDeclarationFragment v = (VariableDeclarationFragment)obj;
			System.out.println("Field:\t" + v.getName());
		}
		
		return true;
	}
 
	@Override
	public boolean visit(MethodDeclaration node) {
		System.out.println("Method:\t" + node.getName());
		return true;
	}
 
	@Override
	public boolean visit(TypeDeclaration node) {
		System.out.println("Class:\t" + node.getName());
		return true;
	}
}
复制代码

下面提供一个测试类,对上述解析源代码的工具类(JdtAstUtil)和访问AST的访问者类(DemoVisitor )进行测试:

import org.eclipse.jdt.core.dom.CompilationUnit;
 
import com.ast.util.JdtAstUtil;
import com.ast.visitor.DemoVisitor;
 
public class DemoVisitorTest {
	
	public DemoVisitorTest(String path) {
		CompilationUnit comp = JdtAstUtil.getCompilationUnit(path);
		
		DemoVisitor visitor = new DemoVisitor();
		comp.accept(visitor);
	}
}
复制代码

提供一个待处理的简单Java源代码文件(ClassDemo.java)如下:

public class ClassDemo {
	
	private String text = "Hello World!", text2;
	
	public void print(int value) {
		System.out.println(value);
	}
	
	public void input(String value) {
		text2 = value;
	}
}
复制代码

显示结果为:

P.S.,本实例工程中需要导入的jar包如下:

org.eclipse.core.contenttype.jar

org.eclipse.core.jobs.jar

org.eclipse.core.resources.jar

org.eclipse.core.runtime.jar

org.eclipse.equinox.common.jar

org.eclipse.equinox.preferences.jar

org.eclipse.jdt.core.jar

org.eclipse.osgi.jar

猜你喜欢

转载自juejin.im/post/7032675224399118372
今日推荐