Java has always provided adhoc annotation mechanisms -- the transient keyword (which marks those instance fields that are to be ignored during serialization -- class fields can be marked transient , but they are not serialized anyway so doing so is redundant) and Javadoc's @deprecated tag (which documents methods that are no longer supported) are examples. |
This support consists of @interface
(for introducing new annotation types) and seven standard types: Deprecated
,Override
, SuppressWarnings
, Documented
, Inherited
, Retention
, and Target
-- the final four types are used to annotate new annotation types. J2SE 5.0 also introduced an apt
tool for processing annotations.
Java SE 6 kept these annotation types and also introduced new annotation types (particularly in the Java Architecture for XML Binding, Java API for XML Web services, Java Web Service, and Java Management Extensions APIs). Furthermore, it introduced a framework for processing annotations and extended javac
to support annotation processing. (apt
was removed in Java SE 8.)
This article shows you how to use this framework to build annotation processors in Java SE 6 and beyond. After introducing annotation processing fundamentals (where you discover key types and methods), I present an example consisting of a ToString
annotation type and a ToStringProcessor
annotation processor (for Java SE 7 and beyond) that (when used) requires classes to override the toString()
method.
Annotation Processing Fundamentals
Central to the annotation-processing framework is the processor , which is a class that implements thejavax.annotation.processing.Processor
interface or subclasses this interface's implementing
javax.annotation.processing.AbstractProcessor
class.
A tool (such as javac
) performs the following sequence of steps to interact with a processor:
- Instantiate the processor and invoke its no-argument constructor.
- Invoke the processor's
void init(ProcessingEnvironment processingEnv)
method with an appropriatejavax.annotation.processing.ProcessingEnvironment
instance that allows the processor to write new files (viaprocessingEnv
'sFiler getFiler()
method), report various kinds of messages (viaprocessingEnv
'sMessager getMessager()
method), and so on. - Invoke the processor's
Set<String> getSupportedAnnotationTypes()
,Set<String> getSupportedOptions()
, andSourceVersion getSupportedSourceVersion()
methods (which are inherited from theAbstractProcessor
class) to return the values of the@SupportedAnnotationTypes
,@SupportedOptions
, and@SupportedSourceVersion
annotations, which are used to annotate the processor class. - Invoke the processor's
public abstract boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv)
method (declared in theProcessor
interface) in a series of rounds (process()
invocations) to process a subset of the annotations found on the source and class files produced by a prior round -- rounds are needed because new annotations can be introduced in a given round and they need to be processed.The
annotations
parameter specifies the current round's subset of supported annotations. TheroundEnv
parameter makes available an instance of thejavax.annotation.processing.RoundEnvironment
interface, lettingprocess()
obtain information about the current round of processing. After performing its processing,process()
returns true when it claims the annotations (so that other processors cannot process them), or returns false when it makes the annotations available to subsequent processors.
Whether or not a processor runs depends on the the processor's supported annotation types, the annotations present on a file's root elements (e.g., a class), or whether another processor has claimed its processed annotations. |
For a given round, the tool computes the set of annotation types on the root elements, which is passed to process()
'sannotations
parameter. As processors claim annotation types, they're removed from the set of unmatched annotations. When the set is empty or no more processors are available, the round has run to completion.
RoundEnvironment
provides a boolean processingOver()
method that returns true when the current round is finished and types generated by this round are not subject to another round of processing. When this method returns true, process()
's annotations
argument references an empty set of annotation types.
Some processors will need to create XML or other files in response to processing annotations. The annotation-processing framework supports this task by providing the javax.annotation.processing.Filer
interface. This interface provides the following methods to create source, class, and resource files, and also to read an existing resource file:
-
JavaFileObject createClassFile(CharSequence name, Element... originatingElements) throws IOException
-
FileObject createResource(JavaFileManager.Location location, CharSequence pkg, CharSequence relativeName, Element... originatingElements) throws IOException
-
JavaFileObject createSourceFile(CharSequence name, Element... originatingElements) throws IOException
-
FileObject getResource(JavaFileManager.Location location, CharSequence pkg, CharSequence relativeName) throws IOException
The javax.annotation.processing.Messager
interface provides methods for outputting error messages, warnings, and other notices to tool-specific locations. Elements, annotations, and annotation values can be passed to provide a location hint for the message, but this hint may be unavailable or only approximate. The following methods are available:
-
void printMessage(Diagnostic.Kind kind, CharSequence msg)
-
void printMessage(Diagnostic.Kind kind, CharSequence msg, Element e)
-
void printMessage(Diagnostic.Kind kind, CharSequence msg, Element e, AnnotationMirror a)
-
void printMessage(Diagnostic.Kind kind, CharSequence msg, Element e, AnnotationMirror a, AnnotationValue v)
For each method, kind
identifies the kind of message to output via a javax.tools.Diagnostic.Kind
enumerated value (e.g., ERROR
or WARNING
). Also, msg
identifies the message to output. Outputting an error message raises an error, which can be detected in a subsequent processing round via RoundEnvironment
's boolean errorRaised()
method.
Require Subclasses to Override toString()
Suppose you want to ensure thatjava.lang.Object
's
public String toString()
method is overridden by various subclasses. During development, you mark these classes with an annotation stating that the method has been overridden. If you forget to override the method, the annotation processor should alert you to your forgetfulness.
Listing 1 describes a ToString
annotation type whose @ToString
annotation instances could be used for this task.
import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target(ElementType.TYPE) @Retention(RetentionPolicy.SOURCE) public @interface ToString { }
Listing 1: ToString.java
Listing 1 introduces ToString
as an annotation type via the @interface
syntax. Instances of this type (annotations) are prefixed with the @
character. Because ToString
specifies no fields, @ToString
instances are known as marker annotations -- they exist only to mark program elements.
The @Target
annotation identifies the kinds of program elements to which the associated annotation type applies -- if@Target
isn't present, the type applies to all elements. By specifying ElementType.TYPE
, ToString
annotations apply to classes, interfaces (including annotation types), and enums. The compiler enforces this usage restriction.
The @Retention
annotation identifies how long instances of the associated annotation type are retained -- if@Retention
isn't present, these instances are recorded in the classfile, but don't need to be retained by the virtual machine at runtime. By specifying RetentionPolicy.SOURCE
, these annotations are not stored in the classfile.
Listing 2 introduces a ToStringProcessor
class for processing @ToString
annotations.
import static javax.lang.model.SourceVersion.RELEASE_7; import java.util.Iterator; import java.util.List; import java.util.Set; import javax.annotation.processing.AbstractProcessor; import javax.annotation.processing.RoundEnvironment; import javax.annotation.processing.SupportedAnnotationTypes; import javax.annotation.processing.SupportedSourceVersion; import javax.lang.model.element.Element; import javax.lang.model.element.ElementKind; import javax.lang.model.element.TypeElement; import javax.tools.Diagnostic; @SupportedAnnotationTypes("ToString") @SupportedSourceVersion(RELEASE_7) public class ToStringProcessor extends AbstractProcessor { @Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { if (!roundEnv.processingOver()) { Set<? extends Element> elements; elements = roundEnv.getElementsAnnotatedWith(ToString.class); Iterator<? extends Element> iter = elements.iterator(); while (iter.hasNext()) { Element element = iter.next(); if (element.getKind() != ElementKind.CLASS) { error("@ToString must prefix a class -- "+element+ " is not a class"); continue; } List<? extends Element> subElements; subElements = element.getEnclosedElements(); Iterator<? extends Element> iterChild = subElements.iterator(); boolean found = false; while (iterChild.hasNext()) { Element subElement = iterChild.next(); if (subElement.toString().equals("toString()")) { found = true; break; } } if (!found) error("toString() not overridden in class "+element); } } return true; } void error(String msg) { processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, msg); } }
Listing 2: ToStringProcessor.java
ToStringProcessor
is annotated with @SupportedAnnotationTypes("ToString")
(it processes only @ToString
annotations), and is also annotated with @SupportedSourceVersion(RELEASE_7)
(it supports Java SE 7 as the latest source version). Its process()
method completes the steps below when called, and when its processingOver()
method returns false:
- Invoke
RoundEnvironment
'sSet<? extends Element> getElementsAnnotatedWith(Class<? extends Annotation> a)
method to return elements annotated with theToString
type. The returned set includes root elements (e.g., a class) and member elements (e.g., a method) of the root elements. - Obtain an iterator to iterate over the set's members.
- For each iteration, obtain the element's
javax.lang.model.element.Element
instance and invoke itsElementKind getKind()
method to determine the kind of element (e.g.,javax.lang.model.element.ElementKind.CLASS
). - If the element is not a class (e.g., you've annotated an interface), output an error message and skip to the next iteration.
- Invoke
Element
'sList<? extends Element> getEnclosedElements()
method to obtain a list of the class's members. - Obtain an iterator to iterate over the list's members.
- For each iteration, obtain the member element and determine if it's the
toString()
method. Exit the inner loop when this method is found. - When the inner loop ends, output an error message if
toString()
wasn't found -- this method must be present in a class annotated with@ToString
. - Always claim annotation types by returning true.
Now that you know how ToString
is declared and how ToStringProcessor
works, compile these source files by executing the following command:
javac ToStringProcessor.java
ToStringProcessor Demonstration
I've created aClasses.java
source file that presents various instances of
ToString
. Check out Listing 3.
class A { } class B { @Override public String toString() { return "B"; } } class C { public String toString(int x) { return "C "+x; } } @ToString class D { } class E { @ToString @Override public String toString() { return "E"; } } @ToString class F { public String toString(int x) { return "F "+x; } } @ToString interface G { }
Listing 3: Classes.java
Listing 3 declares six classes and one interface. Classes A
, B
, and C
are not annotated @ToString
, so they'll never be checked by ToStringProcessor
. However, the processor will examine each of classes D
, E
(because @ToString
is used in a method context), and F
, and interface G
.
The javac
compiler's -processor
option lets you identify one or more processors to process a source file's annotations. Assuming that ToString.java
and ToStringProcessor.java
compiled successfully, execute the following command to apply ToStringProcessor
to Classes.java
before compiling this source file:
javac -processor ToStringProcessor Classes.java
ToStringProcessor
outputs four messages via the compiler. Two of these messages indicate that public String toString()
hasn't been overridden in the subclass, and two of these messages indicate that @ToString
isn't applied to a class:
error: toString() not overridden in class D error: @ToString must prefix a class -- toString() is not a class error: toString() not overridden in class F error: @ToString must prefix a class -- G is not a class 4 errors
The presence of these error messages causes the compiler to terminate without compiling Classes.java
-- there's no point in continuing.
Suppose you executed the following command line, which doesn't specify a processor:
javac Classes.java
You would observe the following error message because @ToString
doesn't apply to methods:
Classes.java:29: error: annotation type not applicable to this kind of declaration @ToString ^ 1 error
You'll observe the same error message when attempting to compile Classes.java
with the Java SE 8 compiler, regardless of the -processor ToStringProcessor
option. Unlike Java SE 7's compiler, it appears that Java SE 8's compiler validates the targets of source code annotations before executing an annotation processor.
Exercises
- What are annotations?
- What is a processor?
- How is the
javac
compiler tool made aware of processors?
Summary
J2SE 5.0 added support for annotations via@interface
, seven standard types, and an annotation-processing tool. Java SE 6 kept these standard types, introduced new annotation types, introduced a new framework for processing annotations, and extended
javac
to support annotation processing. This article introduced this framework and presented an example annotation processor for detecting overridden
toString()
methods.