Annotation Processing in Java

Jintin
5 min readJun 26, 2018

--

Java is often criticized by its heavy syntax which make you to write longer code than other modern programming languages. Which is true but more important here is there are several ways you can improve by it’s essential functionality. Annotation Processing is definitely the one you’ll like.

What is Annotation Processing?

So what is Annotation Processing? Annotation is sort of tag mechanism you can label some meta on Classes, Methods or Parameters and Annotation Processing will analyze those Annotations in compile time and generate Classes for you according to your Annotations. Here is how it works.

  1. Create Annotation classes.
  2. Create Annotation Parser classes.
  3. Add Annotations in your project.
  4. Compile, and Annotation Parsers will handle the Annotations.
  5. Auto-generated classes will added in build folder.

Note that the Annotation Parser classes is only need when compile your project, and is not necessary include in your release application.

Example

Let’s try to implement Simple Factory Pattern using Annotation Processing. (Often use reflection as another way to implement).

Assume we have Dog and Cat need to be create by Factory Class.

public interface Animal {
void bark();
}
public class Dog implement Animal {
@Override
public void bark() { System.out.println("woof"); }
}
public class Cat implement Animal {
@Override
public void bark() { System.out.println("meow"); }
}
public class AnimalFactory {
public static Animal createAnimal() {
switch(tag) {
case "dog":
return new Dog();
case "cat":
return new Cat();
default:
through new RuntimeException("not support animal");
}
}
public static void main(String[] args) {
Animal dog = AnimalFactory.createAnimal("dog");
dog.bark(); // woof
Animal cat = AnimalFactory.createAnimal("cat");
cat.bark(); // meow
}

We don’t need to care what Dog and Cat is when we create the instance by AnimalFactory, but the AnimalFactory is quite messy. Let’s try to use Annotation Processing auto-generate this file for us.

Create Annotation Class

@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE)
public @interface AutoFactory {
}
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE)
public @interface AutoElement {
String tag();
}

Here we define two annotation for Factory and Real Class. @Retention is where we define the lifecycle of the annotations. There are SOURCE(only used in compile), CLASS(will package into .class file) and RUNTIME(used in runtime). Here we use SOURCE as we only need in compile time. @Target is a indicator of what is our target type to annotated. TYPE is used for Class and Interface.

Create Annotation Parser

We will use following library for easy config:

compile 'com.google.auto.service:auto-service:1.0-rc4'
compile 'com.squareup:javapoet:1.10.0'

Here is the basic form of a custom annotation processor looks like.

@AutoService(Processor.class)
public class AutoFactoryProcesser extends AbstractProcessor {
@Override
public synchronized void init(ProcessingEnvironment env) { ... }
@Override
public Set<String> getSupportedAnnotationTypes() { ... }

@Override
public SourceVersion getSupportedSourceVersion() { ... }
@Override
public boolean process(Set<? extends TypeElement> set,
RoundEnvironment env) { ... }
}

AbstractProcessor is the plugin we can add into the compile pipeline, and analyze source code and generate code as well. @AutoService is a google library which help us bind our processor to compiler automatically. And we will discuss other methods one by one below.

private Filer filer;
private Messager messager;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
filer = processingEnv.getFiler();
messager = processingEnv.getMessager();
}

We can get ProcessingEnvironment in init function which contain Filer (write file) and Messager (log purpose). If you don’t want to add init function, by the way the processingEnv variable will be kept in parent Class.

@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> annotations = new LinkedHashSet<>();
annotations.add(AutoFactory.class.getCanonicalName());
annotations.add(AutoElement.class.getCanonicalName());
return annotations;
}

We need to define what the annotation we care about in getSupportedAnnotationTypes for compiler for performance perspective. Here we add our two Annotations.

@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}

getSupportedSourceVersion indicate your java version, usually you can leave it there if you don’t have to stick to specific version.

@Override
public boolean process(Set<? extends TypeElement> set,
RoundEnvironment roundEnvironment) {
Map<ClassName, List<ElementInfo>> result = new HashMap<>();
for (Element annotatedElement : roundEnvironment.getElementsAnnotatedWith(AutoFactory.class)) {
if (annotatedElement.getKind() != ElementKind.INTERFACE) {
error("Only interface can be annotated with AutoFactory", annotatedElement);
return false;
}
TypeElement typeElement = (TypeElement) annotatedElement;
ClassName className = ClassName.get(typeElement);
if (!result.containsKey(className)) {
result.put(className, new ArrayList<>());
}
}
for (Element annotatedElement : roundEnvironment.getElementsAnnotatedWith(AutoElement.class)) {
if (annotatedElement.getKind() != ElementKind.CLASS) {
error("Only class can be annotated with AutoElement", annotatedElement);
return false;
}
AutoElement autoElement = annotatedElement.getAnnotation(AutoElement.class);
TypeElement typeElement = (TypeElement) annotatedElement;
ClassName className = ClassName.get(typeElement);
List<? extends TypeMirror> list = typeElement.getInterfaces();
for (TypeMirror typeMirror : list) {
ClassName typeName = getName(typeMirror);
if (result.containsKey(typeName)) {
result.get(typeName).add(new ElementInfo(autoElement.tag(), className));
break;
}
}
}
try {
new FactoryBuilder(filer, result).generate();
} catch (IOException e) {
error(e.getMessage());
}
return false;
}

process is where we manipulate with the annotations. It’s not like writing normal Java application, here we only deal with type (like reflection does). The main idea here is to extract the annotate object information (name, package, addition tag) and then pass to FactoryBuilder to generate code.

public void generate() throws IOException {
for (ClassName key : input.keySet()) {
MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("create" + key.simpleName())
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.addParameter(String.class, "type")
.beginControlFlow("switch(type)");
for (ElementInfo elementInfo : input.get(key)) {
methodBuilder
.addStatement("case $S: return new $T()", elementInfo.tag, elementInfo.className);
}

methodBuilder
.endControlFlow()
.addStatement("throw new RuntimeException(\"not support type\")")
.returns(key);
MethodSpec methodSpec = methodBuilder.build();
TypeSpec helloWorld = TypeSpec.classBuilder(key.simpleName() + "Factory")
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
.addMethod(methodSpec)
.build();
JavaFile javaFile = JavaFile.builder(key.packageName(), helloWorld)
.build();

javaFile.writeTo(filer);
}
}

generate in FactoryBuilder using javapoet to easy generate Java code, and filer will do the rest to write files inside build folder.

That’s it, we can back to our main project.

@AutoFactory
public interface Animal {
void bark();
}
@AutoElement(tag = "dog")
public class Dog implement Animal {
@Override
public void bark() { System.out.println("woof"); }
}
@AutoElement(tag = "cat")
public class Cat implement Animal {
@Override
public void bark() { System.out.println("meow"); }
}

Mark the relevant annotations on suitable Classes, and after build success you’ll find a class named AnimalFactory in your build folder like below.

public final class AnimalFactory {
public static Animal createAnimal(String type) {
switch(type) {
case "cat": return new Cat();
case "dog": return new Dog();
}
throw new RuntimeException("not support type");
}
}

Through Annotation Processing, we can get rid of the hard maintain Factory class now. Enjoy it.

--

--

Jintin

Android/iOS developer, husband and dad. Love to build interesting things to make life easier.