Sunday, November 29, 2015

Annotation processing during compilation time: Error Handling

Even though the annotation processing is part of the source code compilation process, the annotation processor just likes any other normal Java program where we could implement the validation logic in order to make sure it processes the annotation properly at compile time. While the annotation processor could throw any runtime exception like what other normal Java programs do, there are other better ways it could handle and deliver the validation error. In this post, I will go through and evaluate different ways of reporting a validation error from an annotation processor.

Given the Java classes with different role below:

Custom annotation type: PrivateField
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.SOURCE)
public @interface PrivateField {
}

Application: Application
public class Application {
    
    @PrivateField
    private String id;
    
    @PrivateField
    String name;
}

The PrivateField annotation type above is used to ensure the annotated field is declared as private. This restriction cannot be enforced by the PrivateField annotation type itself and hence annotation processor come in place to enforce the restriction.

Note: Please refer to Annotation Processing: Annotation Processor Service Loader to learn how the classes are structured in Maven projects and how to register the annotation processor.

By the way, this post is not about designing and implementing a useful annotation type. The code example given here are mainly used to demonstrate how the annotation processor dealing with the error at compile time.

Throwing runtime exception directly

@AutoService(Processor.class)
@SupportedAnnotationTypes("com.hauchee.annotation.PrivateField")
public class PrivateFieldProcessor extends AbstractProcessor {
    
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        for (Element element : roundEnv.getElementsAnnotatedWith(PrivateField.class)) {
            if (!element.getModifiers().contains(Modifier.PRIVATE)) {
                throw new IllegalArgumentException(
                        String.format("The field \"%s\" is not private.",
                                element.getSimpleName()));
            }
        }
        return true;
    }
}

Now, when we compile the Application source code with the annotation processor above, we get the following error at compile time.

Fatal error compiling: java.lang.IllegalArgumentException: The field "name" is not private.

We are able to see the validation message from the error above. However, throwing a runtime exception directly from the annotation processor is deficient because,
  • The error does not tell specific detail such as which Java class introduces the problem. It may too obvious in this example because there is only Application.java in the Application project. When there are many other classes also make use of the same annotation, then it is hard to find out which one is the root cause.
  • The error is giving a misleading impression that the compiler is not functioning properly. Fatal error compiling means the compiler having an issue to perform its job. Compilation error means the compiler done its job and found the Java source code compilation error.
Anyway, this does not mean that we cannot use Java Exception at all in annotation processor. After all it still is a Java application which can be designed and implemented properly.

Return false from process() method

@AutoService(Processor.class)
@SupportedAnnotationTypes("com.hauchee.annotation.PrivateField")
public class PrivateFieldProcessor extends AbstractProcessor {
    
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        for (Element element : roundEnv.getElementsAnnotatedWith(PrivateField.class)) {
            if (!element.getModifiers().contains(Modifier.PRIVATE)) {
                return false;
            }
        }
        return true;
    }
}

The annotation processor above would not cause any error at compile time. However, it had misused of the return parameter of process() method. By returning false, the annotation processor will excuse itself from claiming this annotation type. If there is a good reason to do that, then it is fine. For our case, the validation error requires programmer attention and action. By returning false, this does not resolve the problem but even worst it hide the problem without any notice.

Make use of Annotation Messager

@AutoService(Processor.class)
@SupportedAnnotationTypes("com.hauchee.annotation.PrivateField")
public class PrivateFieldProcessor extends AbstractProcessor {
    
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        for (Element element : roundEnv.getElementsAnnotatedWith(PrivateField.class)) {
            if (!element.getModifiers().contains(Modifier.PRIVATE)) {
                processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR,
                    String.format("The field \"%s\" is not private.",
                            element.getSimpleName()));
            }
        }
        return true;
    }
}

Any Java class that extends javax.annotation.processing.AbstractProcessor will inherit its javax.annotation.processing.ProcessingEnvironment. We can get a set of useful tools from this processingEnv. One of them is javax.annotation.processing.Messager which can be used to reporting annotation processing error. Compile the Application source code with the annotation processor above, we get the following compilation error.

Compilation failure
The field "name" is not private.

Compare to the first approach, the error message above is indeed a compilation error in Application.java. However, the error message above also does not tell any specific detail which could help in fixing the invalid annotation. This could be resolved by passing the affected source element to the Messager as below.

processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR,
    String.format("The field \"%s\" is not private.",
            element.getSimpleName()), element);

Compile the Application source code with the new changes made in annotation processor above, we get the following compilation error.

Compilation failure
com/hauchee/application/Application.java:[11,12] The field "name" is not private.

It specifically tells us that which Java class, at which line causing the compilation error. This information is very useful in identifying the invalid field. In fact, the IDE could digest the error and affected source element reported via Messager and highlights the invalid annotated source element in the script editor. The Netbeans IDE screenshot below proves that the handling of annotation processing error is consistent with other compilation error.


Summary:

Prevent from throwing any runtime exception from annotation processor. We still can make use of Java Exception but catch them in the process() method and report the error via Messager. Whenever possible pass the affected source code element together with the error message as it would help in identifying the invalid source element. Only return false from process() method if you have a good reason to do so.


References:
http://hannesdorfmann.com/annotation-processing/annotationprocessing101/
https://docs.oracle.com/javase/7/docs/api/javax/annotation/processing/Messager.html



No comments: