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



Wednesday, November 18, 2015

Annotation processing during compilation time: Annotation Processor Service Loader

How to register an Annotation Processor

Annotation Processor appears in the form of Service Provider. However, instead of being accessed during application runtime, the annotation processor is being scanned and processed by the Annotation Processing Tool which built in the javac at application compile time

This post mainly demonstrates how to register a working annotation processor to the annotation processing tool in order to process certain annotation that used in an application source code. There are 3 roles here and are built in 3 different Maven projects.
  • Custom annotation type
  • Application
  • Annotation Processor


Annotation Maven Project: 

Custom Annotation type

A custom annotation type named Count is created. It targets to instance field only and having the source code level retention policy. This also means that the Count annotation (which applied in the application source code) information will be discarded after source code compilation.

Note: The source code level retention policy is used in this example but the annotation processor could also process annotation type with other retention policy.
package com.hauchee.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

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

pom.xml

Nothing special. It is just a simple JAR after built.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
                            http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.hauchee</groupId>
    <artifactId>Annotation</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>
</project>


Application Maven Project:

Application Class

A simple class which its fields are annotated by Count annotations.
package com.hauchee.application;

import com.hauchee.annotation.Count;

public class Application {
    @Count
    String name;
    
    @Count
    int age;
}

pom.xml

This project has the dependency on the Annotation artifact (the Maven project you have seen above) and the AnnotationProcessor artifact which you will see it in the following sections.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
                            http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.hauchee</groupId>
    <artifactId>Application</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>
    <dependencies>

        <!-- Dependency to Annotation -->
        <dependency>
            <groupId>com.hauchee</groupId>
            <artifactId>Annotation</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>

        <!-- Dependency to Annotation Processor -->
        <dependency>
            <groupId>com.hauchee</groupId>
            <artifactId>AnnotationProcessor</artifactId>
            <version>1.0-SNAPSHOT</version>
            <optional>true</optional>
        </dependency>

    </dependencies>
</project>


Conventional approach

Annotation Processor Project

Annotation Processor Implementation 

We write an annotation processor by doing the following:
  • Extends javax.annotation.processing.AbstractProcessor which make the class an annotation processor recognizable by the annotation processing tool. 
  • Override getSupportedAnnotationTypes() to tells the tool about the annotation types which are supported by this processor.
  • Provide implementation in process() method to process the annotated elements. The implementation below captures and counts those elements that annotated by Count annotation type.
package com.hauchee.annotationprocessor;

import com.hauchee.annotation.Count;
import java.util.HashSet;
import java.util.Set;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;

public class AnnotationProcessor extends AbstractProcessor {
    
    private int count;

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> annotationTypes = new HashSet<>();
        annotationTypes.add("com.hauchee.annotation.Count");
        return annotationTypes;
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        for (Element element : roundEnv.getElementsAnnotatedWith(Count.class)) {
            System.out.format("\"%s\" is annotated with Count annotation.\n",
                    element.getSimpleName());
            count++;
        }
        if (count > 0) {
            System.out.format("Total of variable element which annotated with "
                    + "Count annotation: %d\n", count);
        }
        count = 0;
        return true;
    }
}

Service Configuration file

As mentioned at the beginning, annotation processor is a service provider. In order to register the processor, we create a file named javax.annotation.processing.Processor in /resources/META-INF/services directory. Then write down the fully qualified class name of the processor in the newly created file. The project structure looks like below:

pom.xml

This project has the dependency on the Annotation artifact. More importantly, we need to disable the annotation processing for this project by specifying -proc:none in maven compiler plugin. By default, the compiler compiles all classes and carries out the annotation processing. Without turning off the annotation processing, we will get the following compilation error:

error: Bad service configuration file, or exception thrown while constructing Processor object: javax.annotation.processing.Processor: Provider com.hauchee.annotationprocessor.AnnotationProcessor not found

Again, as mentioned at the beginning, the annotation processor is scanned and processed by the annotation processing tool at the compile time. That is the time our processor is not compiled yet and therefore, the tool never find it. By turning off the annotation processing, this will allow our processor get compiled.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
                            http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.hauchee</groupId>
    <artifactId>AnnotationProcessor</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>
    
    <dependencies>
        <!-- Dependency to Annotation -->
        <dependency>
            <groupId>com.hauchee</groupId>
            <artifactId>Annotation</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
    </dependencies>
    
    <build>
        <pluginManagement>
            <plugins>
                <plugin>
                    <artifactId>maven-compiler-plugin</artifactId>
                    <version>2.3.2</version>
                    <configuration>
                        <source>1.8</source>
                        <target>1.8</target>
                        <!-- Disable annotation processing for this project. -->
                        <compilerArgument>-proc:none</compilerArgument>
                    </configuration>
                </plugin>
            </plugins>
        </pluginManagement>
    </build>
</project>

Compilation Result:

Now, all projects are set. Build the Application project, the annotation processing kicks in and we get the result below from the compilation output.
"name" is annotated with Count annotation.
"age" is annotated with Count annotation.
Total of variable element which annotated with Count annotation: 2


Handy approach

There is a handy approach which could save us from doing all the manual works and avoid human errors by using AutoSerivice annotation processor developed by Google. Below is the enhanced version of Annotation Processor project.

Annotation Processor Implementation 

The implementation is improved by
  • Using AutoService annotation which help to auto generate service configuration file at compile time.
  • Using SupportedAnnotationTypes annotation which save us from overriding the getSupportedAnnotationTypes() method.
package com.hauchee.annotationprocessor;

import com.google.auto.service.AutoService;
import com.hauchee.annotation.Count;
import java.util.Set;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Processor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;

@AutoService(Processor.class)
@SupportedAnnotationTypes("com.hauchee.annotation.Count")
public class AnnotationProcessor extends AbstractProcessor {
    
    private int count;

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        for (Element element : roundEnv.getElementsAnnotatedWith(Count.class)) {
            System.out.format("\"%s\" is annotated with Count annotation.\n",
                    element.getSimpleName());
            count++;
        }
        if (count > 0) {
            System.out.format("Total of variable element which annotated with "
                    + "Count annotation: %d\n", count);
        }
        count = 0;
        return true;
    }
}

pom.xml

Added auto-service dependency so that we could make sure of AutoService annotation. The -proc:none compiler configuration option is no longer required because the service file not explicitly exists hence never scanned by the compiler. The service file is then auto-generated by com.google.auto.service.processor.AutoServiceProcessor when this processor is scanned and processed by annotation processing tool.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" 
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
                            http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.hauchee</groupId>
    <artifactId>AnnotationProcessor</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>
    
    <dependencies>
        <!-- Dependency to Annotation -->
        <dependency>
            <groupId>com.hauchee</groupId>
            <artifactId>Annotation</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        
        <!-- Dependency to AutoService annotation processor -->
        <dependency>
            <groupId>com.google.auto.service</groupId>
            <artifactId>auto-service</artifactId>
            <version>1.0-rc2</version>
            <optional>true</optional>
        </dependency>
    </dependencies>
</project>

Compilation Result:

Compile Application project and viola, we get the same result from compilation output.

Conclusion:

The annotation processor example given in this post is a dummy as this post mainly telling the ways to register a service. In fact, it could do a lot of useful stuff. AutoService processor is the best example. It changed and simplified the way we register a service and make the service registration piece of cake.