@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.
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.
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"} );
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.