The apt Tool for Source-Level Annotation Processing


The apt Tool for Source-Level Annotation Processing

One use for annotation is the automatic generation of "side files" that contain additional information about programs. The "Enterprise Edition" of Java is notorious for making programmers fuss with lots of boilerplate code, and an effort is underway to develop a standardized set of annotations to generate most of it automatically.

In this section, we demonstrate this technique with a simpler example. We write a program that automatically produces bean info classes. You tag bean properties with an annotation and then run a tool that parses the source file, analyzes the annotations, and writes out the source file of the bean info class. Rather than writing our own parser, we use an annotation processing tool called apt that is part of the SDK. We will first describe how to generate the bean info class, and then we will show you how to use apt.

Recall from Chapter 8 that a bean info class describes a bean more precisely than the automatic introspection process can. The bean info class lists all of the properties of the bean. Properties can have optional property editors. The ChartBeanBeanInfo class in Chapter 8 is a typical example.

To eliminate the drudgery of writing bean info classes, we supply an @Property annotation. You can tag either the property getter or setter, like this:

 @Property String getTitle() { return title; } 

or

 @Property(editor="TitlePositionEditor") public void setTitlePosition(int p) { titlePosition = p; } 

Example 13-4 contains the definition of the @Property annotation. Note that the annotation has a retention policy of SOURCE. We analyze the annotation at the source level only. It is not included in class files and not available during reflection.

Example 13-4. Property.java
 1. import java.lang.annotation.*; 2. 3. @Documented 4. @Target(ElementType.METHOD) 5. @Retention(RetentionPolicy.SOURCE) 6. public @interface Property 7. { 8.    String editor() default ""; 9. } 

NOTE

It would have made sense to declare the editor element to have type Class. However, the annotation processor cannot retrieve annotations of type Class since the meaning of a class can depend on external factors (such as the class path or class loaders). Therefore, we use a string to specify the editor class name.


To automatically generate the bean info class of a class with name BeanClass, we carry out the following tasks:

  1. Write a source file BeanClassBeanInfo.java. Declare the BeanClassBeanInfo class to extend SimpleBeanInfo, and override the getPropertyDescriptors method.

  2. For each annotated method, recover the property name by stripping off the get or set prefix and "decapitalizing" the remainder.

  3. For each property, write a statement for constructing a PropertyDescriptor.

  4. If the property has an editor, write a method call to setPropertyEditorClass.

  5. Write code for returning an array of all property descriptors.

For example, the annotation

 @Property(editor="TitlePositionEditor") public void setTitlePosition(int p) { titlePosition = p; } 

in the ChartBean class is translated into

 public class ChartBeanBeanInfo extends java.beans.SimpleBeanInfo {    public java.beans.PropertyDescriptor[] getProperties()    {       java.beans.PropertyDescriptor titlePositionDescriptor          = new java.beans.PropertyDescriptor("titlePosition", ChartBean.class);       titlePositionDescriptor.setPropertyEditorClass(TitlePositionEditor.class)       . . .       return new java.beans.PropertyDescriptor[]       {          titlePositionDescriptor,          . . .       }    } } 

(The boilerplate code is printed in gray color.)

All this is easy enough to do, provided we can locate all methods that have been tagged with the @Property attribute. Rather than writing our own parser, we use apt, the annotation processing tool, that is a part of the JDK.

NOTE

apt is not related to the Advanced Packaging Tool of the Debian Linux distribution.


You can find documentation for apt at http://java.sun.com/j2se/5.0/docs/guide/apt/. The tool processes annotations in source files, using annotation factories to locate annotation processors. There is a mechanism for discovering factories automatically, but for simplicity, we explicitly specify the factory on the command line. To invoke the bean info processor, run


apt -factory BeanInfoAnnotationFactory BeanClass .java

The apt program locates the annotations of the source files that are specified on the command line. It then selects the annotation processors that should be applied. Each annotation processor is executed in turn. If an annotation processor creates a new source file, then the process is repeated. If a processing round yields no further source files, then apt invokes the javac compiler on all source files.

The parser inside apt analyzes each source file and enumerates the classes, methods, fields, and variables. The API used by apt is called the "mirror API" because it is similar to the reflection API, except that it operates on the source level. Currently, the mirror API is a part of the com.sun hierarchy, and the documentation cautions that the API may change in the future. We do not discuss the API in detail, but we do examine the program in Example 13-5 to give you a flavor of its capabilities. You can find a complete API reference in the apt documentation.

The first two methods in the BeanInfoAnnotationFactory class support the factory discovery process (which we are not using). The factory is willing to deal with annotations named Property, and it supports no command-line options. The third method yields the actual processor.

The BeanInfoAnnotationProcessor has a single public method, process, that is called for each file. In the process method, we iterate through all classes, ignore classes that are not public, then iterate through all methods and ignore methods that are not annotated with @Property. Here is the outline of the code:


public void process()
{
   for (TypeDeclaration t : env.getSpecifiedTypeDeclarations())
   {
      if (t.getModifiers().contains(Modifier.PUBLIC))
      {
         for (MethodDeclaration m : t.getMethods())
         {
            Property p = m.getAnnotation(Property.class);
            if (p != null)
            {
               process property
            }
         }
      }
      write bean info source file
   }
}

The code for writing the source file is straightforward, just a sequence of out.print statements. Note that we create the output file with a call to

 env.getFiler().createSourceFile(beanClassName + "BeanInfo"); 

The env object encapsulates the apt processing environment. The Filer class is responsible for creating new files. The filer keeps track of the newly created source files so that they can be processed in the next processing round.

When an annotation processor detects an error, it uses the Messager to communicate with the user. For example, we issue an error message if a method has been annotated with @Property but its name doesn't start with get, set, or is:

 if (!found)    env.getMessager().printError(m.getPosition(),       "@Property must be applied to getXxx, setXxx, or isXxx method"); 

When compiling the BeanInfoAnnotationFactory class, you need to add tools.jar to the class path. That file is contained in the lib subdirectory of your JDK installation:


javac -classpath .:jdk/lib/tools.jar BeanInfoAnnotationFactory.java

NOTE

You need to add the tools.jar file to the class path only when compiling annotation processors. The apt tool locates the library automatically.


In the companion code for this book, we supply you with an annotated file, ChartBean.java.

Run

 apt -factory BeanInfoAnnotationFactory ChartBean.java 

and have a look at the automatically generated file ChartBeanBeanInfo.java.

This example demonstrates how tools can harvest source file annotations to produce other files. The generated files don't have to be source files. Annotation processors may choose to generate XML descriptors, property files, shell scripts, HTML documentation, and so on.

NOTE

Some people have suggested using annotations to remove an even bigger drudgery. Wouldn't it be nice if trivial getters and setters were generated automatically? For example, the annotation

 @Property private String title; 

could produce the methods

 public String getTitle() {return title;} public void setTitle(String title) { this.title = title; } 

However, those methods need to be added to the same class. This requires editing a source file, not just generating another file, and is beyond the capabilities of apt. It would be possible to build another tool for this purpose, but such a tool would probably go beyond the mission of annotations. An annotation is intended as a description about a code item, not a directive for code editing.

The annotation facility gives programmers a lot of power, and with that power comes the potential for abuse. If annotations are used without restraint, they can result in code that becomes incomprehensible to programmers and development tools alike. (C and C++ programmers have had the same experience with unrestrained use of the C preprocessor.)

We offer the following rule of thumb for the responsible use of annotations: Your source files should compile without errors even if all annotations are removed. This rule precludes "meta-source" that is only turned into valid source by an annotation processor.


Example 13-5. BeanInfoAnnotationFactory.java

[View full width]

   1. import com.sun.mirror.apt.*;   2. import com.sun.mirror.declaration.*;   3. import com.sun.mirror.type.*;   4. import com.sun.mirror.util.*;   5.   6. import java.beans.*;   7. import java.io.*;   8. import java.util.*;   9.  10. /**  11.    This class is used to run an annotation processor that creates a BeanInfo file.  12.  */  13. public class BeanInfoAnnotationFactory implements AnnotationProcessorFactory  14. {  15.    public Collection<String> supportedAnnotationTypes()  16.    {  17.       return Arrays.asList("Property");  18.    }  19.  20.    public Collection<String> supportedOptions()  21.    {  22.       return Arrays.asList(new String[0]);  23.    }  24.  25.    public AnnotationProcessor getProcessorFor(Set<AnnotationTypeDeclaration> atds,  26.       AnnotationProcessorEnvironment env)  27.    {  28.       return new BeanInfoAnnotationProcessor(env);  29.    }  30.  31.    /**  32.       This class is the processor that analyzes @Property annotations.  33.    */  34.    private static class BeanInfoAnnotationProcessor implements AnnotationProcessor  35.    {  36.       BeanInfoAnnotationProcessor(AnnotationProcessorEnvironment env)  37.       {  38.          this.env = env;  39.       }  40.  41.       public void process()  42.       {  43.          for (TypeDeclaration t : env.getSpecifiedTypeDeclarations())  44.          {  45.             if (t.getModifiers().contains(Modifier.PUBLIC))  46.             {  47.                System.out.println(t);  48.                Map<String, Property> props = new TreeMap<String, Property>();  49.                for (MethodDeclaration m : t.getMethods())  50.                {  51.                   Property p = m.getAnnotation(Property.class);  52.                   if (p != null)  53.                   {  54.                      String mname = m.getSimpleName();  55.                      String[] prefixes = { "get", "set", "is" };  56.                      boolean found = false;  57.                      for (int i = 0; !found && i < prefixes.length; i++)  58.                         if (mname.startsWith(prefixes[i]))  59.                         {  60.                            found = true;  61.                            int start = prefixes[i].length();  62.                            String name = Introspector.decapitalize(mname.substring (start));  63.                            props.put(name, p);  64.                         }  65.  66.                      if (!found)  67.                         env.getMessager().printError(m.getPosition(),  68.                            "@Property must be applied to getXxx, setXxx, or isXxx  method");  69.                   }  70.                }  71.  72.                try  73.                {  74.                   if (props.size() > 0)  75.                      writeBeanInfoFile(t.getQualifiedName(), props);  76.                }  77.                catch (IOException e)  78.                {  79.                   e.printStackTrace();  80.                }  81.             }  82.          }  83.       }  84.  85.       /**  86.          Writes the source file for the BeanInfo class.  87.          @param beanClassName the name of the bean class  88.          @param props a map of property names and their annotations  89.       */  90.       private void writeBeanInfoFile(String beanClassName, Map<String, Property> props)  91.          throws IOException  92.       {  93.          PrintWriter out = env.getFiler().createSourceFile(beanClassName + "BeanInfo");  94.          int i = beanClassName.lastIndexOf(".");  95.          if (i > 0)  96.          {  97.             out.print("package ");  98.             out.println(beanClassName.substring(0, i));  99.          } 100.          out.print("public class "); 101.          out.print(beanClassName.substring(i + 1)); 102.          out.println("BeanInfo extends java.beans.SimpleBeanInfo"); 103.          out.println("{"); 104.          out.println("   public java.beans.PropertyDescriptor[]  getPropertyDescriptors()"); 105.          out.println("   {"); 106.          out.println("      try"); 107.          out.println("      {"); 108.          for (Map.Entry<String, Property> e : props.entrySet()) 109.          { 110.             out.print("         java.beans.PropertyDescriptor "); 111.             out.print(e.getKey()); 112.             out.println("Descriptor"); 113.             out.print("            = new java.beans.PropertyDescriptor(\""); 114.             out.print(e.getKey()); 115.             out.print("\", "); 116.             out.print(beanClassName); 117.             out.println(".class);"); 118.             String ed = e.getValue().editor().toString(); 119.             if (!ed.equals("")) 120.             { 121.                out.print("         "); 122.                out.print(e.getKey()); 123.                out.print("Descriptor.setPropertyEditorClass("); 124.                out.print(ed); 125.                out.println(".class);"); 126.             } 127.          } 128.          out.println("         return new java.beans.PropertyDescriptor[]"); 129.          out.print("         {"); 130.          boolean first = true; 131.          for (String p : props.keySet()) 132.          { 133.             if (first) first = false; else out.print(","); 134.             out.println(); 135.             out.print("            "); 136.             out.print(p); 137.             out.print("Descriptor"); 138.          } 139.          out.println(); 140.          out.println("         };"); 141.          out.println("      }"); 142.          out.println("      catch (java.beans.IntrospectionException e)"); 143.          out.println("      {"); 144.          out.println("         e.printStackTrace();"); 145.          out.println("         return null;"); 146.          out.println("      }"); 147.          out.println("   }"); 148.          out.println("}"); 149.          out.close(); 150.       } 151. 152. 153.       private AnnotationProcessorEnvironment env; 154.     } 155. } 



    Core JavaT 2 Volume II - Advanced Features
    Building an On Demand Computing Environment with IBM: How to Optimize Your Current Infrastructure for Today and Tomorrow (MaxFacts Guidebook series)
    ISBN: 193164411X
    EAN: 2147483647
    Year: 2003
    Pages: 156
    Authors: Jim Hoskins

    flylib.com © 2008-2017.
    If you may any questions please contact us: flylib@qtcs.net