Annotations to the rescue

Using Java annotations for object lifecycle management

Complex applications usually have many initialization problems that a developer must solve. A number of different steps prove necessary to set up a panel, to configure a service, etc. To make things even more difficult, some of those steps must be repeated, others do not. To put this kind of management into the class itself is troublesome because the logic might change. In addition, modern software design urges the separation of responsibilities. In short, the goal is to separate the logic of what is done from how it is done.

This article shows you how to use annotations beyond simple markings for initialization control. It introduces a small API that you can use to develop your own "phaseable" annotations and gives you some inspiration concerning this new feature's possibilities.

Annotations

An annotation is a new language feature introduced in J2SE 5.0. Simply put, annotations allow developers to mark classes, methods, and members with secondary information that is not part of the operating code. This allows, for instance, a rating like "good method," "bad method," or, more adequately, "deprecated method" or "overridden method." The possibilities are endless. Note that it is irrelevant what the method or class actually does when it is marked as "deprecated," for example. For a more detailed discussion of annotations, see Java 5.0 Tiger: A Developer's Notebook.

Since annotations can be used to describe the usage and meaning of entities like methods and classes, they are, loosely put, a sort of semantic sugar. In turn, this additional information can be evaluated by others (e.g., frameworks) and used for all kinds of actions, like generating documentation (Javadoc) or, as discussed here, for controlling behavior like the lifecycle of objects in specific contexts.

Lifecycle management

Lifecycle management usually occurs in middleware environments like application servers. The idea is to separate the logic of on object's construction, usage, and destruction from the object itself. In an application server, for example, where different services are published, it doesn't usually matter what particular service is requested; the mechanism of invoking the service within the application server follows a more or less identical scheme. Depending on the state of the application, the caller, and other parameters, some variation might prove necessary, but in a manageable environment, the basic algorithm will follow a sequential chain of operations. In Java client applications, for instance, it is necessary to manage the display of masks or forms that allow the user to enter or alter application data.

Sample problem

In Java applications, masks are used for data collection and manipulation—data handling often referred to as the CRUD (create, read, update, delete) cycle. The user is presented some data that can be altered, deleted, or newly entered. As a simple business problem, we want to manage how masks display in a client application. Thus, we separate the display into a chain of operations like this:

  1. Construction: The mask is laid out in this phase, preferably only once
  2. Initialization: In this phase, data is retrieved from a file, database, etc., and the mask's fields are filled with the data
  3. Activation: Here, the user is given control over the mask

In the real world, many more aspects are usually considered: access, validation, control dependencies, etc.

Phases

In this discussion, I refer to each operational step as a phase, and the basic idea is simple: We mark class methods as phases of a chain of operations and leave invocation of those methods to a service (framework) class. Actually, this approach is not limited to lifecycle management. It can be used for all kinds of invocation control mechanisms needed in a business process.

The annotation we use is simply called Phase, and we use it to denote a method as a part of the chain of operations that can be invoked. In the code below, you'll see that the annotation declaration is similar to that of an interface:

 @Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface Phase {
   int index();
   String displayName() default "";
}

Let's walk through the code above. In the first two lines, you see the annotation marked with two other annotations. This looks a little confusing at first glance, but those two lines simply specify that annotation Phase applies to methods only and should be retained after compilation. These annotations are added because other annotations may be used only during compilation and may target classes or members.

The @interface is the syntactic description of an annotation. The code that follows, specifically, index and displayName—which not only declares a member, but also, at the same time, a method—is also new in terms of Java syntax. displayName is given an empty string as a default value, in case it is not provided, and can be used for monitoring purposes, say a progress bar. index() is required; it tells the framework in which order the phases will execute by default.

As I said earlier, we should separate this logic from the object, so we define an interface that must be implemented to take part in the invocation management. This interface then can be implemented by a client object. For management purposes, we define a common marker interface from which all other "phaseable" interfaces will be derived so the framework has a unified access point to classes that are managed:

 public interface Phased {
}

A concrete implementation of this interface might look like the code below. Here, the interface defines how a mask, or form, that contains several controls must be properly set up as described above:

 

public interface PhasedMask extends Phased {

@Phase(index=0) public void construct();

@Phase(index=1) public void initialize();

@Phase(index=2,displayName="Activating...") public void activate();

}

You can see how an annotation is used. It is written right before the declaration of the interface method and has an introductory @ sign. Its property index is provided within parentheses. Note that, because an annotation is not a Java statement, no semicolon appears at the end. Now we need a class that brings everything together and evaluates the phases we have defined.

Phaser

The main management class is properly called Phaser. (Hey, don't we all love Star Trek?) It executes all phases and provides a simple monitoring mechanism to the user. The implementation of this class is not included with this article, however, you can look up the code after downloading the framework from Resources.

A Phaser is provided with an object that implements some concrete PhasedXxxx interface and manages the invocation of the phases. Suppose we have a class MyMask like this:

 

public class MyMask implements PhasedMask {

@Phase(index = 0) public void construct() { // Do the layout }

@Phase(index = 1) public void initialize() { // Fill the mask with data }

@Phase(index = 2) public void activate() { // Activate the listeners and allow the user to interact with the mask } // Business code

}

Now we can control the invocation of those PhasedMask methods like this:

 Phaser phaser = new Phaser( phasedMask );
phaser.invokeAll();

This results in the methods construct(), initialize(), and activate() being invoked in that order.

How about controlling the phases? Let's omit the construction phase because, when we call the phasedMask() a second time, the layout no longer proves necessary. This essentially means we don't want the method construct() being called anymore. Since we've marked this method with the index 0, we can simply omit this index and tell the Phaser specifically what phases should execute:

 Phaser phaser = new Phaser( phasedMask );
phaser.invoke( new int[] {1,2} );

This is okay, but not explicit. Who can remember what phases the indices actually stand for? Fortunately, we can be a little more verbose like so:

 Phaser phaser = new Phaser( phasedMask );
phaser.invoke( new String[] {"initialize","activate"} );

Here, we use the method names from the interface. Also, note that we can reorder the phases if need be. So, to switch the sequence, we could write:

 Phaser phaser = new Phaser( phasedMask );
phaser.invoke( new String[] {"activate","initialize"} );

This hardly makes sense here, but, in a setting where we have more phases that are less tightly dependent on each other, that approach proves useful.

Since we're using reflection here to call those methods, potentially, many exceptions could be thrown. The Phaser will catch the ones it expects and wrap them in the so-called PhaserException. So, if a method call fails (say, because it is private) the Phaser's invoke() method will throw a PhaseException that contains the originating exception. For those unfamiliar with reflection. see the sidebar "Notes on Reflection."

You may also add a PhaseListener to a Phaser to observe what's happening inside and to provide the user with feedback during lengthy phase invocations:

 

PhaseListener listener = new PhaseListener() { public void before( PhaseEvent e ) { // This is called before the Phaser invokes a phase } public void after( PhaseEvent e ) { // This is called after the Phaser has successfully invoked a phase } };

Phaser phaser = new Phaser( phasedMask ); phaser.addPhaseListener( listener ); phaser.invoke( new String[] {"initialize","activate"} );

Discussion and summary

In this article, you have seen how to utilize annotations to manage the lifecycle of an arbitrary class split into separate phases. In order for classes to be ready for management by an external framework component, they must simply implement an interface derived from the parent interface called Phased, in which the methods are annotated with the annotation Phase. Management is completed with a Phaser class that controls the sequence and invokes the annotated methods of the implemented interface. It is possible to control the sequence in which the operations are invoked, and an event-handling mechanism provides the means for observing Phaser's workings.

This approach also shows how annotations can be used for more than just Javadoc enhancements. They can be used not only for lifecycle management, but also for object initialization purposes in general.

The implementing classes do not concern themselves with the sequence in which their methods are invoked. If you keep this is mind during design, you can be much more flexible with the use of classes.

If the phases must be rearranged or omitted, these actions can occur outside the implementing classes.

As with any tool, there are drawbacks. If the interface must be changed, either new interfaces must be defined to maintain backward compatibility or, if the source code is entirely available, the implementing classes must be altered. This solution lacks parameter support and return-value support. Parameters must be fully provided before the phases are invoked. Also, for performance-hungry systems, a bottleneck might occur since reflection is heavily used.

Finally, the invocation chain is not transparently available to IDEs. In other words, for [put your favorite Java IDE here], it is impossible to show the developer at compile-time what method is going to be called from where.

Norbert Ehreke is a senior development lead at Impetus Unternehmensberatung GmbH, a consulting company in Frankfurt, Germany. He is responsible for its framework development and has been involved in several Perl-, Java-, and C#-oriented projects. He studied at the Technical University (TU) in Berlin, Germany, the University of Maryland in College Park, Maryland, and at the Eidgenoessische Technische Hochschule (ETH) in Zurich, Switzerland. He earned a master's degree in systems engineering from TU Berlin. His research interests include object-oriented programming (Java, Perl, C#), aspect-oriented programming (AOP), and most recently language-oriented programming (LOP). In his spare time he likes mountain biking, cooking, and the movies.

Learn more about this topic

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