Build Your Own Annotation Processors

J2SE 5.0 added support for  annotations , encapsulated data associated with packages, types, fields, constructors, methods, parameters, and local variables. For example, you might associate an annotation with a method to indicate that the method overrides its superclass method. Annotations are also known as  metadata .

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,OverrideSuppressWarningsDocumentedInheritedRetention, 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 the javax.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:

  1. Instantiate the processor and invoke its no-argument constructor.
  2. Invoke the processor's void init(ProcessingEnvironment processingEnv) method with an appropriatejavax.annotation.processing.ProcessingEnvironment instance that allows the processor to write new files (via processingEnv's Filer getFiler() method), report various kinds of messages (via processingEnv'sMessager getMessager() method), and so on.
  3. Invoke the processor's Set<String> getSupportedAnnotationTypes()Set<String> getSupportedOptions(), and SourceVersion getSupportedSourceVersion() methods (which are inherited from the AbstractProcessor class) to return the values of the @SupportedAnnotationTypes,@SupportedOptions, and @SupportedSourceVersion annotations, which are used to annotate the processor class.
  4. Invoke the processor's public abstract boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) method (declared in the Processor 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. The roundEnvparameter makes available an instance of the javax.annotation.processing.RoundEnvironment interface, letting process() 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 that  java.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.TYPEToString 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 @ToStringannotations), 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:

  1. Invoke RoundEnvironment's Set<? extends Element> getElementsAnnotatedWith(Class<? extends Annotation> a) method to return elements annotated with the ToString type. The returned set includes root elements (e.g., a class) and member elements (e.g., a method) of the root elements.
  2. Obtain an iterator to iterate over the set's members.
  3. 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).
  4. If the element is not a class (e.g., you've annotated an interface), output an error message and skip to the next iteration.
  5. Invoke Element's List<? extends Element> getEnclosedElements() method to obtain a list of the class's members.
  6. Obtain an iterator to iterate over the list's members.
  7. For each iteration, obtain the member element and determine if it's the toString() method. Exit the inner loop when this method is found.
  8. 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.
  9. 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 a  Classes.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 AB, and C are not annotated @ToString, so they'll never be checked by ToStringProcessor. However, the processor will examine each of classes DE (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

  1. What are annotations?

  2. What is a processor?

  3. 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.

Get ZIP

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值