Skip to content

vincenzomazzeo/apt-tutorial

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

5 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Using Annotation Processing Tool (APT) for Java code generation

A tutorial about Java Annotation Processing Tool.

Table of Contents

Introduction

“The apt tool is a command-line utility for annotation processing. It includes a set of reflective APIs and supporting infrastructure to process program annotations (JSR 175). These reflective APIs provide a build-time, source-based, read-only view of program structure. They are designed to cleanly model the Java programming language's type system after the addition of generics (JSR 14).

The apt tool first runs annotation processors that can produce new source code and other files. Next, apt can cause compilation of both original and generated source files, thus easing the development cycle.” (Oracle APT)


In the last times I have used APT to auto generate Java code in order to improve developers’ productivity and to minimize errors. Since the APT is integrated in the Java compiler, it is activated by default at compile time so no extra effort is required to adopt it in the projects. Moreover modern IDEs, such as Eclipse, have built-in features to support it at development time. Since the APT works at compile time, all the code (annotations, processors, etc.) needed to activate the APT has to be included in the classpath. Moreover the code used by the APT cannot have direct reference to the source code. At least the source code cannot be modified: only new classes can be created.

In this tutorial I’ll show a simple example about how to use it. In order to generate Java code I’ll use Code Model.

Overview

In this tutorial there are two projects, one containing the code needed to activate the APT, called code-generator, and one representing a hypothetical client project, called example.

Typically the annotations used to activate the APT are included in a third project referred by both the code generator and the client project in order to prevent the client project from having a dependency from the code generator project.

Project' Dependencies

The hypothetical client project requires that every entity of the model can be uniquely identified on the basis of one or more of its fields. Moreover when a field is modified a Journal Monitor has to be notified. The first solution is that every entity class overrides the equals and hashCode methods and that every set method invokes the Journal Monitor to notify it about the update. This approach is error prone and it is expensive to make changes. The second solution is to automate the process using APT to generate the boiler plate code. As the source code cannot be modified during the APT execution, it is necessary to design the model architecture in order to support this constraint. The following class diagram describes the model architecture adopted to allow the usage of the APT for code generation.

Model Class Diagram

The equals and hashCode methods are placed in an external class called Key that extends AbstractModelKey and that is auto generated by the APT. The equals and hashCode methods of the AbstractModel delegate the operations to the related Key class: the instance of the Key class is created using the Reflections. For each entity, an abstract class is created and it is annotated in order to activate APT during the compilation. These abstract classes extend AbstractModel and they contain only the attributes as the set and get methods will be included in the auto generated classes that will extend the abstract entity class. Every set method will invoke the notifyUpdate method of AbstractModel. Two annotations will be created: Model and ModelId. The first is used to annotate the abstract entity class and will activate the APT, the second is used to annotate the attributes representing the key.

APT Code Generator Project

The Code Generator project contains the annotations used to activate the APT, the Processor and all the support classes needed to generate the code.

Code Generator Project Structure

Annotations

There are only two annotations, Model and ModelId.

Model

The Model annotation is used to activate the APT.

@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE)
public @interface Model {}

The @Retention annotation specifies the scope of the Model annotation. We want the compiler to discard it after the compilation phase so we set it to SOURCE. The @Target annotation specifies that this annotation can be applied only to TYPE elements. In fact we want to apply the Model annotation only to classes.

ModelId

The ModelId annotation is used to generate the Key class. It is not used to activate the APT but it is used during the processing of the Model annotation.

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

As well as for the Model annotation we don’t want to retain the ModelId annotation after the compilation phase so the @Retention is set to SOURCE. The @Target is set to FIELD as we want to apply the annotation only to the fields of the models.

ModelProcessor

The APT needs the Annotation Processors to operate. Each Processor can process more than one annotation. All the annotations a Processor can process are specified by the @SupportedAnnotationTypes annotation. Each Processor inherits from AbstractProcessor and have to override the process method.

The ModelProcessor is the Processor used to process the Model annotation.

@SupportedAnnotationTypes({
"it.ninjatech.apt.codegenerator.annotation.Model"
})
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class ModelProcessor extends AbstractProcessor {}

I have overridden the init method too, that is invoked by the APT to initialize the Processor. In this method I set some helper objects I will use after:

  • Filer used to create new files.
  • Messager used to report error messages, warnings and other notices.
  • Element Utils containing utility methods for operating on program elements.
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
    super.init(processingEnvironment);

    this.filer = processingEnvironment.getFiler();
    this.messager = processingEnvironment.getMessager();
    this.elementUtils = processingEnvironment.getElementUtils();
}

As said, the Processor has to override the process method

@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnvironment) {
    try {
        Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(Model.class);
        if (!elements.isEmpty()) {
            JCodeModel codeModel = new JCodeModel();
            ModelGenerator modelGenerator = new ModelGenerator(codeModel);
            ModelKeyGenerator modelKeyGenerator = new ModelKeyGenerator(codeModel);

            for (Element element : elements) {
                if (element.getKind() == ElementKind.CLASS) {
                    TypeElement modelType = (TypeElement) element;
                    generate(modelType, modelGenerator, modelKeyGenerator);
                    this.messager.printMessage(Kind.NOTE, String.format("[Model] Generated class %s", modelType.getQualifiedName().toString()));
                }
            }

            codeModel.build(new FilerCodeWriter(this.filer));
        }
    }
    catch (Exception e) {
        this.messager.printMessage(Kind.ERROR, e.getMessage());
    }

    return true;
}

In the process method I retrieve all the elements annotated by the Model annotation and for each element I call the generate method to generate the Model and the ModelKey classes. I use the Code Model library to generate the code.

The classes used by the APT belong to the javax.lang.model package and subpackages.

SPI

To notify APT about Processors it can be used the Annotation Processing Factories or the Service Provider Interface (SPI). In this tutorial I use SPI. I create a file called javax.annotation.processing.Processor located in the META-INF/services in which I put the fully qualified name of the processor:

it.ninjatech.apt.codegenerator.ModelProcessor

POM

The project uses Apache Maven and in order to create a JAR containing all the third party libraries I add the assembly plugin.

<plugin>
	<groupId>org.apache.maven.plugins</groupId>
	<artifactId>maven-assembly-plugin</artifactId>
	<version>${assembly-plugin.version}</version>
	<executions>
		<execution>
			<phase>package</phase>
			<goals>
				<goal>single</goal>
			</goals>
		</execution>
	</executions>
	<configuration>
		<descriptorRefs>
			<descriptorRef>jar-with-dependencies</descriptorRef>
		</descriptorRefs>
		<appendAssemblyId>false</appendAssemblyId>
	</configuration>
</plugin>

The need to create a single JAR with all the dependencies is to avoid adding several JARs to the classpath.

APT Example Project

The Example project represents the hypothetical client project.

Example Project Structure

In this project there are the ModelJournalMonitor, AbstractModel and AbstractModelKey classes. Moreover there is the AbstractProduct class that is the first entity of the project.

ModelJournalMonitor

The ModelJournalMonitor is the journal monitor that has to be notified when an update to an entity takes place. It’s a Singleton and exposes two methods

public static ModelJournalMonitor getInstance()

that returns the instance of the journal monitor and

public void notifyUpdate(AbstractModel model)

invoked when an entity is updated.

AbstractModel

AbstractModel is the abstract class of the entities.

It exposes only one abstract method

protected abstract String keyQualifiedName();

that returns the fully qualified name of the related Key class and it is invoked by the getKey method to create a new instance of the Key

public final <Model extends AbstractModel> AbstractModelKey<Model> getKey() {
    AbstractModelKey<Model> result = null;

    try {
        result = (AbstractModelKey<Model>)Class.forName(keyQualifiedName()).getConstructor(this.getClass()).newInstance(this);
    }
    catch (Exception e) {
        throw new RuntimeException(String.format("Key construction failed: %s", keyQualifiedName()), e);
    }

    return result;
}

It implements the hashCode and equals methods delegating the execution to the Key class

@Override
public int hashCode() {
    AbstractModelKey<?> key = getKey();
    return key.hashCode();
}

@Override
public boolean equals(Object obj) {
    if (this == obj) {
        return true;
    }
    if (obj == null) {
       return false;
    }
    if (getClass() != obj.getClass() || !(obj instanceof AbstractModel)) {
        return false;
    }
    AbstractModelKey<?> myKey = getKey();
    AbstractModelKey<?> otherKey = ((AbstractModel)obj).getKey();

    return myKey.equals(otherKey);
}

Moreover it contains the notifyUpdate method used by the children classes to notify the journal monitor about the update of the entity

protected final void notifyUpdate() {
    ModelJournalMonitor.getInstance().notifyUpdate(this);
}

AbstractModelKey

AbstractModelKey is the abstract class of the Keys. It exposes only two abstract methods

@Override
public abstract int hashCode();

@Override
public abstract boolean equals(Object other);

AbstractProduct

AbstractProduct is the first entity class. It is annotated with the Model annotation in order to activate APT and it contains only two attributes, id and name. The id attribute is annotated with ModelId as it is the key of the entity.

@Model
public abstract class AbstractProduct extends AbstractModel {

    @ModelId
    protected Integer id;
    protected String name;
    
}

When the APT is executed it produces two classes Product and ProductKey. The generated classes are saved in the target/generated-sources/annotations directory.

Note: Since the generated classes are placed under the target directory they are not versioned by a code versioning tool.

Product

Following is the generated code of the Product class

@Generated(value = "it.ninjatech.apt.example.model.Product", date = "2016-08-29T15:55:35+0200")
public class Product
    extends AbstractProduct
{


    @Override
    public String keyQualifiedName() {
        return "it.ninjatech.apt.example.model.key.ProductKey";
    }

    public Integer getId() {
        return this.id;
    }

    public void setId(Integer id) {
        this.id = id;
        notifyUpdate();
    }

    public String getName() {
        return this.name;
    }

    public void setName(String name) {
        this.name = name;
        notifyUpdate();
    }

}

The class overrides the keyQualifiedName returning the name of the ProductKey class and it contains the set and get methods of the two attributes, id and name. The set methods invoke the notifyUpdate method as expected.

ProductKey

Following is the generated code of the ProductKey class

@Generated(value = "it.ninjatech.apt.example.model.key.ProductKey", date = "2016-08-29T15:55:35+0200")
public class ProductKey
    extends AbstractModelKey<Product>
{

    private final Integer id;

    public ProductKey(Product model) {
        this.id = model.getId();
    }

    public ProductKey(Integer id) {
        this.id = id;
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = ((prime*result)+((id == null)? 0 :id.hashCode()));
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null) {
            return false;
        }
        if (getClass()!= obj.getClass()) {
            return false;
        }
        ProductKey other = ((ProductKey) obj);
        if (this.id == null) {
            if (other.id!= null) {
                return false;
            }
        } else {
            if (!this.id.equals(other.id)) {
                return false;
            }
        }
        return true;
    }

    public Integer getId() {
        return this.id;
    }

}

It overrides the hashCode and equals methods evaluating the result on the base of the Product id attribute, as expected.

Integration with Eclipse

Since the APT is integrated into the Java compiler it can be used by Eclipse too. In fact, Eclipse, has an option to enable annotation processing. In the project properties, under Java Compiler, there is the Annotation Processing settings pane.

Annotation Processing Settings Pane

To add our code generator we have to enable the project specific settings in the Factory Path pane and add the code generator JAR to the Plug-ins and JARs that contain annotation processors list

Factory Path Settings Pane

Now we can enable the project specific settings in the Annotation Processing pane, enable annotation processing and set the generated source directory to /target/generated-sources/annotations.

Annotation Processing Settings Pane

Applying the new settings the project structure changes and now Eclipse shows the generated classes.

Changed Example Project Structure

It is possible to access to the messages published by the Messager class opening the Error Log

Eclipse Error Log

At this point every time the AbstractProduct class is saved the generated code is produced.

Conclusion

The APT is a powerful tool to automate some development tasks. It is integrated into the Java compiler so it requires no extra effort to adopt it. There are many situations in which APT can be used, especially when there are much boiler plate code. Moreover the main IDEs are ready to take advantage of it.

License

Released and distributed under the Apache License Version 2.0.

References

Apache Maven
Code Model
Oracle APT
Reflections

About

A tutorial about Java Annotation Processing Tool

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Languages