HomeAboutArchivesTools & ResourcesSupport me
Hi! 👋 I'm Szymon

I help you become a better software developer.

  • 1.15kFollowers
  • 2.36kSubscribers
  • 110Followers
  • 30.2kReputation

Latest posts from the Groovy Cookbook

Groovy Ecosystem Usage Report (2020)
How to merge two maps in Groovy?
Groovy dynamic Maps, generic type erasure, and raw types - an...
Groovy 3 @NullCheck annotation - less code and less NPE
Groovy 3 String GDK improvements - takeRight, takeBetween, an...
Three Groovy String methods that will make your life Groovier!
Quicksort in Groovy - can it be as fast as implemented in Java?
More

Popular categories

  • Groovy Cookbook28
  • Jenkins Pipeline Cookbook9
  • Programmer's Bookshelf6
  • Micronaut Cookbook4
  • Ratpack Cookbook4
  • Learning Java4
  • jq cookbook3
  • How to3
  • Blog Reports2

Don't miss anything - join my newsletter

Additional materials and updates for subscribers. No spam guaranteed.
Unsubscribe at any time.

Did you like this article?

Spread the !
  • ☕️
  1. e.printstacktrace.blog
  2. Groovy Cookbook

How to remove any class annotation with Groovy compiler configuration script?

  • February 19, 2019
  • 7 min. read
  • 0 Comments

One of the most interesting Groovy features is its ability to configure advanced compiler[1] options using DSL script. It becomes handy when you want to apply some global modifications to all Groovy classes. (For instance, you want to add @CompileStatic annotation to all classes, without applying changes to the source code). In most cases, you want to add something to the existing source code, e.g., classes imports or useful annotations, but what if we want to remove one annotation or another?

Kudos to Søren Glasius who brought the question about removing annotations at the compile time to the Groovy Community Slack![2]

Introduction

Let’s start by defining a simple example. Imagine you work with a Groovy code base, and all classes are annotated with the @CompileStatic annotation. It’s an excellent practice to favor static compilation over the dynamic one if we don’t use Groovy’s dynamic language capabilities. However, let’s say that at some point we need to compile the same source code with a static compilation disabled. There are various ways to do it. We could temporarily remove all annotations from the code, but it doesn’t sound like a good solution. Instead, let’s use a compiler configuration script to do it for us (without making a single change to the source code).

An example

We use a simple Person class to illustrate the use case.

Listing 1. Person.groovy
import groovy.transform.CompileStatic

@CompileStatic
class Person {
    private String firstName
    private String lastName

    String introduceYourself() {
        "${firstName} ${lastName}"
    }

    String greet(Person person) {
        "${introduceYourself()} greets ${person.introduceYourself()}"
    }

    static void main(String[] args) {
        def joe = new Person(firstName: "Joe", lastName: "Doe")
        def mark = new Person(firstName: "Mark", lastName: "Smith")

        println joe.greet(mark)
    }
}

As you can see this class uses static compilation, so when we compile it, we get a bytecode that is an equivalent of the following Java code.

Listing 2. Decompiled Person.class file
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

import groovy.lang.GroovyObject;
import groovy.lang.MetaClass;
import org.codehaus.groovy.runtime.DefaultGroovyMethods;
import org.codehaus.groovy.runtime.GStringImpl;
import org.codehaus.groovy.runtime.ScriptBytecodeAdapter;
import org.codehaus.groovy.runtime.typehandling.ShortTypeHandling;

public class Person implements GroovyObject {
    private String firstName;
    private String lastName;

    public Person() {
        MetaClass var1 = this.$getStaticMetaClass();
        this.metaClass = var1;
    }

    public String introduceYourself() {
        return (String)ShortTypeHandling.castToString(new GStringImpl(new Object[]{this.firstName, this.lastName}, new String[]{"", " ", ""}));
    }

    public String greet(Person person) {
        return (String)ShortTypeHandling.castToString(new GStringImpl(new Object[]{this.introduceYourself(), person.introduceYourself()}, new String[]{"", " greets ", ""}));
    }

    public static void main(String... args) {
        Person var1 = new Person();
        String var2 = "Joe";
        ScriptBytecodeAdapter.setGroovyObjectProperty(var2, Person.class, var1, (String)"firstName");
        String var3 = "Doe";
        ScriptBytecodeAdapter.setGroovyObjectProperty(var3, Person.class, var1, (String)"lastName");
        Person var5 = new Person();
        String var6 = "Mark";
        ScriptBytecodeAdapter.setGroovyObjectProperty(var6, Person.class, var5, (String)"firstName");
        String var7 = "Smith";
        ScriptBytecodeAdapter.setGroovyObjectProperty(var7, Person.class, var5, (String)"lastName");
        DefaultGroovyMethods.println(Person.class, var1.greet(var5));
        Object var10000 = null;
    }
}
I use Groovy 2.5.6 version in this example. Depending on Groovy version, the output bytecode represented as a Java code may look differently.

Using compiler configuration script

A Groovy compiler allows us to use the compiler configuration script to add some useful features. For instance, if we would like to add @CompileStatic annotation to all classes, we would create a config.groovy script like the one below.

Listing 3. config.groovy
withConfig(configuration) {
    ast(groovy.transform.CompileStatic)
}

Now, all we have to do is to use --configscript switch to enable our custom compiler configuration.

groovyc --configscript config.groovy Person.groovy

OK, so we know how to add an annotation - let’s see how we can remove one from all Groovy classes. We need to create an AST transformation customizer, but instead of creating a new class that extends org.codehaus.groovy.control.customizers.ASTTransformationCustomizer class we are going to use inline directive that allows us using a closure directly in the configuration script instead.

import org.codehaus.groovy.ast.ClassNode
import org.codehaus.groovy.classgen.GeneratorContext
import org.codehaus.groovy.control.SourceUnit

withConfig(configuration) {
    inline(phase:'CONVERSION') { SourceUnit source, GeneratorContext context, ClassNode classNode -> (1)
        context.compileUnit.classes.each { clazz -> (2)
            clazz.annotations.removeAll { antn -> antn.classNode.name  == 'CompileStatic' } (3)
        }
    }
}

In this compiler configuration, we attach our customizer to the CompilePhase.CONVERSION phase[3] - the phase that is responsible for creating an abstract syntax tree (AST). We take the compilation unit to access all Groovy classes from our source code. Then for each class node, we remove @CompileStatic annotation. Thanks to this we have a chance to modify the source unit before it gets analyzed and compiled. If we take a compiled class file and we decompile it, we get a bytecode represented as the following Java code equivalent.

Listing 4. Decompiled Person.class file (the one compiled without static compilation)
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

import groovy.lang.GroovyObject;
import groovy.lang.MetaClass;
import org.codehaus.groovy.runtime.BytecodeInterface8;
import org.codehaus.groovy.runtime.GStringImpl;
import org.codehaus.groovy.runtime.ScriptBytecodeAdapter;
import org.codehaus.groovy.runtime.callsite.CallSite;
import org.codehaus.groovy.runtime.typehandling.ShortTypeHandling;

public class Person implements GroovyObject {
    private String firstName;
    private String lastName;

    public Person() {
        CallSite[] var1 = $getCallSiteArray();
        super();
        MetaClass var2 = this.$getStaticMetaClass();
        this.metaClass = var2;
    }

    public String introduceYourself() {
        CallSite[] var1 = $getCallSiteArray();
        return (String)ShortTypeHandling.castToString(new GStringImpl(new Object[]{this.firstName, this.lastName}, new String[]{"", " ", ""}));
    }

    public String greet(Person person) {
        CallSite[] var2 = $getCallSiteArray();
        return !__$stMC && !BytecodeInterface8.disabledStandardMetaClass() ? (String)ShortTypeHandling.castToString(new GStringImpl(new Object[]{this.introduceYourself(), var2[2].call(person)}, new String[]{"", " greets ", ""})) : (String)ShortTypeHandling.castToString(new GStringImpl(new Object[]{var2[0].callCurrent(this), var2[1].call(person)}, new String[]{"", " greets ", ""}));
    }

    public static void main(String... args) {
        CallSite[] var1 = $getCallSiteArray();
        Object joe = var1[3].callConstructor(Person.class, ScriptBytecodeAdapter.createMap(new Object[]{"firstName", "Joe", "lastName", "Doe"}));
        Object mark = var1[4].callConstructor(Person.class, ScriptBytecodeAdapter.createMap(new Object[]{"firstName", "Mark", "lastName", "Smith"}));
        var1[5].callStatic(Person.class, var1[6].call(joe, mark));
    }
}

You see the difference. Compiling the same Groovy class produced the same bytecode as if we remove @CompileStatic annotation from the source file.

Imported annotation vs. fully qualified name

There is one corner case worth explaining. You have seen in the previous example that we can access annotation name through the ClassNode field of AnnotatedNode class. We silently assumed that all annotations use imports and simple names like @CompileStatic that. However, that is not always true, and you may find yourself in a situation where the same annotation is added using the qualified name @groovy.transform.CompileStatic. It affects our compiler script significantly because this annotation cannot be found using its simple name - classNode.name in this case returns groovy.transform.CompileStatic.

How to deal with that? We could define a predicate that searches for both names, a simple and qualified one.

Listing 5. Predicate that gets satisifed by simple and qualified @CompileStatic annotation name
{ antn -> antn.classNode.name in ['CompileStatic', 'groovy.transform.CompileStatic'] }

Alternatively, we could "unqualify" all annotation names using tokenize() and capturing the last segment - just in case one of the classes is annotated using qualified annotation name.

Listing 6. Groovy compiler script that supports qualified and simple annotation names
import org.codehaus.groovy.ast.ClassNode
import org.codehaus.groovy.classgen.GeneratorContext
import org.codehaus.groovy.control.SourceUnit

withConfig(configuration) {
    inline(phase:'CONVERSION') { SourceUnit source, GeneratorContext context, ClassNode classNode ->
        context.compileUnit.classes.each { ClassNode clazz ->
            clazz.annotations.removeAll { antn -> antn.classNode.name.tokenize(/./).last() == 'CompileStatic' }
        }
    }
}

Choose whatever works for you better.

Conclusion

I hope you have learned something useful from this blog post. A Groovy compiler configuration script gives you a lot of different options to customize a compiler behavior. If you want to learn more about it, check the official Groovy documentation[4] for more examples. See you next time!


1. http://docs.groovy-lang.org/latest/html/documentation/tools-groovyc.html
2. https://groovycommunity.com/
3. http://docs.groovy-lang.org/latest/html/api/org/codehaus/groovy/control/CompilePhase.html
4. http://groovy-lang.org/dsls.html#_customizer_builder
  • groovy
  • compiler-configuration
  • compile-static
  • annotation
  • ast

Programmer's Bookshelf

Release It! 2nd edition - book review

  • March 3, 2019
  • 7 min. read
  • 0 Comments

We are software developers. Our daily duty is to write programs. We spend a lot of time and effort into doing the right things and doing them right. At some point, the production launch day comes and depending on our level of confidence - we are calm and ready for the first wave of unpredictable users, or deadly terrified.

Groovy Cookbook

Groovy 3 @NullCheck annotation - less code and less NPE

  • February 14, 2020
  • 7 min. read
  • 0 Comments

Groovy 3 helps you write less, but more secure code. Today I want to show you one of the feature...

Groovy Cookbook

Why "grails package" executes Config.groovy file?

  • September 30, 2017
  • 7 min. read
  • 0 Comments

This blog post is inspired by one of my recent Stack Overflow answers to following question: Gra...

Groovy Cookbook

Groovy script: closure does not modify @Field annotated variable

  • October 8, 2017
  • 7 min. read
  • 0 Comments

Recently I have answered a few questions on Stack Overflow related to Groovy scripts and how the...

Any thoughts or ideas?

Let's talk in the comment's section 💬

Want to put a code sample in the comment? Read the Syntax highlighting guide for more information.
Empty
Latest blog posts
  • Merging JSON files recursively in the command-line
  • Jenkins Declarative Pipeline with the dynamic agent - how to configure it?
  • Groovy Ecosystem Usage Report (2020)
  • How to convert JSON to CSV from the command-line?
  • 5 Common Jenkins Pipeline Mistakes
  • How to merge two maps in Groovy?
  • Building stackoverflow-cli with Java 11, Micronaut, Picocli, and GraalVM
  • How to catch curl response in Jenkins Pipeline?
  • Atomic Habits - book review
  • Parsing JSON in command-line with jq: basic filters and functions (part 1)
Trending videos
Jenkins Declarative Pipeline vs Scripted Pipeline - 4 key differences | #jenkinspipeline

Jenkins Pipeline as a code is a new standard for defining continuous integration and delivery pipelines in Jenkins. The scrip...

Building command-line app with Java 11, Micronaut, Picocli, and GraalVM | #micronaut

In this video, I will show you how to create a standalone command-line application (CLI app) using Java 11, Micronaut, Picocl...

Useful links
  • Start here
  • About
  • Archives
  • Resources
  • Privacy Policy
  • Merchandise
  • My Kit
  • RSS
  • Support the blog
Popular categories
  • Groovy Cookbook28
  • Jenkins Pipeline Cookbook9
  • Programmer's Bookshelf6
  • Micronaut Cookbook4
  • Ratpack Cookbook4
  • Learning Java4
  • jq cookbook3
  • How to3
  • Blog Reports2
Popular tags
affiliate async benchmark blogging book career cicd continuous-integration curl devops docker git github graalvm gradle grails groovy haskell hexo java java-8 jenkins jenkins-pipeline jenkinsfile jmh jq json junit learning maven metaprogramming micronaut native-image non-blocking progress ratpack reactive-programming reading recursion review rxjava sdkman session split spock stackoverflow string tail-call tail-recursion unit-test
  • Designed by @wololock
  • Created with Hexo
  • Hosted on GitHub
  • Deployed with Circle CI
  • License CC BY-NC-SA 4.0