概述
抽象语法树(Abstract Syntax Tree, AST)是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的结构,树的每个节点ASTNode都表示源码中的一个结构。Eclipse java的开发工具(JDT)提供了Java源代码的抽象语法树AST。抽象语法树就像是java文件的dom模型,比如dom4j通过标签解析出xml文件。AST把java中的各种元素比如类、属性、方法、代码块、注解、注释等等定义成相应的对象,在编译器编译代码的过程中,语法分析器首先通过AST将源码分析成一个语法树,然后再转换成二进制文件。
作用
AST有什么作用了?用过lombox的人一定能感觉到那种简洁舒服的代码风格,lombox的原理也就是在代码编译的时候根据相应的注解动态的去修改AST这个树,为他增加新的节点(也就是对于的代码比如get、set)。所以如果想动态的修改代码,或者直接生成java代码的话,AST是一个不错的选择。
Java项目模型对象
每个Java项目都通过模型在内部表示,此模型是eclipse中Java项目的轻量级和容错表示。这些模型对象在org.eclipse.core.resource插件中定义,因为在介绍下面AST时会用到一些,看代码基本也能理解是做什么用,比如从空间中查找某个项目,找某个文件等等。在eclipse中手动去完成的操作也是模型对象所能做的操作。这里就不再多介绍,简单描述一下的几种模型,有兴趣的可以找找相关资料,这些模型在做插件开发时经常能用到。
对象 | 模型元素 | 描述 |
---|---|---|
IWorkspace | 工作空间 | 项目所在工作空间 |
IJavaProject | Java项目 | 包含所有其他对象的Java项目 |
IPackageFragmentRoot | src文件夹/ bin文件夹/或外部库 | 保存源文件或二进制文件,可以是文件夹或库(zip / jar文件) |
IPackageFragment | 包 | 它们直接列在IPackageFragmentRoot下 |
ICompilationUnit | Java源文件 | 源文件始终位于包节点下方 |
IType/ IField / IMethod | 类型/字段/方法 | 类型,字段和方法 |
IPath | 路径 | 项目文件路径 |
// 得到当前的工作空间
private IWorkspace workspace = ResourcesPlugin.getWorkspace();
/**
* 获取当前工作台文件
* @return
*/
public static IFile getIfile() {
IEditorPart parts = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage().getActiveEditor();
IEditorInput input = parts.getEditorInput();
if (input instanceof IFileEditorInput) {
IFileEditorInput fileInput = (IFileEditorInput) input;
IFile file = fileInput.getFile();
return file;
}else {
return null;
}
}
/**
* 根据路径获取文件
* @return
*/
public static IFile getIfile(String path) {
File file = new File(filePath);
if (!file.isDirectory()) {
IFile ifile = workspace.getRoot().findFilesForLocationURI(file.toURI());
return file;
}
return null;
}
AST模型对象
重点介绍AST语法树中的对象模型,主要包是org.eclipse.jdt.core.dom,位于org.eclipse.jdt.core插件中
ASTNode: 描述节点的类,每个Java源元素都表示为ASTNode该类的子类。为每个特定的AST节点提供有关其所代表的对象的特定信息,是所有模型的父类。
CompilationUnit: 源文件对应的一个编辑单元,非常重要的一个对象,是源文件与模型的映射。可以从工作空间中的文件创建,如果是对项目以外的文件操作也可以从磁盘中的文件创建。
// 工作空间中创建
IWorkspace workspace = ResourcesPlugin.getWorkspace();
IPath path = Path.fromOSString("/com/demo/path");
// 文件路径得到模型对象
IFile file = workspace.getRoot().getFile(path);
// 文件路径得到模型对象
ICompilationUnit element = JavaCore.createCompilationUnitFrom(file);
// 创建语法解析器
ASTParser parser = ASTParser.newParser(AST.JLS8);
parser.setResolveBindings(true);
parser.setKind(ASTParser.K_COMPILATION_UNIT);
parser.setBindingsRecovery(true);
parser.setSource(element);
// 转换成ast的模型对象
CompilationUnit astRoot = (CompilationUnit) parser.createAST(null);
// 从磁盘文件中创建
ASTParser parser = ASTParser.newParser(AST.JLS8);
parser.setResolveBindings(true);
parser.setStatementsRecovery(true);
parser.setBindingsRecovery(true);
parser.setKind(ASTParser.K_COMPILATION_UNIT);
File resource = new File("./tests/code/demo.java");
java.nio.file.Path sourcePath = Paths.get(resource.toURI());
String sourceString = new String(Files.readAllBytes(sourcePath));
char[] source = sourceString.toCharArray();
parser.setSource(source);
parser.setUnitName(sourcePath.toAbsolutePath().toString());
CompilationUnit astRoot = (CompilationUnit) parser.createAST(null);
ASTParser:AST语法树的解析器,setSource方法是重载方法,参数可以是char[],也可以是ICompilationUnit和IClassFile类型的java模型对象,主要作用是,把传入的源码或者javamodel对象,转换为你所需要的AST节点
AST: 抽象语法树的工厂类,可以创建各种ASTNode
ASTRewrite:语法树的重写器,在修改代码原文件后用它将修改后的内容重写写入原文件
// 创建重写器
ASTRewrite rewriter = ASTRewrite.create(ast);
BodyDeclaration: 文件本体的定义公告,可以理解为模型的一个节点,属性java反射的应该很好理解。它的子类有TypeDeclaration(类)、MethodDeclaration(方法)、AnnotationTypeDeclaration(注解)、FieldDeclaration(属性)、Comment(注释,有多行注释javaDoc、行注释LineComment、代码块注释BlockComment)等等,这些节点模型都能够在通过所在的节点获取
// 获取抽象树
AST ast = astRoot.getAST();
// 获取类,如果有内部类就会有多个
List<TypeDeclaration> types = cu.types();
TypeDeclaration type = types.get(0);
// 获取属性
FieldDeclaration[] fields = type.getFields();
// 获取方法
MethodDeclaration[] methods = type.getMethods();
// 获取方法的修饰,包括注解@Annotation
List<ASTNode> typeModifiers = type.modifiers();
……
PackageDeclaration: 包的定义
ImportDeclaration: 引入外部类的定义
Expression: 定义了注解、数组、数据类型等语法相关的表达描述
AST试图
Eclipse编辑器中打开的Java文件的AST(抽象语法树)的视图,如果没有在该地址下载:http://archive.eclipse.org/jdt/ui/update-site/plugins/org.eclipse.jdt.astview_1.1.9.201406161921.jar,这个是Eclipse Luna(4.4)和更高版本使用,其他版本可去参考官网下载,下载后拷贝到eclipse安装目录dropins中重启即可。借助试图可以方便的弄清java源码与AST节点模型的对应关系
用法:
- 打开AST视图
o 从视图菜单中:窗口>显示视图>其他…,Java> AST视图
o 通过快捷方式:Alt + Shift + Q,A - 在编辑器中打开Java文件
- 单击“显示活动编辑器的AST”( )以填充视图:该视图显示在编辑器中打开的文件的AST,还将显示与当前文本选择相对应的元素。
- 启用“与编辑器链接”( )以自动跟踪活动编辑器和活动编辑器中的选择。
- 双击AST节点以在编辑器中显示相应的元素。
- 再次双击以查看节点的“扩展范围”,这意味着该范围包括与之关联的所有注释(注释映射器启发式)。
- 打开绑定上的上下文菜单以将其添加到比较托盘
- AST的基础文档已更改后,请使用“刷新”( )更新AST。
具体使用
创建类
AST ast = AST.newAST(AST.JLS3);
CompilationUnit compilationUnit = ast.newCompilationUnit();
TypeDeclaration programClass = ast.newTypeDeclaration();
programClass.setName(ast.newSimpleName("MyController"));
// 设定类或接口的修饰类型public
programClass.modifiers().add(ast.newModifier(ModifierKeyword.PUBLIC_KEYWORD));
// 将创建好的类添加到文件
compilationUnit.types().add(programClass);
创建包
PackageDeclaration packageDeclaration = ast.newPackageDeclaration();
// 设定包名
packageDeclaration.setName(ast.newName("com.demo.controller"));
compilationUnit.setPackage(packageDeclaration);
引入包
ImportDeclaration importDeclaration = ast.newImportDeclaration();
importDeclaration.setName(ast.newName("org.springframework.web.bind.annotation.RequestMapping"));
compilationUnit.imports().add(importDeclaration);
创建方法
MethodDeclaration helloMethod= ast.newMethodDeclaration();
helloMethod.setName(ast.newSimpleName("hello"));
helloMethod.modifiers().add(ast.newModifier(Modifier.ModifierKeyword.PUBLIC_KEYWORD));
helloMethod.setReturnType2(ast.newPrimitiveType(PrimitiveType.VOID));
// 将方法装入类中
programClass.bodyDeclarations().add(helloMethod);
// 为方法增加语句块
Block helloBlock = ast.newBlock();
helloMethod.setBody(helloBlock);
// 最后打印出创建的代码内容
System.out.println(compilationUnit.toString());
......
创建代码块的方式网上有一些资料可以参考,这里就不再一一去列举,创建的步骤虽然比较繁琐,仔细阅读其实也很简单。原本一句代码在定义过程中就需要很多句,没办法谁让这是定义代码的代码了。这里主要想重点讲下对注解的创建,尤其是稍微有些复杂嵌套型的注解(注解中包含注解),当初在研究的时候可参考的资料有限,本人也是经过多次尝试方才创建成功,如果童鞋们有其他高明的见解请不吝赐教。
创建注解
比如我们要为类或者类某些方法增加注解,注解的形式可能是eg:@Annotation(“demo”),也可能是eg:@Annotation(value=“a”,name=“demo”),或者是eg:@Annotations({@Annotation(value=“a”),@Annotation(value=“b”)})
第一种情况:创建单一参数注解
/**
* 添加单一参数注解 eg:@Annotation("demo")
* @param ast
* @param rewriter 重写器
* @param node 写入位置节点,比如类、方法、属性上
* @param previousElement 位置参照节点
* @param annoNm 注解名
* @param value 注解内容
*/
public void addSingleMemberAnnotation(AST ast,ASTRewrite rewriter,ASTNode node,ASTNode previousElement,
String annoNm,String value) {
// 创建单数注解
SingleMemberAnnotation newAnnot = ast.newSingleMemberAnnotation();
newAnnot.setTypeName(ast.newName(annoNm));
// 创建字符串值对象
StringLiteral stringLiteral = ast.newStringLiteral();
stringLiteral.setLiteralValue(value);
newAnnot.setValue(stringLiteral);
// 指定注解写入的位置,这里是MethodDeclaration.MODIFIERS2_PROPERTY即为方法的修饰
ListRewrite newmodifiers = rewriter.getListRewrite(node,
MethodDeclaration.MODIFIERS2_PROPERTY);
// 将注解写入某个指定的注解后面,如果当前没有注解使用insertFirst方法直接写入
newmodifiers.insertAfter(newAnnot, previousElement, null);
}
第二种情况:创建标准的注解,使用MemberValuePair键值对包装所需参数,最后赋予注解
/**
* 添加单参数注解 eg:@Annotation(value="a",name="demo")
* @param ast
* @param rewriter 重写器
* @param node 写入位置节点
* @param previousElement 位置参照节点
* @param annoNm 注解名
* @param map 注解参数和值
*/
public void addNormalMemberAnnotation(AST ast,ASTRewrite rewriter,ASTNode node,ASTNode previousElement,
String annoNm,Map<String,Object> map) {
// 创建标准注解
NormalAnnotation newAnnot = ast.newNormalAnnotation();
newAnnot.setTypeName(ast.newName(annoNm));
// 根据参数值的类型配置
for(String name:map.keySet()) {
MemberValuePair generatedMemberValue = ast.newMemberValuePair();
generatedMemberValue.setName(ast.newSimpleName(name));
if(map.get(name) instanceof String) {
StringLiteral internalNameLiteral = ast.newStringLiteral();
internalNameLiteral.setLiteralValue((String)map.get(name));
generatedMemberValue.setValue(internalNameLiteral);
}
if(map.get(name) instanceof Integer) {
NumberLiteral numberLiteral = ast.newNumberLiteral(map.get(name).toString());
generatedMemberValue.setValue(numberLiteral);
}
if(map.get(name) instanceof Boolean) {
BooleanLiteral booleanLiteral = ast.newBooleanLiteral((Boolean)(map.get(name)));
generatedMemberValue.setValue(booleanLiteral);
}
newAnnot.values().add(generatedMemberValue);
}
// 指定注解写入的位置,这里是类的修饰,如果是包或者引入则配置CompilationUnit的属性
ListRewrite newmodifiers = rewriter.getListRewrite(node,TypeDeclaration.MODIFIERS2_PROPERTY);
newmodifiers.insertFirst(newAnnot, null);
}
第三种情况:最后这种也最复杂,先将每个注解的参数值存入List<Map<String,Object>>中,其他参数与上面一样。嵌套的注解需要使用ArrayInitializer数组序列把子注解包装,最后赋予最外层的注解
/**
* 添加多参数注解 eg:@Annotations({@Annotation(value="a"),@Annotation(value="b")})
* @param ast
* @param rewriter 重写器
* @param node 写入位置节点
* @param previousElement 位置参照节点
* @param annoNm 注解名
* @param innerAnnoNm 内部注解名
* @param list 包含注解的参数和值的集合,集合的元素分别对应每个内部注解
*/
public void addComplexAnnotation(AST ast,ASTRewrite rewriter,ASTNode node,ASTNode previousElement,String annoNm,
String innerAnnoNm,List<Map<String,Object>> list) {
SingleMemberAnnotation newAnnot = ast.newSingleMemberAnnotation();
newAnnot.setTypeName(ast.newName(annoNm));
// 这里是重点,嵌套的注解需要使用数组初始化器包装
ArrayInitializer array = ast.newArrayInitializer();
// 遍历每个注解的参数
for(Map<String,Object> map:list) {
NormalAnnotation innerAnnot = ast.newNormalAnnotation();
innerAnnot.setTypeName(ast.newName(innerAnnoNm));
// 根据参数值的类型配置
for(String name:map.keySet()) {
MemberValuePair generatedMemberValue = ast.newMemberValuePair();
generatedMemberValue.setName(ast.newSimpleName(name));
if(map.get(name) instanceof String) {
StringLiteral internalNameLiteral = ast.newStringLiteral();
internalNameLiteral.setLiteralValue((String)map.get(name));
generatedMemberValue.setValue(internalNameLiteral);
}
if(map.get(name) instanceof Integer) {
NumberLiteral numberLiteral = ast.newNumberLiteral(map.get(name).toString());
generatedMemberValue.setValue(numberLiteral);
}
if(map.get(name) instanceof Boolean) {
BooleanLiteral booleanLiteral = ast.newBooleanLiteral((Boolean)(map.get(name)));
generatedMemberValue.setValue(booleanLiteral);
}
innerAnnot.values().add(generatedMemberValue);
}
array.expressions().add(innerAnnot);
}
newAnnot.setValue(array);
ListRewrite newmodifiers = rewriter.getListRewrite(node,
MethodDeclaration.MODIFIERS2_PROPERTY);
newmodifiers.insertAfter(newAnnot, previousElement, null);
}
然后把注解的包引入
/**
* 添加import
* @param ast
* @param rewriter 重写器
* @param node 写入位置节点
* @param importstr 内容 eg: org.springframework.web.bind.annotation.RequestMapping
*/
public void addImport(AST ast,ASTRewrite rewriter,ASTNode node,List<String> importstr) {
ListRewrite lrw = rewriter.getListRewrite(node, CompilationUnit.IMPORTS_PROPERTY);
for(String imp:importstr) {
if(haveImport(imp,node))
continue;
ImportDeclaration import = ast.newImportDeclaration();
import.setName(ast.newName(imp));
lrw.insertLast(id, null);
}
}
最后借助eclipse的编辑写入到源码中
// 编辑单元切换到工作副本模式,写入文件
element.becomeWorkingCopy(null);
org.eclipse.jface.text.Document document = new Document(element.getSource());
org.eclipse.text.edits.TextEdit edits = rewriter.rewriteAST(document, null);
edits.apply(document);
element.getBuffer().setContents(document.get());
element.commitWorkingCopy(false, null);
好了以上就是我分享的内容,希望看到的本文的童鞋能获得一点帮助,如果有不对或者疑惑的地方欢迎留言