Java抽象语法树AST浅析与使用
阅读原文时间:2021年04月22日阅读:1

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

工作空间

项目所在工作空间

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节点模型的对应关系

用法:

  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>中,其他参数与上面一样。嵌套的注解需要使用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);

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

手机扫一扫

移动阅读更方便

阿里云服务器
腾讯云服务器
七牛云服务器

你可能感兴趣的文章