Friday 16 September 2011

Groovy Logging Injection - AST Transformation

Following on from the theme of AST transformations, I created a global AST transform that would iterate through all classes and effectively inject the following field:
//SLF4J Logger/LoggerFactory
Logger logger = LoggerFactory.getLogger(this.class) 
Like with Grails, this means that a class can simply call 'logger' without worrying about creating it (or it's implementation for that matter). The following classes achieve this:
package annotations

import org.codehaus.groovy.transform.ASTTransformation
import org.codehaus.groovy.control.SourceUnit
import org.codehaus.groovy.ast.ASTNode
import org.codehaus.groovy.transform.GroovyASTTransformation
import org.codehaus.groovy.control.CompilePhase
import org.codehaus.groovy.ast.ClassNode
import org.codehaus.groovy.ast.FieldNode
import org.codehaus.groovy.ast.expr.MethodCallExpression
import org.codehaus.groovy.ast.expr.Expression
import org.codehaus.groovy.ast.expr.ClassExpression
import org.codehaus.groovy.ast.expr.ArgumentListExpression
import org.codehaus.groovy.ast.expr.ConstantExpression
import org.codehaus.groovy.ast.expr.PropertyExpression
import org.codehaus.groovy.ast.expr.VariableExpression
import java.lang.annotation.ElementType
import java.lang.annotation.Target
import java.lang.annotation.RetentionPolicy
import java.lang.annotation.Retention

//Injects the following into each non-static, non-enum class
// Logger logger = LoggerFactory.getLogger(this.class)

@GroovyASTTransformation(phase=CompilePhase.SEMANTIC_ANALYSIS)
class LoggerInjectionASTTransformation implements ASTTransformation {
    public void visit(ASTNode[] astNodes, SourceUnit sourceUnit) {

        List classes = sourceUnit.getAST()?.getClasses()
        classes?.each { ClassNode classNode ->
            if (classNode.isEnum() || classNode.isStaticClass() || classNode.isInterface() || classNode.isAnnotationDefinition()) return //skip to next class

            //ignore these transformations!
            if (classNode.declaresInterface(new ClassNode(org.codehaus.groovy.transform.ASTTransformation.class))) return

            //don't add another logger field (allows for overriding)
            if (classNode.getField("logger")) return

            //exclude classes decorated with the ExcludeLoggerInjection class
            if (classNode.getAnnotations(new ClassNode(ExcludeLoggerInjection.class))) return

            Expression objectExpression = new ClassExpression(new ClassNode(org.slf4j.LoggerFactory.class))
            PropertyExpression classReference = new PropertyExpression(new VariableExpression("this"),new ConstantExpression("class"))
            ArgumentListExpression arguments = new ArgumentListExpression(classReference)
            Expression initialValueExpression = new MethodCallExpression(objectExpression,"getLogger", arguments)

            FieldNode loggingField = new FieldNode("logger",2,new ClassNode(org.slf4j.Logger.class),classNode,initialValueExpression)

            classNode.addField(loggingField)

        }
    }
}

@Retention(RetentionPolicy.SOURCE)
@Target([ElementType.TYPE])
public @interface ExcludeLoggerInjection {
}
Note that it only injects the logger field when there isn't already such a field (allowing the developer to override the logger implementation at compile-time if they desire), and it only adds a logger field to 'standard' classes, excluding the likes of interfaces and enums. Furthermore, an annotation 'ExcludeLoggerInjection' was created that, when decorated on a class, would stop the AST transform from injecting the logging class: this is especially useful when Java reflection is used to instantiate a class. One final step is required for invoking the global transform during class compilation. The classpath needs to contain a META-INF/services directory containing a file called 'org.codehaus.groovy.transform.ASTTransformation'. The contents of the file should contain all of the global annotation classed used during Groovy compilation. In our case the file simply contains:
nz.ac.auckland.mediaservices.digitizer.annotations.LoggerInjectionASTTransformation
If you're using Maven to do building this is pretty simple as resources are placed onto the classpath before compilation (we also need to build the annotation first before the remaining classes, see a previous post for how I used an additional ant groovyc task to force early compilation of these annotations).

No comments:

Post a Comment