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.


No comments: