全局抽象语法树 AST 转换

       Groovy 提供了几种途径,可以在编译器中做代码的抽象语法树 AST 的转换。你可以开发一个自己的 AST Vistior,你可以使用标注,使用全局 AST 转换或使用局部 AST转换。

       这篇文档展示了如何开发和调试一个全局 AST 转换。

       我们仍旧沿用本地 AST 转换文档中的那个简单例子。假设我们想在控制台上打印每个方法调用的进入点消息和退出点消息。下面的 hello world 例子将在方法的进入点和退出点分别打印消息。

 def greet() {
   println "Hello World"
}

greet()

       这不是一个很大的用例,但可以很好的解释全局 AST 转换。

       一个全局转换需要经过 4 个步骤:

             1) 实现一个ASTTransformation 子类

             2) 创建一个 Jar 的 Metadata 文件,其中包含你实现的ASTTransformation 的名字。

             3) 创建这个Jar,并且包含上面开发的类和元描述文件。

            4) 将上面的 Jar 加入groovyc 的 classpath 中。

撰写一个ASTTransformation 类

       这一步和开发一个本地的 AST 转换的第一步是一样的。你必须实现一个ASTTransformation 的子类,以读或者修改编译器产生的抽象语法树。下面展示了如何修改 AST ,以实现在控制台上打印方法调用的进入消息和退出消息。

 @GroovyASTTransformation(phase=CompilePhase.CONVERSION)
public class LoggingASTTransformation implements ASTTransformation {

   static final def TARGET = WithLogging.getName()

   public void visit(ASTNode[] astNodes, SourceUnit sourceUnit) {
       List methods = sourceUnit.getAST()?.getMethods()
       methods?.each { MethodNode method ->
           Statement startMessage = createPrintlnAst("Starting $method.name")
           Statement endMessage = createPrintlnAst("Ending $method.name")

           List existingStatements = method.getCode().getStatements()
           existingStatements.add(0, startMessage)
           existingStatements.add(endMessage)
       }
   }

   private Statement createPrintlnAst(String message) {
       return new ExpressionStatement(
           new MethodCallExpression(
               new VariableExpression("this"),
               new ConstantExpression("println"),
               new ArgumentListExpression(
                   new ConstantExpression(message)
               )
           )
       )
   }
}

       上面的第一行的标注(@GroovyASTTransformation) 告诉编译器,这个 AST 转换类应该应用的编译期相位。不像本地 AST 转换,全局转换可以发生在编译期的任何相位上。

       公共方法 visit(ASTNode[], SourceUnit) 在每个编译单元编译时都会被调用一次。在这个例子中,我仅仅把源代码中定义的所有方法都拉出来。一个方法对编译器来说,仅是一个语句对象(statement object),因此我在这个语句列表的开始增加了一个进入消息打印,在语句列表的末尾增加了一个退出消息打印。

       创建 println 语句的复杂逻辑都单独封装在createPrintlnAst 方法中。一个方法调用有三个部分:目标(this),名称符号(println),一个参数列表(the message)。一个创建 AST 的方便方法是,观察编译器在 IDE 的debuger 中生成的 AST。这要求一个测试工具,它带一个自定制的 Groovy 类加载器以及一个 AST visitor

撰写 Jar 的元信息

       Groovy 编译器发现ASTTransformation 的方式是通过一个文件实现的,这个文件的名字是“org.codehaus.groovy.transform.ASTTransformation”。这个文件包含你开发的转换,而且必须是完整限定的包和名字。在我的例子里,这个文件仅包含一行内容:

 gep.LoggingASTTransformation

创建 JAR

       创建的ASTtransformation 类和元描述文件必须打包在一个 jar 中。上述的org.codehaus.groovy.transform.ASTTransformation 文件,必须放在META-INF/services 目录下。这个 jar 的布局看起来像是这样的:

 LogMethodTransform.jar
--gep
----LoggingASTTransformation.class
----LoggingASTTransformation$_visit_closure1.class
--META-INF
----services
------org.codehaus.groovy.transform.ASTTransformation

编译这个例子

       上面得到的 jar 必须放在 groovyc 的 classpath 下面,以被调用。假设上面我们的那个简单的例子脚本,保存在文件“LoggingExample.groovy”中,则编译命令如下:

 groovyc -cp LogMethodTransform.jar LoggingExample.groovy

       上面的命令将生成LoggingExample.class,它的运行结果如下:

 Starting greet
Hello World
Ending greet

调试全局 AST 转换

       本地的 AST 转换的调试比较容易,因为 IDE 可以很好的支持。但全局的 AST 转换的调试就不那么容易了,你必须写一个测试工具,在一个文件中显式的调用LoggingASTTransformation 类。这个测试工具的源代码在这里,并且可以方便的修改以符合你的需求。如果你有其他更方便的调试方式,请告诉大家。

-----------------------------------------------------------