@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface WhenReady {
String[] value() default {""}; // the ready group this object is part of
}
The Ready Processing components allow you to perform application startup operations in a controlled manner. When certain defined conditions are met, your code is notified, and therefore able to perform any desired operations.
There are two ways this startup processing works.
In the simplest case, you can monitor when beans have been autowired, either after a particular phase, or when all autowiring is complete. This is handled through the CtxEventListener interface. Bean autowiring is handled by the KOS BeanContext.
In the more complex case, you can define when individual objects become ready (that is, fully functional), and objects can be notified when dependent objects are ready.
What is "Ready"
An object is "ready" when it is fully functional. This is an important concept because some beans, like those that interact with external systems or devices, are not fully operational until these external components are available. For example, we don’t want the UI to start pouring a drink before a connection to the dispenser is made. What that in mind, ready processing gives us the opportunity to:
|
In this section, we’ll learn about the four Ready Processing API components.
Annotations:
@WhenReady
Interfaces:
Ready
ReadyListener
Classes:
ReadyIndicator
This section describes the @WhenReady annotation, which is used to monitor when a field becomes ready. It takes one optional parameter.
@WhenReady
annotation@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface WhenReady {
String[] value() default {""}; // the ready group this object is part of
}
The class that uses this annotation must also implement ReadyListener in order to get the onReady() callback when all @WhenReady objects are ready.
This is only meaningful if the referenced object implements Ready, so that it has an indicator to attach to.
Here’s an example:
public class MyClass1 {
// (1)
@Autowired
@WhenReady
private MyClass2 myClass2;
// (2)
@Autowired
@WhenReady("Group-A")
private MyClass3 myClass3;
// (3)
@Autowired
@WhenReady({"Group-A", "Group-B"})
private MyClass4 myClass4;
// . . .
}
As you can see in the above figure, all of the autowired fields are also annotated with @WhenReady.
Looking at the code:
(1) No "group" parameter is given. This indicates TBD.
(2) The group parameter is "Group-A". TBD
(3) Two groups are defined.
For this code to actually do something, this class must implement the ReadyListener interface, which we see in the next section.
For the @WhenReady annotation to be of any value, the class must also implement the ReadyListener interface. This interface defines two callbacks that are made when a ready condition is met.
ReadyListener
interfacepublic interface ReadyListener {
void onReady();
default void onGroupReady(String group) {
}
}
The onReady()
method (required) is called when all @WhenReady annotations in the object become ready. This will never be called more than once, and will never be called if any dependent object never becomes ready. This can be used in conjunction with onGroupReady()
to know when all groups have been become ready.
The onGroupReady()
method (optional) is called when all fields annotated with @WhenReady and a particular group become ready. The value of @WhenReady is a group name (default empty string) which allows for multiple callbacks to occur, once for each time all objects in a named group are ready. The order of callbacks is non-deterministic, and will occur as the dependent objects become ready and complete the groups. This can be overridden in order to perform a phased startup based on the availability of various combinations of beans. Multiple groups can be enumerated in each @WhenReady annotation.
Let’s add to the previous example to see the ReadyListener working in conjunction with @WhenReady:
@WhenReady
and ReadyListener
public class MyClass1 implements ReadyListener {
@Autowired
@WhenReady
private MyClass2 myClass2;
@Autowired
@WhenReady("Group-A")
private MyClass3 myClass3;
@Override
public void onGroupReady(String group) {
switch (group) {
case "Group-A":
// We know myClass3 is now ready, so perform related work.
break;
default:
// Group we're not interested in:
break;
}
}
@Override
public void onReady() {
// We now know that both myClass2 and myClass3 are ready.
}
}
In the figure above, you see that both methods of the ReadyListener interface are implemented.
The onGroupReady()
method is called every time a group of beans is ready.
The onReady()
method is called one time, after all beans in the context are ready.
Continuing on with our example, how do MyClass2 and MyClass3 indicate if they’re ready or not?
This is accomplished by implementing the Ready interface. When a class implements the Ready interface, it is telling the rest of the system that there’s some work it must perform before it can be relied upon by other objects. Other objects can then query this object’s isReady()
method to determine if it is indeed ready or not.
The Ready interface defines one required and two optional methods. The optional methods are almost never overridden.
Ready
interfacepublic interface Ready {
// Returns the readyIndicator object:
ReadyIndicator getReady();
// Returns true if the implementing object is ready:
default boolean isReady() {
return getReady().isReady();
}
// Indicates that the implementing object is ready:
default void setReady() {
getReady().setReady();
}
}
The following is a partial example of a class implementing the Ready interface. The next section will finish this by bringing the ReadyIndicator into play.
Ready
interfacepublic class MyClass2 implements Ready {
@Override
public ReadyIndicator getReady() {
// Return readyIndicator (not defined yet)
}
}
The ReadyIndicator class is used in conjunction with the Ready interface.
ReadyIndicator
classpublic class ReadyIndicator {
// Methods:
public void setReady();
public boolean isReady();
}
Building on the partial example given in the figure above, we now have a more complete example:
Ready
interfacepublic class MyClass2 implements Ready {
private final ReadyIndicator readyIndicator = new ReadyIndicator();
@Override
public ReadyIndicator getReady() {
return readyIndicator;
}
}
This section gives a more complete example, which uses all API components associated with this topic.
This class depends on three objects that get inject (autowired): myClass2, myClass3, and myClass4.
When all objects (throughout the entire application) whose @WhenReady group is "Apple" have been injected, then onGroupReady()
is called with the group parameter equal to "Apple". The myClass3 object then performs some work that relies on all objects in the apple group being ready.
Likewise, when all objects whose @WhenReady group is "Banana" have been injected, then onGroupReady()
is called with the group parameter equal to "Banana". The myClass4 object then performs some work that relies on all objects in the banana group being ready.
The same applies when the @WhenReady group is "Cherry".
Finally, the onReady()
method is called when all objects in the entire application has been instantiated, injected, and have indicated that they are ready.
MyClass1
classpublic class MyClass1 implements ReadyListener {
@Autowired
@WhenReady
private MyClass2 myClass2;
@Autowired
@WhenReady("Apple")
private MyClass3 myClass3;
@Autowired
@WhenReady({"Banana", "Cherry"})
private MyClass4 myClass4;
//--- Implement the ReadyListner ---
@Override
public void onGroupReady(String group) {
switch (group) {
case "Apple":
myClass3.doSomeAppleRelatedWork();
break;
case "Banana":
myClass4.doSomeBananaRelatedWork();
break;
case "Cherry":
myClass4.doSomeCherryRelatedWork();
break;
default:
// Group we're not interested in:
break;
}
}
@Override
public void onReady() {
// We now know that myClass2, myClass3, and myClass4 are all ready.
myClass2.doSomeWork();
myClass3.doMoreWork();
myClass4.doMoreWork();
}
}
This object is ready when its autowiring is complete. Therefore, the onCtxAutowiringCompleted()
method sets the "is ready" indicator. This also sets the myClass3 "is ready" indicator, demonstrating that one object can indicate that another object is ready.
MyClass2
classpublic class MyClass2 implements CtxEventListner, Ready {
private final ReadyIndicator readyIndicator = new ReadyIndicator();
@Autowired
private MyClass3 myClass3;
//--- Implement the CtxEventListner ---
@Override
public void onCtxAutowiringCompleted() {
readyIndicator.setReady(); // this object is ready when autowiring is finished
myClass3.setReady(); // when this class is ready, so is myClass3
}
//--- Implement the ReadyListner ---
@Override
public ReadyIndicator getReady() {
return readyIndicator;
}
}
The MyClass3 object is told by the MyClass2 class when it’s ready, therefore it doesn’t need to do much.
MyClass3
classpublic class MyClass3 implements Ready {
private final ReadyIndicator readyIndicator = new ReadyIndicator();
//--- Implement the ReadyListner ---
@Override
public ReadyIndicator getReady() {
return readyIndicator;
}
}
The MyClass4 class starts a thread (code not given) in its constructor. This hypothetical thread performs all of the startup that it needs, after which it marks the MyClass4 object ready.
MyClass4
classpublic class MyClass4 implements Ready {
private final ReadyIndicator readyIndicator = new ReadyIndicator();
public class MyClass4() {
// Start a thread that runs which will eventually make the indicator "ready".
}
//--- Implement the ReadyListner ---
@Override
public ReadyIndicator getReady() {
return readyIndicator;
}
}
This concludes the section on the KOS bean context. You should now have a good idea how to create some beans, wire them together, and perform actions when they are available and ready. If it’s still a bit fuzzy, we’d encourage to re-read these five pages, as seeing the information again after gaining some core understanding will help with the learning process.
If you’re ready to move ahead, take a look at the TBD section.