An annotation-based persistence framework

Use J2SE 5.0 annotations to eliminate getters and setters

I've talked at length in JavaWorld about the downside of the getter/setter idiom (see Resources). This idiom was originally introduced in the JavaBeans spec as a way of "tagging" properties of an object so that an external UI-layout tool (called a BeanBox) could build a property sheet for that object. You would "tag" the property by providing methods like the following:

 String getFoo();
void setFoo( String newValue );

The BeanBox uses the introspection APIs of the Class class to get a list of methods, then it uses pattern matching to find the getter/setter pairs. From these, it infers that a property exists and determines the property's type. (In the current example, a property called Foo is a String.) You are never expected to call these methods yourself; they exist solely for use by the BeanBox.

Interestingly, the writers of the JavaBeans spec understood just how problematic this getter/setter tagging mechanism is. (The main downside, discussed in earlier articles, is that the getter/setter methods expose too much information about the object's implementation, thereby making the underlying class much harder to maintain.) Consequently, the designers provided a more object-oriented solution in the BeanInfo and Customizer interfaces. User-provided implementations of these interfaces let you build a GUI without the getters and setters. Unfortunately, this overly complicated object-oriented approach was described poorly in the spec. The getter/setter approach was easy, and if you didn't understand the object-oriented-related maintenance issues, then the getter/setter approach seemed reasonable. Consequently, the BeanInfo/Customizer approach fell by the wayside and getter/setter strategies have been propagating like rabbits. The fact that you see the idiom everywhere doesn't make it good, however.

Back when JavaBeans were first proposed, many people (including myself) argued for a new keyword in Java to eliminate the need for the getters and setters. With the ability to introduce a new keyword, the Foo property I described earlier could be represented as:

 private @property String foo;

Since foo is private, exposing it to the BeanBox with the new keyword doesn't violate encapsulation. At the time, however, introducing new keywords was anathema, even keywords impossible to confuse with existing identifiers because they included an illegal symbol, like @.

With J2SE 5.0, however, Sun has gotten over its delicacy and made a few major syntactic changes to the language. Now, you can introduce a new keyword (called an annotation) into the language to specify an attribute that can be examined at either compile-time or runtime. You may introduce arbitrary keywords of your own choosing. The only requirements are: the annotation (the keyword) must be preceded with an @ sign, and you must use the annotation as an adjective. (Annotations can go anywhere that you might say static, final, or public.) Finally, you can toss the getters and setters for a much cleaner syntax that does the same thing.

Two great examples of annotations are now built into Java. Consider the situation where you extend one of the Abstract Window Toolkit (AWT)/Swing Adapter classes and accidentally misspell a base-class method name. You think you've overridden a base-class method, but, in fact, have not. This unexpected inheritance can be a hard bug to find, but is now easily detected by the compiler as follows:

 public class myListener implements MouseListener
{   
    @Overrides
    void MousePressed(MouseEvent e)
    {   System.out.println("Mouse button clicked!");
    }
}

The compiler will complain here because the base-class method is called

mousePressed()

(with a lowercase

m

), not

MousePressed()

, as specified in the class definition.

In addition, an @Deprecated annotation is available that is syntactically much cleaner than @deprecated in the Javadoc (because a comment's contents shouldn't affect a class's compatibility).

There are two approaches to processing the annotation. First, Class's introspection APIs provide a way to get the annotations associated with the class itself and with any of the class's fields or methods. BeanBox can use this mechanism to look for special tagged fields to build its property sheet.

If you don't have the source code for your BeanBox, there's another alternative. The apt (Annotation Processing Tool) processor, supplied with the JDK, is a front end for javac that understands annotation and allows you to build Java source code on the fly as it processes the user-supplied source. You supply apt with various annotation processor plug-ins. In the current example, a plug-in could create a wrapper class that exposes annotated properties using the getter/setter idiom that your old-style BeanBox understands. The documentation for apt is really awful, however (even by Sun standards). I'll talk about how to use it in a future article.

In this article, I show you how to use runtime-evaluated annotations by presenting the "export" side of a small persistence framework. This framework isn't meant to solve all persistence-related problems, but it makes a painless job of representing an object's state as an XML string. You can apply the principles I use to replace getters and setters in other applications, such as GUI building and help-system support (annotate a class to specify help information).

Using the XMLExporter class

Listing 1 demonstrates how my persistence framework uses annotations, and Listing 2 shows the associated output.

Listing 1. Test.java: Use the XMLExporter

  1  package com.holub.persist.test;
   2  
   3  import java.io.*;
   4  import java.util.*;
   5  import com.holub.persist.*;
   6  import com.holub.persist.Exportable;
   7  //----------------------------------------------------------------------
   8  @Exportable
   9  class Address
  10  {   private @Persistent             String  street;
  11      private @Persistent             String  city;
  12      private @Persistent             String  state;
  13      private @Persistent("zipcode")  int zip;
  14  
  15      public Address( String street, String city, String state, int zip )
  16      {   this.street = street;
  17          this.city   = city;
  18          this.state  = state;
  19          this.zip    = zip;
  20      }
  21  }
  22  //----------------------------------------------------------------------
  23  public class Test
  24  {   
  25      @Exportable( name="customer", description="A Customer" )
  26      public static class Customer
  27      {   
  28          @com.holub.persist.Persistent
  29          private String  name    = "Allen Holub";
  30          
  31          @Persistent 
  32          private Address streetAddress = 
  33                          new Address( "1234 MyStreet", 
  34                                       "Berkeley", "CA", 99999 );
  35          @Persistent
  36          private StringBuffer notes = new StringBuffer( "Notes go here ");
  37          
  38          private int garbage; // Is not persistant
  39          
  40          @Persistent Collection<Invoice> invoices = new LinkedList<Invoice>();
  41          {   invoices.add( new Invoice(0) );
  42              invoices.add( new Invoice(1) );
  43          }
  44      }
  45      
  46      @Exportable
  47      public static class Invoice
  48      {   private @Persistent int number;
  49          public Invoice( int number ){ this.number = number; }
  50      }
  51  
  52      public static void main(String[] args ) throws IOException
  53      {   Customer x = new Customer();
  54          XmlExporter out = 
  55              new XmlExporter(
  56                  new PrintWriter(System.out, true) );
  57          out.flush( x );
  58      }
  59  }

Listing 2. Test output

  1  <!-- A Customer -->
   2  <customer className="com.holub.persistent.test.Test$Customer"  >
   3      <name>
   4          Allen Holub
   5      </name>
   6      <Address className="com.holub.persistent.test.Address"  name="streetAddress" > 
   7          <street>
   8              1234 MyStreet
   9          </street>
  10          <city>
  11              Berkeley
  12          </city>
  13          <state>
  14              CA
  15          </state>
  16          <zipcode>
  17              99999
  18          </zipcode>
  19      </Address>
  20      <notes classname="java.lang.StringBuffer">
  21          Notes go here 
  22      </notes>
  23      <invoices>
  24          <Invoice className="com.holub.persistent.test.Test$Invoice"  >
  25              <number>
  26                  0
  27              </number>
  28          </Invoice>
  29          <Invoice className="com.holub.persistent.test.Test$Invoice"  >
  30              <number>
  31                  1
  32              </number>
  33          </Invoice>
  34      </invoices>
  35  </customer>

I use annotations to identify both classes that can be written in XML format and also the fields of those classes that should be stored (typically, you don't want to store all the fields). The system handles cycles (an

Employee

that references a

Company

that holds a list of all its

Employee

s) simply: the self-referential field is just ignored. In the example I just gave you, exporting the

Company

exports all the

Employee

s, but none of those

Employee

s contains a

Company...Company

element. Exporting an

Employee

exports the

Company

(and all its

Employee

s, except the original one). Note that this second situation is best avoided by not tagging the

Employee

's

Company

field as persistent. If a persistent field is a

Collection

or

Map

, then the elements are all flushed. In the case of a

Map

, the key values are preserved as well.

Let's look at how the system is used: First, note the import statement on Listing 1's Line 5. Annotations are like medieval monsters: part lion, part eagle, part snake. In the current situation, they behave like interfaces. As you'll see in a moment, an annotation is declared more or less like an interface declaration. The file that contains the declaration is compiled normally and normally imported into your code. I've used both the * form and an explicit import in Listing 1 just to demonstrate that both forms of import work as expected.

You can also use a fully qualified name to disambiguate if necessary. Ignoring the @ for a moment, both

 @com.holub.persist.Persistent private String name1;

and

 import com.holub.persist.*;
//...
@Persistent private String name2;

work as expected.

Referring back to Listing 1, classes are marked as "exportable" by prefixing them with

@Exportable

. The

Address

class (on Line 9) is an example, as is the

Test.Customer

inner class (on Line 26).

The annotation for

Test.Customer

(on Line 26) demonstrates that annotations can have named arguments. An annotation can also have a single, unnamed argument, as in

@Persistent("zipcode")

on Line 13. You can't mix the two forms, however.

To see what these arguments are doing, look at the test program's output in Listing 2. In general, element names are taken from the field's declared class. Given the field declaration:

   private @Persistent String name;

The value of that field is output as:

 <name>
    Allen Holub
</name>

There is one exception. If a class is tagged as @Exportable, then objects of that class are output with the class name instead of the field name as the element name. For example, the streetAddress field (on Line 32 of Listing 1) is a reference to an @Exportable class—an Address. Consequently, this field is written as:

 <Address>
    ...
</Address>

You can override these default element names in the annotations. The

name="customer"

argument on Line 25 of Listing 1 tells the framework to use

customer

(with a lowercase

c

) rather than

Customer

(with an uppercase

C

—the class name). I've also specified a second argument in the same annotation: The

description="A Customer"

argument to

@Exportable

translates into the comment on the first line of the output file.

The element's contents are, obviously, taken from the field itself, but there is one subtlety here as well. If a field references an object that is not @Exportable, then the value is whatever is returned from toString(). The StringBuffer field on Line 36 of Listing 1 is an example. The field is output as:

 </notes>
    Notes go here 
</notes>

And the text

Notes go here

is the test returned by

notes.toString()

. Values of this sort cannot be reloaded back into the object by the "importer" side of the framework unless the field's class has a

String

constructor that can parse this content.

If a field references an object that

is

@Exportable

, then XML subelements are generated to represent the value, as is the case with the

streetAddress

field on Line 32 of Listing 1.

Exportable collections (and maps) are handled by exporting each element of the collection, but with the entire set of elements wrapped in an outer element that represents the collection as a whole. The <invoices> element, at the bottom of Listing 2 is an example. The associated field in the original object is declared as follows:

 @Persistent Collection<Invoice> invoices = new LinkedList<Invoice>();

Maps are handled similarly, but the elements that represent the values each have a name= attribute that identifies the key (the key object's toString() method is used for this purpose). For example:

 <field_name_of_Map_field>
    <ValueType  name="value_returned_by_key_toString()_method" className="...">
        value is displayed here
    </ValueType">
</field_name_of_Map_field>

Finally, note that this example is a persistence framework, after all, so all elements that represent objects of a known class are exported with the fully qualified class name specified in the className= attribute. The field name, if known, is also output as the name= attribute. There's an example in the output file on Listing 2's Line 6.

Declaring annotations

Before we get started looking at the implementation, I must say that the declaration syntax for annotations is not very good. Too much magic stuff is going on, and many syntax examples prove inappropriate for the task at hand, but we all have to live with it.

Annotations are declared like interfaces, but with some weirdness. Listing 3 is a simple example. The interface name is the annotation name, and you must precede the word interface with an @ sign. All annotation-style interfaces magically extend the Annotation interface.

Listing 3. Persistent.java: The @Persistent declaration

  1  package com.holub.persist;
   2  
   3  import java.lang.annotation.*;
   4  
   5  @Retention(RetentionPolicy.RUNTIME)
   6  @Target( ElementType.FIELD )
   7  public @interface Persistent
   8  {   String  value()  default "";    // Won't accept null;
   9  }
  10  

The two "meta-annotations" on Lines 5 and 6 control how the annotation can be used. The various retention policies (all defined in the

java.lang.annotation.RetentionPolicy

Javadoc page) are:

RetentionPolicy.CLASS Annotation is in the classfile, but cannot be accessed at runtime. Useful for postprocessors that work directly on the classfile
RetentionPolicy.RUNTIMEAnnotation is in the classfile and can be accessed via the introspection APIs
RetentionPolicy.SOURCE Annotation used by the apt preprocessor at compile-time

The

@Target

meta-annotation specifies to which Java elements you can attach annotations. Arguments defined in

java.lang.annotation.ElementType

are, for the most type, self explanatory:

ElementType.ANNOTATION_TYPE A meta-annotation applied to other annotations
ElementType.CONSTRUCTOR A constructor
ElementType.FIELD A field
ElementType.LOCAL_VARIABLE Local variable in a method
ElementType.METHOD A method
ElementType.PACKAGE A package
ElementType.PARAMETERAn argument to a method
ElementType.TYPEA class, interface, or enum declaration

You can combine as if you were initializing an array, as follows:

 @Target( {ElementType.FIELD, ElementType.METHOD} );

Two other meta-annotations are also available, though I haven't used them here:

@DocumentedAnnotation will be documented by javadoc by default
@Inherited Annotation used on a base class will be visible via introspection of derived classes that aren't themselves annotated

Returning to Listing 3, the method names define the annotation arguments. Here, however, the magic name

value()

is used. If a method by this name is present, then you can use the form

@annotation("value")

in your code (without the

arg=

part). The method's return type controls the argument type, which must be a primitive type or a

String

. Use the

default

clause to specify a default value for this argument. If the

default

clause is missing, the argument is required.

Now let's look at the @Exportable annotation, which demonstrates an annotation with more than one argument:

Listing 4. Exportable.java: The @Exportable declaration

  1  package com.holub.persist;
   2  
   3  import java.lang.annotation.ElementType;
   4  import java.lang.annotation.Retention;
   5  import java.lang.annotation.RetentionPolicy;
   6  import java.lang.annotation.Target;
   7  
   8  @Retention(RetentionPolicy.RUNTIME)
   9  @Target( ElementType.TYPE )
  10  public @interface Exportable
  11  {   String description()    default "";
  12      String name()           default "";
  13      String value()          default "";
  14  }
  15  

Note that I've supplied default values for everything and specified a magic value() as well, so there are several possible incarnations of this annotation:

 @Exportable
@Exportable("element_name")
@Exportable(name="element_name")
@Exportable(description="Comment goes here")
@Exportable(name="element_name" description="Comment goes here")

I've implemented the code that processes this annotation to treat the second and third of these forms identically, but that's an implementation detail you don't see in the declaration.

Implementing annotations at runtime

Moving on to the implementation, the XmlExporter class in Listing 5 does all the work:

Listing 5. XmlExporter.java: The XML exporter class

  1  package com.holub.persist;
   2  
   3  import java.lang.reflect.*;
   4  import java.io.*;
   5  import java.util.*;
   6  
   7  /** A very-simple persistent writer. Writes in XML format.
   8   *  Cyclic object graphs are handled by ignoring the
   9   *  cyclic field.  For example, consider a Company that has references
  10   *  to its Employees, which have references to the Company.
  11   *  Exporting the Company will export all the Employees, but
  12   *  none of these Employees will contain a <Company>...</Company>
  13   *  element. Exporting an Employee will cause the Company (and
  14   *  all of it's employees except the original one) to be exported.
  15   *  Note that this second situation is best avoided by not
  16   *  marking the Employee's "Company" field as @Persistent.
  17   */
  18  public class XmlExporter
  19  {
  20      private PrintWriter         out;
  21      private int                 indentLevel     = -1;
  22      private boolean             writeClassNames = false;
  23      private Collection<Object>  amVisiting      = new ArrayList<Object>();
  24  
  25      /** Create an XmlEporter.
  26       * 
  27       * @param out send the XML output to this writer.
  28       * @param writeClassNames if true, then synthesized element names
  29       *      will have a "className=" attribute.
  30       */
  31      public XmlExporter( PrintWriter out, boolean writeClassNames )
  32      {   this.out = out;
  33          this.writeClassNames = writeClassNames;
  34      }
  35      
  36      /** Convenenience constructor, class names are written.
  37       * @see #XmlExporter(PrintWriter,boolean)
  38       */
  39      public XmlExporter( PrintWriter out )
  40      {   this( out, true );
  41      }
  42  
  43      public void flush( Object obj ) throws IOException
  44      {   flush(obj, null, null );
  45      }
  46      /**
  47       * Flush out a single object that's represented as a class (not
  48       * a primitive). The fields of the object are flushed
  49       * recursively. The output takes the form:
  50       * <PRE>
  51       * <eleName className="..." name="..." >
  52       *      ...
  53       * </eleName>
  54       * 
  55       * The element name typically defaults to the class name. However,
  56       * if the object is annotated and has a name= attribute (or
  57       * an unnamed value), then that attribute specifies the
  58       * element name. The className attribute is generated if
  59       * this XmlExporter was created with constructor's 
  60       * writeClassNames argument set to true.
  61       * It holds the fully-qualified class name.
  62       * 
  63       * @param obj           The object to export. If the object is a Collection or
  64       *                      Map, then the elements are exported as if they had
  65       *                      been passed to flush(Object) one at a time.
  66       * 
  67       * @param preferedElementName   if not null, and the object is not annotated, then
  68       *                      use this string instead of the class name for the
  69       *                      element name.
  70       * @param objName       if not null, a name= attribute is
  71       *                      included in the generated element, and this
  72       *                      parameter determines the value. 
  73       * @throws IOException
  74       */
  75      public void flush( Object obj, String nameAttribute, String fallbackElementName ) throws IOException
  76      {   ++indentLevel;
  77      
  78          if( amVisiting.contains(obj) )  // Cyclic object graph.
  79              return;                     // Silently ignore. cycles.
  80          
  81          amVisiting.add(obj);
  82          
  83          if( obj instanceof Map )
  84          {   for( Iterator i = ((Map)obj).keySet().iterator(); i.hasNext(); )
  85              {   Object element = i.next();
  86                  flush( ((Map)obj).get(element), element.toString(), null );
  87              }
  88          }
  89          else if( obj instanceof Collection )
  90          {   for( Iterator i = ((Collection)obj).iterator(); i.hasNext(); )
  91                  flush( i.next() );
  92          }
  93          else
  94          {   Exportable annotation =
  95                      obj.getClass().getAnnotation( Exportable.class );
  96          
  97              if( fallbackElementName == null )
  98                  fallbackElementName = extractNameFromClass( obj );
  99              
 100              String elementName = fallbackElementName;
 101              
 102              if( annotation != null )
 103              {   
 104                  if( annotation.description().length() > 0 )
 105                      out.println("<!-- " + annotation.description() + " -->");
 106                  
 107                  elementName = annotation.value();
 108                  if( elementName.length() == 0 )     // If it's not specified in the value= attribute,
 109                      elementName = annotation.name();    //      check the name= attribute.
 110                  if( elementName.length() == 0 )     // It's not in the name= attribute either.
 111                      elementName = fallbackElementName;
 112              }
 113              
 114              out.println(
 115                      indent()
 116                      + "<"
 117                      + elementName 
 118                      + ( !writeClassNames ? " " :
 119                               (" className=\"" + obj.getClass().getName()+"\" ") )
 120                      + (nameAttribute == null ? " " :
 121                               (" name=\"" + nameAttribute +"\" ") )
 122                      + ">" );
 123              
 124              if( annotation != null )    // If the object is exportable,
 125                  flushFields( obj );     // then process its fields.
 126              else    
 127                  out.println( indent() + "\t" + obj.toString() );
 128              
 129              out.println( indent() + "</" + elementName + ">");
 130          }
 131          
 132          amVisiting.remove(obj);
 133          --indentLevel;
 134      }
 135      
 136      private void flushFields( Object obj ) throws IOException
 137      {   try
 138          {   Field[] fields = obj.getClass().getDeclaredFields();
 139              for( Field f : fields )
 140              {   
 141                  Persistent annotation = f.getAnnotation( Persistent.class );
 142                  if( annotation == null )
 143                      continue;
 144                  
 145                  f.setAccessible(true); // Make private fields accessible.
 146                  
 147                  String value = null;
 148                  Class  type = f.getType();
 149                  if(type == byte.class  ) value=Byte.toString    ( f.getByte(obj) );
 150                  if(type == short.class ) value=Short.toString   ( f.getShort(obj) );
 151                  if(type == char.class  ) value=Character.toString( f.getChar(obj) );
 152                  if(type == int.class   ) value=Integer.toString ( f.getInt(obj) );
 153                  if(type == long.class  ) value=Long.toString    ( f.getLong(obj) );
 154                  if(type == float.class ) value=Float.toString   ( f.getFloat(obj) );
 155                  if(type == double.class) value=Double.toString  ( f.getDouble(obj) );
 156                  if(type == String.class) value= (String)        ( f.get(obj) );
 157                  
 158                  // If an element name is specified in the annotation, use it.
 159                  // Otherwise, use the field name as the element name.
 160                  String name = annotation.value(); 
 161                  if( name.length() == 0 )
 162                      name = f.getName();
 163                  
 164                  if(value != null)   // Then it's a primitive type or a String.
 165                  {   out.println (indent() + "\t<" + name + ">");
 166                      out.println( indent() + "\t\t" + value );
 167                      out.println( indent() + "\t</" + name + ">");
 168                  }
 169                  else if(  f.get(obj) instanceof Collection 
 170                          || f.get(obj) instanceof Map )
 171                  {   out.println (indent() + "\t<" + name + ">");
 172                      flush( f.get(obj), f.getName(), null );
 173                      out.println( indent() + "\t</" + name + ">");
 174                  }
 175                  else
 176                  {   
 177                      // The following if statement will kick out a type-safety warning
 178                      // from the compiler. Since we don't actually know the type (so
 179                      // can't cast it appropriately), there's no way to eliminate
 180                      // this warning.
 181                  
 182                      if( type.getAnnotation(Exportable.class) != null )
 183                          flush( f.get(obj), f.getName(), null );
 184                      else
 185                          flush( f.get(obj), null, name );
 186                  }
 187              }
 188          }
 189          catch( IllegalAccessException e )   // Shouldn't happen
 190          {   assert false : "Unexpected exception:\n" + e ;
 191          }
 192      }
 193      
 194      /** Get the class name from the prefix. If the fully-qualified name
 195       *  contains a $, assume it's an inner class and the class name is
 196       *  everything to the right of the rightmost $. Otherwise, if the
 197       *  fully qualified name has a dot, then the class name is everything
 198       *  to the right of the rightmost dot. Otherwise, the name is the
 199       *  string returned from getClass().getName().
 200       *  
 201       * @param obj
 202       * @return
 203       */
 204      private String extractNameFromClass( Object obj )
 205      {   String name = obj.getClass().getName();
 206          int index;
 207          if( (index = name.lastIndexOf('$')) != -1 )
 208              return name.substring( index + 1 );
 209          
 210          if( (index = name.lastIndexOf('.')) != -1 )
 211              return name.substring( index + 1 );
 212          
 213          return name;
 214      }
 215      private static final String indents[] = new String[]
 216         {
 217              /* 00 */    "",
 218              /* 01 */    "\t",
 219              /* 02 */    "\t\t",
 220              /* 03 */    "\t\t\t",
 221              /* 04 */    "\t\t\t\t",
 222              /* 05 */    "\t\t\t\t\t",
 223              /* 06 */    "\t\t\t\t\t\t",
 224              /* 07 */    "\t\t\t\t\t\t\t",
 225              /* 08 */    "\t\t\t\t\t\t\t\t",
 226              /* 09 */    "\t\t\t\t\t\t\t\t\t",
 227              /* 10 */    "\t\t\t\t\t\t\t\t\t\t",
 228              /* 11 */    "\t\t\t\t\t\t\t\t\t\t\t",
 229              /* 12 */    "\t\t\t\t\t\t\t\t\t\t\t\t",
 230              /* 13 */    "\t\t\t\t\t\t\t\t\t\t\t\t\t",
 231              /* 14 */    "\t\t\t\t\t\t\t\t\t\t\t\t\t\t",
 232              /* 15 */    "\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t",
 233         };
 234                                                         
 235      private final String indent()
 236      {   return indents[ indentLevel %16 ];
 237      }
 238  }
 239  

The first interesting method is flush() (on Line 75). This method is the top-level method called to flush an entire object to the output stream in XML format. The flush() method is also called recursively to flush any fields that aren't primitive types or Strings. The method's first few lines set an indent level (so the XML will look pretty). Cycles in the object graph are handled by checking if the current object is in the amVisiting Collection. If it is, then this instance of flush() is called recursively, and some previous call is in the process of visiting the object passed to the current call. The method just returns in this situation.

The next bit of code (starting on Line 83) handles the case of the current object being a Map or Collection. The individual elements of the aggregate are each processed in turn, as if they had been passed to flush() one at a time. Map and Collections are handled differently only in that a name=... attribute that holds the key's value is passed to the recursive flush() call.

Finally, we get to the code that handles the annotation. You get an annotation from the class object by asking for it (on Line 94):

 Exportable annotation = obj.getClass().getAnnotation( Exportable.class );

Note that I've asked for a particular annotation by specifying its class to getAnnotation(), and the return type is automatically cast into the correct interface type using the magic of generics. In the Javadoc, this method's signature is hideously confusing if you're not used to the generics syntax:

 public <A extends Annotation> A getAnnotation(Class<A> annotationClass)

This declaration says that Class A (the return type) must extend Annotation (which all annotations do by magic) and that same type is the generic type of the argument to the method. In practice, all this means is that you don't have to cast the return value, but...geeez!...I hate this syntax.

You use the object returned from getAnnotation() as if it were a reference to an interface. For example, given the following annotated source code:

 @Exportable(name="Fred")
//...

I can get the string value associated with the name= argument in the following code:

 Exportable annotation = obj.getClass().getAnnotation( Exportable.class );
//...
annotation.name(); // Returns "Fred"

Also note how the code that starts on Line 107 of Listing 5 explicitly makes the value() argument supply a default value for the name= argument. The code checks for a default value first, then uses the name if the value isn't there. This defaulting-to-the-unnamed-argument process must be done manually in your code. There's no way to specify the behavior in the annotation's @interface declaration.

The

flushFields()

method (on Line 136 of Listing 5) works much like the

flush()

method, at least with respect to annotation processing. I get the fields using

Class

's

getDeclaredFields()

method. I tell the system to ignore the access privilege with the

setAccessible(true)

call, so I can process the

private

fields. The

flushFields()

method then outputs elements representing primitive-type or

String

arguments. It calls

flush()

recursively to print objects of any class other than

String

, including

Collection

or

Map

fields.

Conclusion

This article presents a pretty simple example of annotation, but it effectively demonstrates how to use an annotation at runtime to eliminate the getter/setter-based tagging idiom. All of the introspection-related classes (like Class, Method, and Field) have a getAnnotation() method that you can use to access the annotations. As I said at the beginning of this article, this is just one part of the puzzle, because annotations are often best used at the compiler, not the runtime level. I'll talk about how to do that in a future article.

Allen Holub has worked in the computer industry since 1979 on everything from operating systems, to compilers, to Web applications. He is a consultant, providing mentoring and training in object-oriented design and Java development, technical due diligence, design reviews, and he writes programs on occasion. He served as a chief technology officer at NetReliance and sits on the board of advisors for Ascenium and Ontometrics. Holub has authored nine books (including Holub on Patterns: Learning Design Patterns by Looking at Code, Taming Java Threads, and Compiler Design in C) and more than 100 magazine articles. He has been a contributing editor for JavaWorld since 1998 and for SD Times since 2004.

Learn more about this topic

Join the discussion
Be the first to comment on this article. Our Commenting Policies