Writing your own Annotation Processors in Android

Annotations and Annotation Processors are here with us for over a decade now. There are many libraries like ButterKnife, Dagger, Room etc. which uses the power of annotation processors and make our development easy.

In this article we will see what are Annotations, Annotation Processors and how to write one.

What are Annotations ?

An annotation is a form of syntactic metadata that can be added to Java source code.
We can annotate classes, interfaces, methods, variables, parameters etc.
Java annotations can be read from source files. Java annotations can also be embedded in and read from class files generated by the compiler.
Annotations can be retained by Java VM at run-time and read via reflection.

@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.FIELD)
public @interface BindView {
int value();
}

Creating an annotation requires two pieces of information: Retention and Target.

A RetentionPolicy specifies how long, in terms of the program lifecycle, the annotation should be retained for. For example, annotations may be retained during compile-time or runtime, depending on the retention policy associated with the annotation.

The Target of an annotation specifies which Java ElementTypes an annotation can be applied to.

In the above example of BindView the RetentionPolicy is SOURCE because we only want that annotation at compile time. The Target is FIELD to tell that this annotation can only be applied to fields of a class.

Why Annotation Processors ?

1. Compile time

Annotation Processor is actually a part of javac compiler so all the processing happens at compile time rather than runtime. That’s why Annotation Processors are really fast.
It’s always better to know your errors early at compile time instead of later at runtime.

2. No Reflection

Java’s Reflection API is known to throw so many exceptions at runtime, which is not a good thing. Annotation Processors allow us to know the semantic structure of a program without Reflection. It uses something called the Mirror API.

3. Generate boilerplate code*

The most useful feature of an Annotation Processor is the generation of boring boilerplate code. In past few years many libraries like ButterKnife, Dagger, Room have used this feature to make life of developers easy and boilerplate free.

*Annotation processing can only be used to generate new files and not to modify existing ones.

How does Annotation Processing work ?

The annotation processing takes place in many rounds. The compiler reads a java source file with the registered annotations and calls their corresponding annotation processors which will generate more java source files with more annotations. These new annotations will again call their corresponding annotation processors which will again generate more java source files.
This cycle continues until no new java source file is generated in the cycle.

image courtesy: Jorge Castillo

How to register a Processor ?

A processor must be registered to the compiler so that it can run while the application is being compiled.

Annotation Processors can be registered in two ways.

1. Old Way
Create a directory structure like this
<your-annotation-processor-module>/src/main/resources/META-INF/services
Now, in the services directory, we will create a file named javax.annotation.processing.Processor.
This file will list the classes (fully qualified names) that the compiler will call when it compiles the application's source code while annotation processing.

2. New Way
Use Google’s AutoService library.
Just annotate your Processor with @AutoService(Processor.class)

Example:

package foo.bar;
import javax.annotation.processing.Processor;
@AutoService(Processor.class)
final class MyProcessor implements Processor {
// …
}

How to create a Processor ?

To create our custom Annotation Processor we need to make a class that extends AbstractProcessor which defines the base methods for the processing.
We have to override four methods to provide our implementations for the processing.

public class Processor extends AbstractProcessor {
    @Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
// initialize helper/utility classes...
}
    @Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnvironment) {
// do processing...
return true;
}
    @Override
public Set<String> getSupportedAnnotationTypes() {
return new HashSet<String>() {{
add(BindView.class.getCanonicalName());
add(OnClick.class.getCanonicalName());
}};
}
    @Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
}

init() gives you Helper/Utility classes like Filer (to generate files), Messager(for logging errors, warnings etc.), Elements(utility methods for operating on program elements), Types (utility methods for operating on types) etc.
You can get these classes with processingEnvironment.

process() is the method where all the processing happens.
It gives you annotations that need to be processed and roundEnvironmentprovides the information about the round and has some utility methods for querying elements.
e.g. processingOver(), getRootElements(), getElementsAnnotatedWith() etc.

getSupportedAnnotationTypes() returns the names of the annotation types supported by this processor.

getSupportedSourceVersion() returns the latest source version supported by this processor.

Note: You can also use @SupportedAnnotationTypes and @SupportedSourceVersion instead of getSupportedAnnotationTypes() and getSupportedSourceVersion() respectively.

Demo #1 : Singleton

In this example I’ve made an Annotation Processor by which we can verify if we have implemented our Singleton pattern correctly or not by just annotating our class with@Singleton.

Example:

@Singleton
public class MySingleton {
private MySingleton() {}
public static MySingleton getInstance() {
return new MySingleton();
}
}

Demo #2 : KSingleton

This example is same as the above one. But in Kotlin.

Important Classes/Objects

The javax.annotation.processing package in Java has many useful classes which helps us to make our annotation processor. These classes have many utility functions like logging error messages, creating new files, filtering out our annotated elements and much more.

Some of these Classes/Objects are mentioned below.

Elements : Utility methods for operating on program elements. Can be accessed by ProcessingEnvironment.getElementUtils().

Types : Utility methods for operating on types. Can be accessed by ProcessingEnvironment.getTypeUtils().

Messager : A Messager provides the way for an annotation processor to report error messages, warnings, and other notices. Can be accessed by ProcessingEnvironment.getMessager().

Filer : This interface supports the creation of new files by an annotation processor. Can be accessed by ProcessingEnvironment.getFiler().

RoundEnvironment : An annotation processing tool framework will provide an annotation processor with an object implementing this interface so that the processor can query for information about a round of annotation processing. We can get our desired elements with RoundEnvironment.getRootElements()and RoundEnvironment.getElementsAnnotatedWith()methods.

ElementFilter : Filters for selecting just the elements of interest from a collection of elements. Contains methods like ElementFilter.constructorsIn(), ElementFilter.methodsIn(), ElementFilter.fieldsIn() etc.

How to generate .java files ?

In theory, we can write our .java classes line by line like we would be doing it on a notepad. But this approach is time consuming and error prone.

Instead, we can use Square’s JavaPoet library for generating .java files.
JavaPoet makes it really simple to define a class structure and write it while processing. It creates classes that are very close to a handwritten code.

Example:

This HelloWorld class

package com.example.helloworld;
public final class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello, JavaPoet!");
}
}

can be generated by this piece of code

MethodSpec main = MethodSpec.methodBuilder("main")
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.returns(void.class)
.addParameter(String[].class, "args")
.addStatement("$T.out.println($S)", System.class, "Hello, JavaPoet!")
.build();
TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
.addMethod(main)
.build();
JavaFile javaFile = JavaFile.builder("com.example.helloworld", helloWorld)
.build();
javaFile.writeTo(System.out);

Note: There’s also another Square library KotlinPoet for generating .kt files.

Demo #3 : ButterKnife

This example is inspired by the actual ButterKnife library which helps us to bind views and their callbacks using Annotations.

This ButterKnife library will generate separate classes (with suffix Binder) for each of the Activity where we used the annotations @BindView or @OnClick.

MainActivity.java

public class MainActivity extends AppCompatActivity {
    private int numberOfTimesTextViewClicked = 0;
    @BindView(R.id.text_view)
TextView textView;
    @Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
        ButterKnife.bind(this);
}
    @OnClick(R.id.text_view)
void onTextViewClicked(View view) {
textView.setText(String.valueOf(++numberOfTimesTextViewClicked));
}
}

MainActivityBinder.java

public class MainActivityBinder {
public MainActivityBinder(MainActivity activity) {
bindViews(activity);
bindOnClicks(activity);
}
    private void bindViews(MainActivity activity) {
activity.textView = (TextView) activity.findViewById(2131165314);
}
    private void bindOnClicks(final MainActivity activity) {
activity.findViewById(2131165314).setOnClickListener(new View.OnClickListener() {
public void onClick(View view) {
activity.onTextViewClicked(view);
}
});
}
}

Conclusion

It’s clear that making Annotations and Annotation Processors is not at all difficult. There are few steps which one has to follow in order to make his/her own Annotation Processor.

Annotations have become an essential part in Android Development nowadays and I hope by this article you got to know how they are working internally.

The full source-code can be found here!


Thanks for reading! Share this article if you found it useful.
Please do Clap 👏 to show some love :)

Let’s become friends on LinkedIn, GitHub, Facebook, Twitter.