Captain Kirk To The Rescue

Using Java Annotations For Object Lifecycle Management

This article discusses a simple approach to lifecycle management of objects utilizing a novel feature of Java called annotations. This concept can be easily enhanced to support business object state management.

Annotations

Lifecycle Management

Lifecycle management is mostly done in middleware environments like application servers. The idea is to separate the logic of construction, usage and destruction of an object 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 it might be necessary to have some variation but in a manageable environment the basic algorithm will follow a sequential chain of operations.

Sample Problem

In Java applications, masks are used for data collection and manipulation. This is often referred to as the CRUD cycle (Create, Read, Update, Delete). The user is presented some data which can be altered, deleted or newly entered. As a simple business problem we want to manage how masks are displayed in a client application. We will separate the display into a chain of operations like this:
  1. Construction - The mask is layouted in this phase.
  2. Initialization - In this phase the mask is filled with data
  3. Activation - Here the user is given control over the mask.

Phases

In this discussion we'll refer to each such operational step as a Phase and the basic idea is very simple. We'll mark the methods of a class as phases of a chain of operations and leave the invokation 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 invokation control mechanisms that are needed in a business process. The annotation that we will use is simply called Phase.
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface Phase {
   int index();
   String displayName() default "";
}
Let's go through the code above. In the first two lines you'll see the annotation being marked with two other annotations =itself=. Those two lines specify the annotation Phase such that it applies to methods only and that it should be retained after compilation. Other annotations may be used only during compilations and they may target classes or members.

The @interface is the syntactic description of an annotation. What's new in terms of syntax are the lines that follow. Those are not only declaring a member but at the same time a method. They are "index" and displayName". The latter is given a default value of an empty string, in case it is not provided and the first is required to be provided.

The index is going to tell the framework in which order the phases are to be executed by default and the display name can be used for monitoring purposes.

Since I said earlier that we should seperate this logic from the object, we'll define an interface that needs to be implemented in order to take part in the lifecyle management. This interface then can be implemented by a client object. For mangement purposes we'll define a common marker interface, from which all other 'phaseable' interfaces will be derived. This is done so that the framework has a unified access point to classes that are managed.

public interface Phased {
}
A concrete implementation might look like this. Here the interface defines how a mask, or form, that contains several controls needs to be properly setup 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 see how an annotation is used. It is written right before the declaration of the interface method and is has a leading @ sign. Its property "index" is provided within parentheses.

You'll notice that the methods in PhasedMask take no arguments and provide no return value. This is unfortunate but impossible to avoid because it would create a huge overhead in the framework to manage the infinite possibilities of providing arguments and evaluating return values. This is not an optimal situation, since it forces the client object to be initialized with all necessary arguments before the phases are invoked.

Phaser

The main management class is properly called Phaser. It's objective is to execute all phases and provide a simple monitoring mechanism to the user. I'll omit the implementation of this management class. You can look up the code though. [2]

A Phaser is provided with an object that implements some concrete PhasedXxxx interface and manages the invokation 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 invokation 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 this order.

How about controlling the phases? Let's omit the construction phase because when we call the phasedMask a second time the layout is not necessary anymore. This means essentially that we do not 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 be executed.

Phaser phaser = new Phaser( phasedMask );
phaser.invoke( new int[] {1,2} );
This is ok, but not very explicit. Well, who can remember what phases the indicies 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 it is possible to reorder the phases if this is required. So we could write it like this to switch the sequence.
Phaser phaser = new Phaser( phasedMask );
phaser.invoke( new String[] {"activate","initialize"} );
This doesn't make sense here, but in a setting, where we have much more phases that are not as tightly dependent on each other, it is useful to be able to do this.

Since we're using reflection here to call those methods there are possibly a lot of exceptions being thrown. The Phaser will catch the ones it expects and wrap them in so called PhaserExceptions. So, if a method call fails (say, because it is private) the invoke method of the Phaser will throw a PhaseException that contains the originating exception.

Observation

You may add a PhaseListener to a Phaser in order to observe what's happening inside and to provide the user with feedback during lengthy phase invokations.
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

You have seen a possible solution to utilize annotations to manage the lifecycle of an arbitrary class that is split into separate phases. In order to do this, classes simply need to implement an interface derived from the parent interface called Phased, where the methods are annotated with the annotation Phase. The management is done by 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 to observe the workings of the Phaser.

This approach also shows how annotations can be used to be more than just JavaDoc enhancements. Not only can it be used for lifecycle management but for object initialization purposes in general.

The implementing classes do not concern themselves with the sequence in which their methods are invoked. If this is kept in mind during design, it is possible to be much more flexible using classes.

If the phases need to be rearranged or if phases are to be omitted then this can be done outside the implementing classes.

There are drawbacks however. If the interface needs to be changed either new interfaces must be defined to maintain backward compatibility or, if the source code, is entirely available, the implementing classes need to be altered.

This solution does not provide parameter support or return value support. In a more specific business context this could be problematic. Also, for performance hungry systems there might arise a bottleneck issue, since reflection is heavily used.


Norbert Ehreke, Impetus Unternehmensberatung GmbH, http://www.impetus.biz

  1. "Java 1.5 Tiger - A Developers Notebok"; McLaughlin, Flanagan; O'Reilly 2004
  2. Source Code; Impetus Unternehmensberatung GmbH; http://www.impetus.biz