August 27, 2009

Annotation checking at compile time with Java Annotation Processor

Some weeks ago I implemented a feature which collects some information on runtime from some classes. I preferred using annotations against interfaces for flexibility. Practically the task could be described like: decorate with meta information some classes, parse annotations and get information to be stored.

As a sample a had a annotation like:
@Retention( RetentionPolicy.RUNTIME )
@Target( ElementType.METHOD )
public @interface Description
{
}

As you can see, annotation syntax allows me to specify to store for runtime the annotation and that the annotation to be used for methods. That was frustrating for my job, because I would expect to have more flexibility on that. In my specific case, I would like to allow the meta decoration only on methods which returns strings.

I was thinking to document the annotation and specify that if the annotation is not on a method which returns string, the annotation will be ignored. Said and done. But there is a better and fairly simple method to do the task. This is Java Annotation Processor. This feature is documented in JSR 269: Pluggable Annotation Processing API.

The code I wrote is more complex, but for this post I wrote a sample to show the usage of this API.

The JSR 269 states that you can implement a plug-in for the compiler which can handle the annotations. This plug-in can be given as parameter at compile time, so your code will be called when one of your annotation appears in source code.

First step is to create a annotation processor. This can be done by implementing interface javax.annotation.processing.Processor or by extending the class AbstractProcessor from the same package. I used the second way being much easier.
@SupportedAnnotationTypes(value = {"mypackage.LiveDescription"})
@SupportedSourceVersion(SourceVersion.RELEASE_5)
public class AAPProcessor extends AbstractProcessor {

@Override
public boolean process(Set annotations, RoundEnvironment roundEnv)
{
for (TypeElement typeElement : annotations)
{
Set elements = roundEnv.getElementsAnnotatedWith(typeElement);
for (Element element : elements)
{
// we have only one annotation on methods
processElement((ExecutableElement) element);
}
}
return true;
}

private void processElement(ExecutableElement element)
{
String elementTypeStr = element.getReturnType().toString();
if (!"java.lang.String".equals(elementTypeStr))
{
processingEnv.getMessager().printMessage(
Kind.ERROR,
"Method does not return a String",
element);
}
}
}
Some things need to be explained:
  • SupportedAnnotationTypes specify one specific annotation. You can use * and package names also.
  • SupportedSourceVersion specify the Java version. That's OK because we did not had annotations on prior versions of Java language.
  • the process method iterates though annotations and for every one give the code elements annotated with (in my case only methods); after that calls processElement for any annotated element.
  • processElement  verify get the returned type of the method; if the returned type is not java.lang.String that use the message service to signal a syntax error
That's all about processor. As you can see, is quite obvious and simple to implement that. Of course, your implementation will be more flexible (not harcoded) and complicated.

The final step is to give that to the compiler. I used NetBeans (my beloved IDE) for that, but is simple enough in any IDE. You have only to give the compiler a hint about your plug-in. For that you have to:
  • put on compiler options "-processor org.aap.processor.AAPProcessor", aka the -processor option with the fully qualified name of your class. In NetBeans you go on Project Properties->Build->Compiling and put that in "Addidtional Compiler Options" text box.
  • put the class on compiler class path; as I created a jar file containing my annotation processor, you have only to add that to the compiler path. In NetBeans is also trivial. You have to go to Project Properties->Libraries->Compile and add your jar there
That's all. The tested code was:

public class AnnotatedTest {

@Description
public String getName() {
return "String";
}

@Description
public int getAge() {
return 10;
}
}
I bet that the second method will fail at compile time. Do you?

No comments:

Post a Comment