Annotations Processing, Pt. 2

Matthew Potter
8 min readFeb 26, 2017

--

This time we’re using sections

Last time we left off, we’d just created our first little annotation processor. Unfortunately, it does a sum total of zero work, generating zero code and providing zero value. That we’ll have to change.

To do this, we need one more library. It’s called JavaPoet. If you read the documentation on that GitHub page you’ll see that it gives us a really nifty API for writing new Java files: the crux of any annotation processor. So let’s put compile 'com.squareup:javapoet:1.7.0' into our processor’s dependencies. Sync, and then we can go full steam ahead!

The first puzzle is how to get at the actual list of annotated elements so we can… well, do something with them. Looking at our process method, you’ll notice that there’s a couple of parameters we’re getting passed. The first one is a list of annotations to be processed — easy enough — but the second is this mysterious thing called a RoundEnvironment. If we navigate to the source code, the documentation describes it as such:

* An annotation processing tool framework will {@linkplain
* Processor#process provide an annotation processor with an object
* implementing this interface} so that the processor can query for
* information about a round of annotation processing.

Harkening back to my comment about an annotation processor working in “rounds”, you may be able to surmise that this means that in this particular iteration of our mid-processing codebase, the RoundEnvironment serves as a representation of our code’s state for this iteration. Including everything that’s been annotated. So if we call one particular method thusly…

roundEnv.getElementsAnnotatedWith(ComparableField.class);

…we can get a list of all Elements in our code that’s annotated with @ComparableField.

So, what is an Element?

An Element, by the way, is a representation of a package, class or method, but this is all at compile time so they’re very much static and not at all like a method or class you’d deal with at runtime. You can’t invoke any of them, instantiate instances of the classes they map to or anything like that. However, you can use them to get things like class names, determine whether something is a primitive or not, and other nifty things.

I like to think of it as kind of like a weird reflection-esque construct that we can look at but cannot touch.

There are a few different types of Element, chief among them being:

  • TypeElement, which will typically be a class, or a parameter in a method.
  • VariableElement, which maps to a field.
  • ExecutableElement, which will map to a method.
  • PackageElement, which unsurprisingly corresponds to a package.

In our case, we are only really going to deal with TypeElements and plain old Elements. I’ll also add in a Set so we can keep ourselves to one @ComparableField annotation per class, and then create a method for processing each of the elements in our Element set. So now our process method will look a bit like:

@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(ComparableField.class);
Set<String> annotatedClasses = new HashSet<>();
for (Element e : elements) {
processElement(e, annotatedClasses);
}
return true; //return true to consume the annotations
}
private void processElement(Element e, Set<String> annotatedClasses){
}

One method has become two! This feels a bit more meaty already, even though we still do nothing :)

But we’ve managed to isolate each annotated element. Definitely a step in the right direction. So let’s grab the enclosing element of the annotated field, like so:

TypeElement classElement = (TypeElement) e.getEnclosingElement();

so we can get the TypeElement of the class the field is in. Then, because we want to make sure we only ever generate one Comparator implementation per class, we can add in this check

if (annotatedClasses.contains(
classElement.getQualifiedName().toString())) {
//we do something here, but I'm not quite sure what yet!
} else {
annotatedClasses.add(classElement.getQualifiedName().toString());
}

that’ll do a certain something if we have more than one of our annotation in that class, halting the compilation process in its tracks.

This thing is simply sending a message!

The ProcessorEnvironment

To do this, we’ll need to override another method in our annotation processor called init() and get ourselves an instance of what’s called a Messager. We do this like so:

@Override public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
messenger = processingEnv.getMessager();
}

We’re grabbing an instant of the Messager from our processingEnv and saving it to a local field. This ProcessingEnvironment instance is basically our connection to the outside world — we can get access to the console, disk and even grab some very useful little utility classes from it. For now, we only want to grab the Messager. Later on we might want to access some other things, but that’s not for a little while yet.

The Messager instance, aptly called messenger, has a method called printMessage that lets us print diagnostic information to output such as the console. This method takes a couple of things at least: the Kind of error and the message itself. Sending an ERROR-kind will also cause the compilation process to break reasonably gracefully and is preferred over throwing an Exception.

So now we send off an error message using the messenger, so our little duplicity check will now be as follows:

if (annotatedClasses.contains(
classElement.getQualifiedName().toString())) {
String msg = "You may only have one ComparableField per class!";
messenger
.printMessage(Diagnostic.Kind.ERROR, msg);
} else {
annotatedClasses.add(classElement.getQualifiedName().toString());
}

Grabbing our Metadata

How do we get the annotation off the element though? It’s not immediately apparent, but each Element has a method called getAnnotation() that takes a Class object and returns the annotation attached to that Element. Since we want to get the @ComparableField off the Element, we will call

ComparableField annotation = e.getAnnotation(ComparableField.class);

and that will give us the Element’s ComparableField. If we didn’t have one attached to it that method would just return null; however, since we already know that the Element has that annotation, we don’t have to do a check.

Baby’s first TypeSpec

We are ready to write some Java now (with Java — is that meta or what?), and this is where JavaPoet really helps. So let’s write ourselves a class file:

String className = String.format(NAME_FORMAT, classElement.getSimpleName());TypeSpec.Builder comparatorTypeSpec = TypeSpec.classBuilder(className) //
.addModifiers(Modifier.PUBLIC)
.addSuperinterface( //
ParameterizedTypeName.get(ClassName.get(Comparator.class),
TypeName.get(classElement.asType())));

Isn’t that a wee mouthful?

First of all, we want to get a name for our class — you can see it being used in the classBuilder() method — so we quickly put one together and stick it in there. This TypeSpec.Builder forms the basis of our class’s structure. We can add fields, methods, generics, modifiers, and even annotations if we like. You’ll see that we set our class to public and we implement an interface of some cryptic variety.

ParameterizedTypeName lets us extend a class or interface that makes use of generics by providing a concrete class to use for the parameter type. In this case, we’re using the ClassName, TypeName... variant of get()— the first parameter being the name of the interface we’re extending, the varargs being a collection of types we want to plug into our generics. If you look at the definition of Comparator you’ll see there’s only one, so we only want to provide one. In this instance, we’re using the classElement’s type. This will generate a class with the following sort of declaration template going on:

public class ${elementName}Comparator extends Comparator<${elementName}>{}

Writing our compare()

Right now we’re missing one really, really important thing: the definition of our compare method. Let’s get started on that by creating a couple of ParameterSpecs and then putting them into a nicely-constructed MethodSpec builder. These ParameterSpec builders only really need the type and the name of the method.

ParameterSpec lhsParameterSpec =
ParameterSpec.builder(ClassName.get(classElement.asType()), LHS).build();

ParameterSpec rhsParameterSpec =
ParameterSpec.builder(ClassName.get(classElement.asType()), RHS).build();

MethodSpec.Builder methodSpecBuilder = MethodSpec.methodBuilder(COMPARE)
.addModifiers(Modifier.PUBLIC)
.addAnnotation(Override.class)
.addParameter(lhsParameterSpec)
.addParameter(rhsParameterSpec)
.returns(int.class);

where

private final static String COMPARE = "compare"; //our function name

Note that we seem to be missing the the actual code. That’s cool though. We have our method definition, so now we just need to fill in that gap.

Because we want to be able to work with both primitives and more complex types, we need to be able to differentiate. Using a > operator wouldn’t go down too well if we call it with a couple of Strings, and calling compareTo on a primitive would just stuff up the build. Luckily, there’s a way to check if an Element is a primitive, even if it’s a bit long-winded: by calling e.asType().getKind().isPrimitive() we’ll simply get a true or false answer.

I’ll put the contents for each possible method in their respective branches for ease of understanding. It’s obviously not optimal, but it will generate easily understandable code and shows off a couple of features of JavaPoet:

We really need a gist for this one!

Additionally, the definition of isPositive is:

boolean isPositive(ComparableField f) {
return f.greaterThan() == ComparableField.GreaterThan.IS_POSITIVE;
}

Most of it is probably pretty straightforward, other than maybe the:

  • beginControlFlow call. It takes a string uses it to add any if, for, while etc. we might like to define, complete with bracketing and indentation;
  • the weird formatting stuff in the addStatement calls, which simply let us plug in a $Literal. You can also use an $S for inserting a String or a $T for a type, but we don’t need to do that here;
  • and the endControlFlow calls, which end the control block initiated by its corresponding beginControlFlow call;

Finally, we can add our code to our method:

methodSpecBuilder.addCode(compareBuilder.build());

and then our method to our class:

comparatorTypeSpec.addMethod(methodSpecBuilder.build());

and then we can — wait a minute…

How do we save the file?

The home stretch

It so happens that there’s more things we can grab from that ProcessingEnvironment instance :

@Override public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
filer = processingEnv.getFiler();
elementUtils = processingEnv.getElementUtils();
messenger = processingEnv.getMessager();
}

This gives us all of our necessary connections to the outside world. Along with our Messager, we grab a Filer and an Elements instance. I want the Filer because I want to write my java class to the disk. The Elements instance is used in the following method, which will give us the package name of an Element:

String getPackageName(Element element) {
return elementUtils.getPackageOf(element).getQualifiedName().toString();
}

and we use the Filer to write our java file to disk, just below the last addMethod call:

JavaFile file =
JavaFile.builder(getPackageName(classElement), comparatorTypeSpec.build()).build();
try {
file.writeTo(filer);
} catch (IOException ex) {
messenger.printMessage(Diagnostic.Kind.ERROR, ex.getMessage());
}

and there we have it! Our entire annotations processor looks like this rather long gist.

Add processor to your app module like you would any other annotation processor, add lib as a compile dependency, then make a test class and annotate a field with @ComparableField. Upon building, you might see a class like the following being generated:

The end product!

About Us

At jtribe, we proudly create software for iOS, Android and Web and are passionate about what we do. We’ve been working with the iOS and Android platforms since day one, and are one of the most experienced mobile development teams in Australia. We measure success on the impact we have, and with over six-million end users we know our work is meaningful. This continues to be our driving force.

--

--