How to remove any class annotation with Groovy compiler configuration script?
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.
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.
//
// 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.
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.
//
// 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.
@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.
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!
0 Comments