DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports Events Over 2 million developers have joined DZone. Join Today! Thanks for visiting DZone today,
Edit Profile Manage Email Subscriptions Moderation Admin Console How to Post to DZone Article Submission Guidelines
View Profile
Sign Out
Refcards
Trend Reports
Events
Zones
Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Partner Zones AWS Cloud
by AWS Developer Relations
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Partner Zones
AWS Cloud
by AWS Developer Relations
The Latest "Software Integration: The Intersection of APIs, Microservices, and Cloud-Based Systems" Trend Report
Get the report
  1. DZone
  2. Coding
  3. Languages
  4. Compile-Time Metaprogramming in Groovy, Part 2

Compile-Time Metaprogramming in Groovy, Part 2

Victor Savkin user avatar by
Victor Savkin
·
Apr. 20, 11 · Interview
Like (0)
Save
Tweet
Share
10.12K Views

Join the DZone community and get the full member experience.

Join For Free

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 in Groovy, Part 1
  • Groovy in Action2

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

Groovy (programming language) Metaprogramming

Opinions expressed by DZone contributors are their own.

Popular on DZone

  • Journey to Event Driven, Part 1: Why Event-First Programming Changes Everything
  • 5 Steps for Getting Started in Deep Learning
  • OpenVPN With Radius and Multi-Factor Authentication
  • Choosing the Right Framework for Your Project

Comments

Partner Resources

X

ABOUT US

  • About DZone
  • Send feedback
  • Careers
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 600 Park Offices Drive
  • Suite 300
  • Durham, NC 27709
  • support@dzone.com
  • +1 (919) 678-0300

Let's be friends: