Eclipse JDT之APT那些事之二

来源:互联网 发布:java session永不失效 编辑:程序博客网 时间:2024/06/05 21:49

接第一篇 Eclipse JDT之APT那些事之一

原文链接:http://deors.wordpress.com/2011/10/08/annotation-processors/

Code Generation using Annotation Processors in the Java language – part 2: Annotation Processors

This post is the second part in my series about Code Generation using Annotation Processors in the Java language. In part 1 (read ithere) we introduced what Annotations are in the Java language and some of their common uses.

Now in part 2 we are going to introduce Annotation Processors, how to build them and how to run them.

Code Generation using Annotation Processors in the Java language – part 2: Annotation Processors

Annotations are great, sure. You can set any kind of metadata or configuration with them, with a well defined syntax and different types to use.

From what we have seen until now, annotations have advantages compared with Javadocs but not enough to justify their inclusion into the language. Therefore, is it possible to interact with annotations and get the most from them? Sure it is:

  • At runtime, annotations with runtime retention policy are accessible through reflection. The methods getAnnotation() and getAnnotations() in Class class will do the magic (1).
  • At compile time, Annotation Processors, a specialized type of classes, will handle the different annotations found in code being compiled.

The Annotation Processor API

When Annotations were first introduced in Java 5, the Annotation Processor API was not mature or standardized. A standalone tool named apt, the Annotation Processor Tool, was needed to process annotations, and the Mirror API, used by apt to write custom processors, was distributed in com.sun.mirror packages.

Starting with Java 6, Annotation Processors were standardized through JSR 269 (2), incorporated into the standard libraries and the tool apt seamlessly integrated with the Java Compiler Tool, javac.

Although we will only describe in detail the new Annotation Processor API from Java 6, you can find more information about apt and the Mirror API in JDK 5 documentationhere andhere and a fine example in this article.

An annotation processor is no more than a class that implements javax.annotation.processing.Processor interface and adheres to the given contract. For our convenience an abstract implementation with common functionality for custom processors is provided in the class javax.annotation.processing.AbstractProcessor.

The custom processor may use three annotations to configure itself:

  • javax.annotation.processing.SupportedAnnotationTypes: This annotation is used to register the annotations that the processor supports. Valid values are fully qualified names of annotation types – wildcards are allowed.
  • javax.annotation.processing.SupportedSourceVersion: This annotation is used to register the source version that the processor supports.
  • javax.annotation.processing.SupportedOptions: This annotation is used to register allowed custom options that may be passed through the command line.

Finally, we provide our implementation of the process() method.

Writing our first Annotation Processor

Let’s start writing our first Annotation Processor. Following the general notes on previous section, we build the following class to process the Complexity annotation introduced in part 1:

    package sdc.assets.annotations.processors;    import …    @SupportedAnnotationTypes("sdc.assets.annotations.Complexity")    @SupportedSourceVersion(SourceVersion.RELEASE_6)    public class ComplexityProcessor extends AbstractProcessor {        public ComplexityProcessor() {            super();        }        @Override        public boolean process(Set<? extends TypeElement> annotations,                               RoundEnvironment roundEnv) {            return true;        }    }

This incomplete class, although does nothing when called, is registered to support annotations of type sdc.assets.annotations.Complexity. Therefore, each time the Java Compiler founds a class annotated with that type will execute the processor, given that the process is available in the classpath (we will see more about that later).

To interact with the annotated class, the process() method receives two parameters:

  • A set of java.lang.model.TypeElement objects: Annotation processing is done in one or several rounds. In each round the processors are called and they receive in this set the types of the annotations being processed in the current round.
  • A javax.annotation.processing.RoundEnvironment objects: This object gives access to the annotated source elements being processed in the current and previous round.

In addition to the two parameters, a ProcessingEnvironment object is available in the processingEnv instance variable. This object gives access to the log and also to a few utilities; some will be discussed later.

Using the RoundEnvironment object and the reflective methods of the Element interface, we can write a simple implementation for an annotation processor that just logs the complexity of each annotated element found:

    for (Element elem : roundEnv.getElementsAnnotatedWith(Complexity.class)) {        Complexity complexity = elem.getAnnotation(Complexity.class);        String message = "annotation found in " + elem.getSimpleName()                       + " with complexity " + complexity.value();        processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, message);    }    return true; // no further processing of this annotation type

Packaging and registering the Annotation Processor

The final step needed to finish our custom Annotation Processor, is to package and register it so the Java Compiler or other tools can find it.

The easiest way to register the processor is to leverage the standard Java services mechanism:

  • Package your Annotation Processor in a Jar file.
  • Include in the Jar file a directory META-INF/services.
  • Include in the directory a file named javax.annotation.processing.Processor.
  • Write in the file the fully qualified names of the processors contained in the Jar file, one per line.

The Java Compiler and other tools will search for this file in all provided classpath elements and make use of the registered processors.

For our example, the folder structure and file content will be the following:

Once packaged, we are ready to start using it.

Running Processors with javac

Imagine that you have a Java project that is making use of some custom annotations and that have available annotation processors. In Java 5, compilation and processing were two different steps (and two different tools), but with Java 6 both tasks are integrated in the Java Compiler tool, javac.

If you add to javac’s classpath the annotation processors and they are registered using the service mechanism as described before, they will be executed by javac.

Following our example, this command would compile and process a Java source file that is using the Complexity annotation:

    >javac -cp sdc.assets.annotations-1.0-SNAPSHOT.jar;     sdc.assets.annotations.processors-1.0-SNAPSHOT.jar     SimpleAnnotationsTest.java

The contents of the Java class used for the test is:

    package sdc.startupassets.annotations.base.client;    import ...    @Complexity(ComplexityLevel.VERY_SIMPLE)    public class SimpleAnnotationsTest {        public SimpleAnnotationsTest() {            super();        }        @Complexity() // this annotation type applies also to methods                      // the default value 'ComplexityLevel.MEDIUM' is assumed        public void theMethod() {            System.out.println("console output");        }    }

And the output, when we execute the javac command, is, as expected:

Although the default javac behavior will usually be fine, there are some options that will help us to run annotation processors in any possible scenario:

  • -Akey[=value] : Used to pass options to the processors. Only those options registered with the SupportedOptions annotation are passed to a processor.
  • -proc:{none|only} : By the default, javac will run annotation processors and compile all sources. Using proc:none option, no annotation processing is done – useful when you are building annotation processors themselves. Using proc:only option, only annotation processing is done – useful when you are running validations, as quality tools or standards checkers, inside annotation processors.
  • -processorpath path : Used to specify where the annotation processors and their dependencies can be found. It is optional, and if not present the regular classpath is searched. It is useful to keep a clear differentiation between project dependencies and processors dependencies (that do not need to be carried on for runtime).
  • -s dir : Used to specify where the generated sources will be placed. This directory must exists before javac command is executed although the sub-directory structure that matches package hierarchy of generated sources is created if needed.
  • -processor class1[,class2,class3…] : Used to specify the fully qualified names of the annotation processors that will be executed. When this option is present, the default discovery process, based on the services mechanism as discussed before, is bypassed. This is useful when we want to run only a limited set of processor although there are more registered in the classpath.

Running Processors with Eclipse

The Eclipse IDE and other major IDE’s supports annotation processors and integrate them with the regular building process.

In Eclipse IDE, when you access the properties dialog on a Java project, you can find, inside Java Compiler group, a page of options named Annotation Processing.

In the Annotation Processing options page, activate Enable Annotation Processing (it is not activated by default). Processor options can be passed by using the table in this options page.

Also, select the processors to be executed in the Factory Path options page.

Once configured, each time a build action is triggered in the project, the annotation processors will be executed.

Running Processors with Maven

Annotation processors can also be executed integrated in Apache Maven builds.

The level of automation that we can achieve will allow us to seamlessly integrate all types of tasks with the building process. Validating standards or generating code do not need to be a separate process in a project lifecycle anymore. It also allows seamless integration with Continuous Integration engines.

Although there are other ways to integrate annotation processors with Maven builds, we recommend the approach described here. The approach discussed is based on the same Mojo (Maven plug-in) that take care of compilation tasks. Actually, as it using the Java Compiler tool, the approach produces the most standard approach that we know of.

Integration with Maven will require that both our Annotations and our Annotation Processors are available as Maven artifacts.

We recommend to keep them in different artifacts, as Annotation Processors will not be needed to be accessible to other client projects, so we reduce the number of dependencies across projects.

With this approach, we will setup three different projects, corresponding each one to a Maven artifact:

  • The Annotation artifact. It contains only the Annotation Types.
  • The Annotation Processor artifact. It contains the Annotation Processors. It will depend on the Annotation artifact plus the dependencies needed by the processors themselves to do their tasks. The compiler plugin will need to be configured as proc:none so annotation processing is not performed when building this artifact.
  • The client project. It contains the client code. It will depend on the two previous artifacts.

This is an example of the directory structure and POM for the Annotation artifact:

Note the Maven compiler plug-in version used to build this artifact.

This is an example of the directory structure and POM for the Annotation Processor artifact:

Note the services directory for packaging the processor, the Maven compiler plug-in version used, the proc:none option to prevent running processors when building this one, and the dependency on the artifact that contains the Annotation types.

And finally, this is an example of the directory structure and POM for the client artifact:

Note the two dependencies here to the Annotation types artifact and to the processor artifact.

Once done, each time the compile goal is executed in a Maven build, the processor will be executed as expected.

The series will continue on part 3: Generating source code. Read it here.

(1) Visit Class API documentation for more information here.

(2) The JSR 269, Pluggable Annotation Processor API, can be browsed online here.