Tuesday, March 16, 2010

Implementing a @Serializable annotation with Groovy AST transformations

Groovy 1.6 provides hooks to intercept the compilation process and modify the generated AST (abstract syntax tree).

There are two categories of AST transformations:
  • global transformations 
  • local transformations
This blog-post deals with local transformations. A local AST transformation is usually done in three steps:

  1. you have to implement an annotation that marks a code fragement for AST transformation. the chosen code fragement depends on you transformation's functionality. In my case, i just wanted a @Serializable annotation instead of using Java's java.io.Serializable interface, so that @Serializable annotation is ment to be applied at ElementType.TYPE level. If your use-case is a different one, of course you could choose other element-type levels.
  2. you actually have to implement the AST transformation. this is done by implementing the org.codehaus.groovy.transform.ASTTransformation interface.
  3. you have to wire your custom annotation with your AST transformation implementation of step 2. this is done by using the @GroovyASTTransformationClass annotation (a meta-annotation) with the complete path to your ast-transformation class.

In the end, applying the @Serializable annotation is as simple as specifying the annotation on some Groovy class

@Serializable
class User {
  // ...
}

The implementation of step 1) is just a simple annotation declaration

@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE)
@GroovyASTTransformationClass("org.nlr.annotations.SerializableASTTransformation")
public @interface Serializable {
}

and the transformation code is as simple as

@GroovyASTTransformation(phase= CompilePhase.SEMANTIC_ANALYSIS)
public class SerializableASTTransformation implements ASTTransformation {

    public void visit(ASTNode[] nodes, SourceUnit source) {
        if (!(nodes[0] instanceof AnnotationNode) || !(nodes[1] instanceof AnnotatedNode)) {
            throw new RuntimeException("Internal error: wrong types: $node.class / $parent.class");
        }

        AnnotatedNode parent = (AnnotatedNode) nodes[1];
        AnnotationNode node = (AnnotationNode) nodes[0];

        if (parent instanceof ClassNode) {
            ClassNode classNode = (ClassNode) parent;

            classNode.addInterface(ClassHelper.make(java.io.Serializable.class));
        }
    }
}

Notice, that the specified compile phase is "semantic analysis" - a property of local transformations is that they can only be applied in "semantic analysis" compilation phase or above.

If you take a look at the generated (disassembled) byte-code you'll see that the User class's byte-code now implements java.io.Serializable.

public class org.nlr.domain.User extends java.lang.Object implements java.io.Serializable,groovy.lang.GroovyObject{
    public static final java.lang.Class $ownClass;
    java.lang.Long id;
    java.lang.Long version;
    java.util.Set registrations;
    ...

No comments:

Post a Comment