Annotations Processing, Pt. 1

Matthew Potter
jtribe
Published in
6 min readFeb 22, 2017

Occasionally, as an Android developer, you might come across a task that could have been lifted from some weird dictionary’s definition of tedium. An example could be converting an object to JSON — it’s something simple enough that you could do it in your sleep, but long-winded enough to actually take up a reasonable portion of your time if you want to do it for a number of different classes. You could use something like Gson, but what if that’s overkill, or you don’t want the overhead associated with doing it dynamically? But on the other hand, writing out each field sucks. Who wants to do that?

Nobody! Good thing you can use code generation to do it all for you.

An annotation processor is one kind of code generation tool that we can use at compile time to dynamically create new classes. This happens before everything is turned into a runnable form: we take some source code (or a representation thereof) and examine it, process metadata we attach to certain fields, parameters and classes, and use that metadata to generate extra classes.

So how do we, say, make an annotation processor to generate a JSON converter for a class?

Well, luckily for you, you don’t have to. There’s annotation processors out there that can already do that! Lots of them in fact, so there’s not much point in me writing one for this particular article. There’s even really cool stuff like Lombok, a piece of software that does some wicked hacks to actually manipulate already existing code, and you can use that in conjunction with something like Jackson to easily inject the methods straight into the class.

But ubiquitousness aside, that’s definitely the right kind of candidate for annotation processing — generally, you want to go for something robotic, predictable and tedious and make it totally automated. And you believe me, there’s a lot of that in the Android world. Not every annotation processor has to be as sweeping and as complicated as Dagger 2, Android Annotations or the aforementioned Lombok. Even small, trivial, annoying tasks can be made one liners forever with an hour’s effort. And when you can put an annotation on a field to generate ten lines of code, once you use it a few times you begin to see how efficient proper usage of code generation can be.

Instead, let’s just try starting with something really small — a processor that will generate a Comparator for a class when you bang some annotations onto it. It will only support comparing one field, but anyone using our library will be able to pick which field to compare. Afterwards we might write some tests and even experiment with implementing it as a nifty AutoValue extension too, but before we get to any of that stuff we’ll need to have a think about how we will structure this thing. I‘m using Android Studio so everything will be in terms of that.

Let’s have a ponder about what we’ll need. The following are immediately apparent: a processor module for our annotation processor, some kind of API or library module (so we don’t end up dragging a bunch of dependencies from our processor into any projects we want to use our processor in), and our aforementioned example to show people how to use it. With this in mind, this is my typical setup for this kind of thing:

Boom!

As you can see, we have our three modules. They’re imaginatively named app, lib and processor. The latter two are pure Java library modules since we can’t use an Android library module in a Java library module, but in this case we’re fine — we aren’t using any Android framework stuff specifically. If we were, we’d need to make another module that contained the Android-specific parts of the library and then include our lib module in that one as a dependency.

And… now that we have that all set up, we have to do even more setup.

When you write an annotation processor, you need to include a particular file which contains some meta-data about the processors inside your library so it’ll actually run. It’s not that complicated: it’s simply a text file that lists the fully qualified names of your processor classes.

This file will live under the address and name of META-INF/services/javax.annotation.processing.Processor, and will look something like the following:

fully.qualified.name.of.ProcessorOne
fully.qualified.name.of.ProcessorTwo
and.etcetera.ad.nauseam.for.all.the.Processors

And that would provide for those three annotation processors inside your module.

Alternatively, and this is what I prefer to do, you can just add this little library

compile 'com.google.auto.service:auto-service:1.0-rc2'

into your processor module’s build.gradle dependencies. If it complains about not being able to find the dependency, you might need to add in a repositories section and plonk jcenter() in it. Then go and make a class called something like AnnotationsProcessor and put an @AutoService annotation on top of it, and for the value parameter give it Processor.class. You might need to import it from java.annotation.processing.

This will automatically generate the META-INF/… file for you. And with that done, we can begin writing things. You’ll want to make the class you just put the AutoService annotation extend a class called AbstractProcessor:

@AutoService(Processor.class) 
public class AnnotationsProcessor extends AbstractProcessor {
//process() method goes here
}

just like this, and get your IDE to generate an implementation of the abstract process method for you. It’ll look like:

@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
return false;
}

This process method is the method that does all of the work. For each cycle of annotation processing — yes, some processors (not this one!) might have to run through the code twice or more, depending on how they work — it’ll go through and do its thing for the annotations your processor supports.

Now we’re ready to write some proper code.

However, we don’t have any annotations yet! So let’s fix that by making a @ComparableField annotation. We put it inside the lib module so people who want to use your library can get at it.

For reference, here is my implementation:

@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.FIELD)
public @interface ComparableField {

GreaterThan greaterThan() default GreaterThan.IS_POSITIVE;

enum GreaterThan {
IS_NEGATIVE,
IS_POSITIVE
}
}

The GreaterThan enum will be for deciding whether x > y will +1 or -1. By default, x > y will return +1, but we don’t want to force it to do that. Hence the option to change that behaviour.

Now, put compile project (path: ':lib') into our processor’s dependencies. That way we can actually process them. Your build.gradle for your processor module should look like the following:

apply plugin: 'java'

repositories {
jcenter()
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.google.auto.service:auto-service:1.0-rc2'

compile project(path: ':lib')
}

sourceCompatibility = "1.7"
targetCompatibility = "1.7"

And now, going to AnnotationsProcessor.java again, we need to provide it a list of annotations it can process. You can do this two ways — by providing a list in a @SupportedAnnotationType annotation, or by overriding getSupportedAnnotationTypes(). I personally prefer the second so let’s do that. But it’s a matter of preference really. If you wanted to, you could just give some fully qualified class names to that annotation at the top of the class instead.

@Override public Set<String> getSupportedAnnotationTypes() {
Set<String> set = new HashSet<>();
set.add(ComparableField.class.getCanonicalName());
return set;
}

My annotation processor as a whole looks like:

@AutoService(Processor.class) public class AnnotationsProcessor extends AbstractProcessor {

@Override public Set<String> getSupportedAnnotationTypes() {
Set<String> set = new HashSet<>();
set.add(ComparableField.class.getCanonicalName());
return set;
}

@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
return false;
}
}

Pretty puny, huh? This is actually a fully-operational annotation processor that will do absolutely nothing!

Most of what we’ve done so far is simply setup but that’s coming to a close. We can now actually move on to the interesting part — writing the actual code that’ll actually deal with the annotations.

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.

--

--