Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Compile-Time Metaprogramming in Groovy, Part 2

DZone's Guide to

Compile-Time Metaprogramming in Groovy, Part 2

· Java Zone
Free Resource

Learn how to troubleshoot and diagnose some of the most common performance issues in Java today. Brought to you in partnership with AppDynamics.

As we all know if you want to be a cool (or it’d better to say a groovy) guy these days you have to start playing with compile-time metaprogramming. Do you want to start? There are a few places to read about it. You can buy Groovy in Action 2 and read Chapter 9 or you can read this and previous posts - step by step tutorials. Personally, I’d advice you to do both:

Compile-time metaprogramming involves writing special classes that extend Groovy compiler allowing you to transform an abstract syntax tree of your program. There are two types of AST transformations: local and global. Local transformations can be applied to methods, classes, fields using regular Java annotations. Global transformations are different. You don’t specify an element you want to transform explicitly. Instead, you just add a jar containing a compiled global transformation and a file with some meta information. The transformation will be applied to all the source units you will compile. Being implicit global transformations can be confusing and dangerous. You may be unaware that global transformations are used. In addition, it is harder to test them. I’d advice you to go with local one if it’s possible in your case.

I was struggling trying to create an idea of a transformation for this post. The example should have been more or less useful and, at the same time, it should have been as simple as possible. What I’ve decided to write is a transformation recording information about all method calls. It will store a method name, all argument values and a time stamp. It’s very simple but it’s not hard to extend it to make it a useful tool.

Let’s start with writing tests…

Write tests

def 'should record a method call without arguments'() {
    setup:
    def transform = new CallRecorderTransformation()
    def clazz = new TransformTestHelper(transform, CONVERSION).parse '''
        class Service {
            def method(){
            }
        }
    '''

    when:
    def service = clazz.newInstance()
    service.method()

    then:
    CallRecorder.calls.size() == 1

    def recorderCall = CallRecorder.calls.first()
    recorderCall.className == 'Service'
    recorderCall.method == 'method'
    recorderCall.args.size() == 0
    recorderCall.date != null
}

TransformTestHelper is a helper class that can compile a chunk of code with your transformation. CallRecoder is a class where we store all the information about method calls:

class CallRecorder {

    static calls = []

    static synchronized record(String className, String methodName, Object ... args){
        calls << [className: className, method: methodName, args: args*.inspect(), date: new Date()]
    }
}

One more test to clarify a case when we call a method with arguments:

def 'should record a method call with arguments'() {
    setup:
    def transform = new CallRecorderTransformation()
    def clazz = new TransformTestHelper(transform, CONVERSION).parse '''
        class Service {
            def method(a,b){
            }
        }
    '''

    when:
    def service = clazz.newInstance()
    service.method(INT_ARG, STR_ARG)

    then:
    def recorderCall = CallRecorder.calls.first()
    recorderCall.args.size() == 2
    recorderCall.args[0] == INT_ARG.inspect()
    recorderCall.args[1] == STR_ARG.inspect()

    where:
    INT_ARG = 1
    STR_ARG = "aaa"
}

Create a specification object

class CallRecorderTransformationSpecification {

    boolean shouldSkipTransformation(SourceUnit unit) {...} 
    void markUnitAsProcessed(SourceUnit unit){...}
}

Moving all validation to a specification object allows us to reduce the amount of support code in the transformation itself. I’d always advice you to do it. Even if your transformation looks trivial. Making the intent clearer is the highest priority when we adjust the compiler itself.

Create a class implementing ASTTransformation

@GroovyASTTransformation(phase = CONVERSION)
class CallRecorderTransformation implements ASTTransformation{

    private specification = new CallRecorderTransformationSpecification()

    void visit(ASTNode[] astNodes, SourceUnit sourceUnit) {
        if(specification.shouldSkipTransformation(sourceUnit))
            return

        getAllMethodsInUnit(sourceUnit).each {
            addMethodCallTraceStatement it
        }

        specification.markUnitAsProcessed sourceUnit
    }

    private getAllMethodsInUnit(sourceUnit) {
        sourceUnit.ast.classes.methods.flatten()
    }

    private addMethodCallTraceStatement(method) {
        def ast = new CallRecorderAstFactory(method)
        def exprList = [ast.createStatement(), method.code]
        method.code = new BlockStatement(exprList, new VariableScope())
    }
}

I’m using the earliest possible compilation phase here - CONVERSION. My understanding of what happens during each compilation phase isn’t comprehensive but I’m trying to follow this very simple rule: “If you want to manipulate Groovy code use the earliest phase possible. If you want to manipulate generated Java code (including generated getters and setter) use the latest phase possible”.

ASTTransformation is a very confusing interface as it is used by both local and global transformations and it is used differently. Local transformations access AST using astNodes. Whereas global transformations use sourceUnit.ast.

As you can see from the chunk of code above the transformation takes all the methods from all the classes and adds CallRecorder.record method call to all of them.

Create an AstFactory

I prefer not to create AST inside my transformation class as the code gets messy. Instead, I put all the black magic inside a special class AstFactory:

@TupleConstructor(includeFields = true)
class CallRecorderAstFactory {

    private MethodNode method

    Statement createStatement() {
        def className = method.declaringClass.nameWithoutPackage
        createCallRecordStatement className, method.name, getParameterNames(method)
    }

    private getParameterNames(method) {
        method.parameters.toList().name
    }

    private createCallRecordStatement(className, methodName, parameters) {
        def statement = createStringWithStatement(className, methodName, parameters)
        def ast = new AstBuilder().buildFromString CONVERSION, true, statement
        ast[0].statements[0]
    }

    private createStringWithStatement(className, methodName, parameters) {
        def res = "victorsavkin.sample2.CallRecorder.record '${className}', '${methodName}'"
        if(parameters){
            res += ", ${parameters.join(',')}"
        }
        res
    }
}

As you can see, I’m using AstBuilder.buildFromString to construct a piece of AST containing a required statement. In my view, this way of creating AST is better for those who aren’t proficient in writing AST transformation yet. You don’t have to learn a new API. The only thing you have to do is building a string with a chink of Groovy code and pass it to the builder.

That’s all. This transformation may look oversimplified, but it shows all the pieces you will have to write to make it work. If you feel that you have the power you can spend some time on much cooler stuff such as writing code coverage tools, profilers and code analysis tools. Global transformations are perfect for such kind of stuff.

GitHub Repository

 

From http://victorsavkin.com/post/4733504178/compile-time-metaprogramming-in-groovy-part-2

Understand the needs and benefits around implementing the right monitoring solution for a growing containerized market. Brought to you in partnership with AppDynamics.

Topics:

Opinions expressed by DZone contributors are their own.

The best of DZone straight to your inbox.

SEE AN EXAMPLE
Please provide a valid email address.

Thanks for subscribing!

Awesome! Check your inbox to verify your email so you can start receiving the latest in tech news and resources.
Subscribe

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}