This blog is about Java (advanced Java topics like Reflection, Byte Code transformation, Code Generation), Maven, Web technologies, Raspberry Pi and IT in general.

Samstag, 3. September 2011

Grails with ATS Transformation tutorial with a demo example

I wanted to play with  Groovy's AST Transformation in Grails. I thought naively that it would be enough to put the AST Transformation code into the source folder, like in a pure groovy project. I run the grails-app and it didn't worked. So I searched for help in the world-wide-web. Some guys said that the AST code need to be precompiled and there is no way to do it with one compile run. Sadly I couldn't find an example or a tutorial how to do the precompiling. After some frustrating hours of searching and trying I got a working solution. Now I am going to share my knowledge and hope that I can help you with this post.

At first I give you an explanation what an AST Transformation is.
When the Groovy compiler compiles Groovy scripts and classes, at some point in the process, the source code will end up being represented in memory in the form of a Concrete Syntax Tree, then transformed into an Abstract Syntax Tree. The purpose of AST Transformations is to let developers hook into the compilation process to be able to modify the AST before it is turned into bytecode that will be run by the JVM.
In simple words it means that you can modify programmable your code at compile time.Groovy's documentation provides a simple example what you can do with AST. In this example a new Annotation @WithLogging is created. If you add this Annotation to a method then the AST Transformation adds at the beginning and at the end of the method a print line statement with "Starting" and "Ending" and the method name. Now I will show you how to get this example running in grails.

The first thing i had done was creating a new source-folder (src/ast) and put all files in it which needs to be precompiled => all AST related files. In this case WithLogging.groovy and WithLoggingASTTransformation.groovy

WithLogging.groovy
1:  @Retention(RetentionPolicy.SOURCE)  
2:  @Target([ElementType.METHOD])  
3:  @GroovyASTTransformationClass("astexample.WithLoggingASTTransformation")  
4:  public @interface WithLogging {  
5:  }
Here we define the Annotation and bind it to the WithLoggingASTTransformation class. It's important to use the full path to the class.

WithLoggingASTTransformation.groovy
1:  @GroovyASTTransformation(phase = CompilePhase.CANONICALIZATION)  
2:  class WithLoggingASTTransformation implements ASTTransformation {  
3:    private static HashSet<MethodNode> set = new HashSet<MethodNode>()  
4:    public void visit(ASTNode[] nodes, SourceUnit sourceUnit) {  
5:      sourceUnit.getAST()?.getClasses().each { ClassNode classNode ->  
6:        classNode.getAllDeclaredMethods().findAll { MethodNode method ->  
7:          method.getAnnotations(new ClassNode(WithLogging))  
8:        }.each { MethodNode method ->  
9:          if(!set.contains(method)) {  
10:            Statement startMessage = createPrintlnAst("Starting $method.name")  
11:            Statement endMessage = createPrintlnAst("Ending $method.name")  
12:            List existingStatements = method.getCode().getStatements()  
13:            existingStatements.add(0, startMessage)  
14:            existingStatements.add(endMessage)  
15:            set.add(method)  
16:          }  
17:        }  
18:      }  
19:    }  
20:    private Statement createPrintlnAst(String message) {  
21:      return new ExpressionStatement(  
22:      new MethodCallExpression(  
23:      new VariableExpression("this"),  
24:      new ConstantExpression("println"),  
25:      new ArgumentListExpression(  
26:      new ConstantExpression(message)  
27:      )  
28:      )  
29:      )  
30:    }  
31:  }  
This class preforms the AST Transformation. The compiler automatically calls the visit-method.  This method iterates over all methods of all classes and check if the method has the @WithLogging Annotation. If the method has this Annotation then it adds the println-statements to the method.

The next step is to tell the grails app that it needs to precompile those files. To do that you have to create a new file in /scripts called "_Events.groovy". This file is a grant script and with it you can hook into the grails events. We need now a hook into the compiling of the app and it's done like this.
1:  eventCompileStart = {target ->  
2:    compileAST(basedir, classesDirPath)  
3:  }  
4:  def compileAST(def srcBaseDir, def destDir) {  
5:    ant.sequential {  
6:      echo "Precompiling AST Transformations ..."  
7:      echo "src ${srcBaseDir} ${destDir}"  
8:      path id: "grails.compile.classpath", compileClasspath  
9:      def classpathId = "grails.compile.classpath"  
10:      mkdir dir: destDir  
11:      groovyc(destdir: destDir,  
12:          srcDir: "$srcBaseDir/src/ast",  
13:          classpathref: classpathId,  
14:          verbose: grailsSettings.verboseCompile,  
15:          stacktrace: "yes",  
16:          encoding: "UTF-8")  
17:      echo "done precompiling AST Transformations"  
18:    }  
19:  }  

At last we create a simple grails-controller to test the AST Transformation.
1:  class AstController {  
2:    def index = {   
3:      loggedMethod()  
4:      render 'i am just a dummy method to call the method..'  
5:    }  
6:    @WithLogging  
7:    def loggedMethod() {  
8:      println "i am doing some important stuff!"  
9:    }  
10:  }  
When you run the app and call the index-handler than you will get this output:
1:  Starting loggedMethod  
2:  i am doing some important stuff!  
3:  Ending loggedMethod  

That's it. The most important thing for the AST transformation in Grails is the /scripts/_Events.groovy file. The rest is just normal AST transformation code or Grail's code.

You can download the whole tutorial code as a STS project with includes a running AST Grails app here.

I hope you enjoyed the tutorial and you are welcome to leave some feedback.

2 Kommentare: