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>();

1 2 3 Page
Join the discussion
Be the first to comment on this article. Our Commenting Policies
See more