Some time ago I went to a Meetup session “Death To Null” by Ties van de Ven from JDriven. During the presentation it became clear that it wasn‘t so much about null checks and NullPointerExceptions but more about Immutability and how it can help keep your software free of bugs and NullPointerExceptions. One of the statements that got me thinking was (something along the lines of): “Java chose the wrong default. Instead of making everything explicitly final when we want an immutable variable, everything should be implicitly final unless the developer makes it explicitly mutable”. Not knowing that much about the implementation of annotations I thought it would be fun to try to write an annotation which would do exactly that. The first bump I encountered is that annotations cannot modify the programmers source code, or the classes generated from this source code; they can generate new sources or validate code. This quickly turned my attention to project Lombok which does that already. If you use project Lombok for the generation of getters and setters these never show up in your source code. How can your IDE still hint at the existence of the getters and setters?

It turns out that the Java compiler is an iterative process. First the compiler parses your source code into something which is called an AST or Abstract Syntax Tree. Then annotation processors are run to generate extra sources or do some extra validation based on your annotations. Compilation errors, such as invalid classes or invalid method calls, are done in phase 3. If, however, an annotation processor generates new sources, the compiler repeats step 1 and 2 before going to step 3.

Project Lombok, and my proof of concept, will use a bit of a hack. Instead of generating new sources with the annotation processor, it modifies the generated AST. This will allow the processor to change the code without touching the source code or the byte code.

A more detailed explanation can be found here. It is a bit dated, but informative.

Building a PoC

For my proof of concept I‘ve decided to create a new maven project. It will have two sub projects, one of them contains the annotation itself and the processor which will process it, the other one contains a test class which uses the annotation.

I‘ll add two annotations to the project. @FinalizeVars is a Type annotation used to tell the annotation processor all the variables in the Class should be made final. @MutableVar is a Field and Local Variable annotation which will tell the annotation processor to skip this variable.

Extra functionality to remove “final” from variables which have the @MutableVar annotation could be added, but an @MutableVar final String t = “test”; wouldn‘t make much sense anyway.

The code

The annotations

The annotations are very simple, standard annotations.

Listing 1. @FinalizeVars
package nl.johannisk.finalizer.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.CLASS)
@Target({ElementType.TYPE})
public @interface FinalizeVars {
}

The FinalizeVars annotation can be applied to Types. This Annotation is not entirely necessary for the functionality to work, but I didn’t want the annotation processor to change the functionality of the Java Compiler without the programmer explicitly telling it to do something strange.

Listing 2. @MutableVar
package nl.johannisk.finalizer.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.CLASS)
@Target({ElementType.LOCAL_VARIABLE, ElementType.FIELD})
public @interface MutableVar {
}

The MutableVar annotation can be applied to local variables and fields. The annotation processor will skip any local variables and fields which have this annotation.

The Processor

The annotation processor is wired into the compiler through a text file in the resources folder of the annotation subproject: resources/META-INF/services/javax.annotation.processing.Processor

This file contains the fully qualified class name of the processor: nl.johannisk.finalizer.processor.FinalizerProcessor

The actual processor consists of 2 classes. The processor itself and a “visitor” “translator” to visit and translate each root element in the AST. Lets start with the processor.

Listing 3. FinalizerProcessor
package nl.johannisk.finalizer.processor;

import com.sun.source.util.Trees;
import com.sun.tools.javac.processing.JavacProcessingEnvironment;
import com.sun.tools.javac.tree.JCTree;
import com.sun.tools.javac.tree.TreeMaker;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import java.util.Set;

@SupportedSourceVersion(SourceVersion.RELEASE_8)
@SupportedAnnotationTypes({"nl.johannisk.finalizer.annotation.FinalizeVars",
                           "nl.johannisk.finalizer.annotation.MutableVar"})
public class FinalizerProcessor extends AbstractProcessor {
    private Trees     trees;
    private TreeMaker treeMaker;

    @Override
    public void init(final ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        final JavacProcessingEnvironment javacProcessingEnvironment =
            (JavacProcessingEnvironment) processingEnvironment;
        this.trees = Trees.instance(processingEnvironment);
        this.treeMaker = TreeMaker.instance(javacProcessingEnvironment.getContext());
    }

    @Override
    public boolean process(final Set<? extends TypeElement> annotations,
                           final RoundEnvironment roundEnvironment) {
        if (!roundEnvironment.processingOver()) {
            processRootElements(roundEnvironment.getRootElements());
        }
        return false;
    }

    private void processRootElements(final Set<? extends Element> rootElements) {
        rootElements.forEach(this::processRootElement);
    }

    private void processRootElement(final Element element) {
        JCTree tree = (JCTree) trees.getTree(element);
        tree.accept(new FinalizerTranslator(treeMaker));
    }
}

The processor extends AbstractProcessor and should at least override AbstractProcessor‘s process method.

Because it also needs the AST Trees I‘ve also overridden the init method. The init method calls super and initializes the visitor with a treeMaker. It also saves the Trees in a field for the process method.

The process method first checks if processing is already over. If it‘s not (the first run) it will get all the root elements and apply a visitor to them. Because the visitor determines whether a root element should be processed or not a new visitor (FinalizerTranslator) is created for each root element.

Listing 4. FinalizerTranslator
package nl.johannisk.finalizer.processor;
import com.sun.tools.javac.code.Flags;
import com.sun.tools.javac.tree.TreeMaker;
import com.sun.tools.javac.tree.TreeTranslator;
import com.sun.tools.javac.tree.JCTree;

public class FinalizerTranslator extends TreeTranslator {
    private final TreeMaker treeMaker;
    private boolean shouldVisitVarDefinitions = false;
    public FinalizerTranslator(final TreeMaker treeMaker) {
        this.treeMaker = treeMaker;
    }
    @Override
    public void visitClassDef(final JCTree.JCClassDecl classDeclaration) {
        if (isFinalizeVarsAnnotation(classDeclaration.getModifiers())) {
            shouldVisitVarDefinitions = true;
        }
        super.visitClassDef(classDeclaration);
    }
    @Override
    public void visitVarDef(final JCTree.JCVariableDecl variableDeclaration) {
        super.visitVarDef(variableDeclaration);
        JCTree.JCModifiers modifiers = variableDeclaration.getModifiers();
        if (shouldBeMadeFinal(variableDeclaration, modifiers)) {
            variableDeclaration.mods = treeMaker.Modifiers(variableDeclaration.mods.flags | Flags.FINAL);
        }
        this.result = variableDeclaration;
    }
    private boolean shouldBeMadeFinal(final JCTree.JCVariableDecl variableDeclaration,
                                      final JCTree.JCModifiers modifiers) {
        return  !isMutableVarAnnotation(modifiers) &&
            !isVolatile(variableDeclaration.getModifiers()) &&
            shouldVisitVarDefinitions;
    }
    private boolean isMutableVarAnnotation(final JCTree.JCModifiers modifiers) {
        return modifiers.toString().contains("@MutableVar()");
    }
    private boolean isFinalizeVarsAnnotation(final JCTree.JCModifiers modifiers) {
        return modifiers.toString().contains("@FinalizeVars()");
    }
    private boolean isVolatile(final JCTree.JCModifiers modifiers) {
        return (modifiers.flags & Flags.VOLATILE) > 0;
    }
}

The FinalizerTranslator is the visitor. Since annotations are applied to classes and variable declarations the visit methods for these elements are overridden.

First the class definition is visited in visitClassDef. Because annotations are located in the modifiers of the class definition. If the class is annotated with FinalizeVars the visitVarDefinition boolean is set to true. Otherwise, variable declarations will not be processed in this class.

Next, all the variable definitions are visited in visitVarDef. Note that this will visit all variable definitions in the class, not just the ones which are annotated.

If the modifier list includes the Annotation (@MutableVar) and not the volatile modifier (which is not compatible with final) and the class was Annotated with @FinalizeVars indicated by the visitVarDefinition boolean, the treeMaker is instructed to create a new Modifier, based on the original modifiers, shifting the final modifier into the bit field.

Debugging

Debugging the new annotation processor can be a bit cumbersome. This is because debugging the code happens in classes, and these classes are only there, after the annotation processor has run.

The way I got this working is by separating the test classes which use the annotation, from the implementation of the annotation and it‘s processor.

First build the annotation sub project using a simple:

mvn install

Then go into the test project and run a mvnDebug command with the tasks clean, compile, exec:java. This will allow remote debugging of the compile step with an IDE.

mvnDebug clean compile exec:java
Preparing to Execute Maven in Debug Mode
Listening for transport dt_socket at address: 8000

In the IDE (I used IntelliJ) start a remote debugging session on localhost:8000.

Using the processor

IntelliJ requires explicit enabling of annotation processors in Preferences → Build, Execution, Deployment → Compiler → Annotation Processors. When enabled, IntelliJ gives error messages in the problem section: Annotations resulting in errors in the IDE

Unfortunately it will not give an error in the code window: Annotations do not mark errors in IDE window

Source code

The source code is available on GitHub.

shadow-left