通过上篇文章(【Eclipse AST】AST与ASTView简介),大家已经知道什么是抽象语法树?它长什么样子?那么到底如何将它运用到代码分析、优化与重构中呢?从本文开始将由浅入深向大家介绍AST的使用。
Eclipse中的Eclipse JDT提供了一组访问和操作Java源代码的API,Eclipse AST是其中一个重要组成部分,它提供了AST、ASTParser、ASTNode、ASTVisitor等类,通过这些类可以获取、创建、访问和修改抽象语法树。
首先需要掌握如何将Java源代码转换为AST,即解析源代码。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,其中getCompilationUnit()方法的输入参数为需解析的Java源代码文件路径,返回值为该文件对应的CompilationUnit节点:
import java.io.BufferedInputStream; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import org.eclipse.jdt.core.dom.AST; import org.eclipse.jdt.core.dom.ASTParser; import org.eclipse.jdt.core.dom.CompilationUnit; public class JdtAstUtil { /** * 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(); } ASTParser astParser = ASTParser.newParser(AST.JLS3); astParser.setSource(new String(input).toCharArray()); astParser.setKind(ASTParser.K_COMPILATION_UNIT); CompilationUnit result = (CompilationUnit) (astParser.createAST(null)); return result; } }
至此便将源代码转化为了AST,接下来要做的就是访问节点,需要学习两个Eclipse AST的核心类:
● ASTNode:AST节点类,在上一篇文章和上文中都有提到,由于节点种类太多,在此不一一赘述(后面专门写一篇独立的文章来介绍各种不同类型的ASTNode),但需要明确的是所有的节点类型都是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; } }
显示结果为:
图1 测试运行结果
上述示例代码DemoVisitor还有点小瑕疵,例如TypeDeclaration节点其实不仅表示了类的声明,还包括了接口的声明,实际运用中需要根据情况使用TypeDeclaration提供的isInterface()方法来进行过滤。这也说明了一方面各位开发者需要认真了解各种不同类型的节点具有哪些特性、不同的节点类有哪些方法,另一方面开发者还需要对Java语言本身有较为深入的了解。
OK,本节有关AST的获取和访问就介绍到这里,,大家如果有兴趣的话可以做做如下两个练习:
(1) 寻找一个类的构造方法、抽象方法和Main方法;
(2) 统计一个If语句的Then语句块中语句的数量。
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
例如这是我们之前用过的两组使用AST解析Java源文件的jar包(只有版本号不一样):
【本文作者:刘伟,刘宏韬 http://blog.csdn.net/lovelion】