Java抽象语法树AST浅析与使用

概述

抽象语法树(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工作空间项目所在工作空间
IJavaProjectJava项目包含所有其他对象的Java项目
IPackageFragmentRootsrc文件夹/ bin文件夹/或外部库保存源文件或二进制文件,可以是文件夹或库(zip / jar文件)
IPackageFragment它们直接列在IPackageFragmentRoot下
ICompilationUnitJava源文件源文件始终位于包节点下方
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节点模型的对应关系

用法:

  1. 打开AST视图
    o 从视图菜单中:窗口>显示视图>其他…,Java> AST视图
    o 通过快捷方式:Alt + Shift + Q,A
  2. 在编辑器中打开Java文件
  3. 单击“显示活动编辑器的AST”( )以填充视图:该视图显示在编辑器中打开的文件的AST,还将显示与当前文本选择相对应的元素。
  4. 启用“与编辑器链接”( )以自动跟踪活动编辑器和活动编辑器中的选择。
  5. 双击AST节点以在编辑器中显示相应的元素。
  6. 再次双击以查看节点的“扩展范围”,这意味着该范围包括与之关联的所有注释(注释映射器启发式)。
  7. 打开绑定上的上下文菜单以将其添加到比较托盘
  8. 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);

好了以上就是我分享的内容,希望看到的本文的童鞋能获得一点帮助,如果有不对或者疑惑的地方欢迎留言

评论 13
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值