In this section, we will look at two examples of possible applications of the timer service. We will look at enough of the source code in this chapter to discuss the fundamental concepts, but you will get the most benefit from this chapter by having the full source listings available. In the first example, we will use the timer service as a scheduler to start the sample application via a timer notification that is emitted once. The second example shows how to use the timer service to handle repeated timer notifications to write log messages to disk.
As we’ve seen, the minimal steps to use the timer service are:
Create the timer service by instantiating the
Timer
class.
Add a notification, which includes at a minimum the notification type (which is up to you to define) and the date the notification is to start.
Add a notification listener to the timer service.
Start the timer.
Step 2 is repeated for each notification type that is to be sent by the timer service. Step 3 is repeated for as many notification listeners as are needed in the system. In the sample application code for this chapter, there are two notification types:
sample.timer.startController
The notification type string that indicates the controller is to be started
sample.timer.flushlog
The notification type string that indicates that any queued messages are to be written (i.e., committed) to the log
There are also two notification listeners:
sample.timer.Scheduler
The class that is responsible for creating the timer service and adding the above notifications
sample.utility.MessageLogQueue
The class that is used to queue log messages, listen for
sample.timer.flushlog
notifications, and write
them to the disk-based log file
The Scheduler
class acts as the JMX agent for our
example. Example 10-2 shows a partial source listing
for this class.
Example 10-2. Partial source listing of the Scheduler class
package sample.timer; import sample.utility.*; // . . . public class Scheduler { public static void main(String[] args) { try { Properties props = new Properties( ); FileInputStream propFile = new FileInputStream("scheduler.properties"); props.load(propFile); String controllerStartWaitTime = (String)props.get("controller.startWaitTime"); long startTime = System.currentTimeMillis( ) + (new Long(controllerStartWaitTime)).longValue( ); Date startDate = new Date(startTime); Timer timer = new Timer( ); // Add notification that starts the controller. . . timer.addNotification("sample.timer.controllerStart", null, props, startDate); Listener listener = new Listener( ); timer.addNotificationListener(listener, null, null); // add notification that flushes the log. . . String logFlushWaitTime = (String)props.get("logger.flushWaitTime"); timer.addNotification("sample.timer.flushlog", "Time to flush the log to disk.", null, new Date( ), (new Long(logFlushWaitTime)).longValue( )); timer.addNotificationListener(MessageLogQueue.instance( ), null, null); timer.start( ); ObjectName objName = new ObjectName("Timer:type=generic"); MBeanServer mbs = MBeanServerFactory.createMBeanServer( ); mbs.registerMBean(timer, objName); } catch (Exception e) { } } }
The main( ) method of
Scheduler
contains the agent code for our example
and reads the following properties from the file
scheduler.properties
:
The number of milliseconds to delay before starting the controller,
used to calculate the start date for sending the
sample.timer.startController
notification
The number of milliseconds to wait before sending the
sample.timer.flushlog
notifications, which results
in the listener writing any queued log messages to disk
The number of prime numbers calculated by the consumer thread for each item it removes from the queue
The number of prime numbers calculated by the supplier thread for each item it adds to the queue
Example 10-3 shows the contents of this file.
Example 10-3. The scheduler.properties file
controller.startWaitTime=60000 logger.flushWaitTime=10000 controller.supplier.workFactor=150 controller.consumer.workFactor=100
Using the property values in this file, the controller will start in approximately one minute and the queued log messages will be written to disk every 10 seconds.
Once the start wait time for the controller is read from the
properties file, that wait time is added to the current system time
to calculate the date (in milliseconds) when the notification to
start the controller will be sent. Next, the timer service is created
and the controller start notification is added. Notice that the
Properties
object we created earlier is passed as
the userData parameter to the
addNotification( ) call. This allows the
listener access to the properties from
scheduler.properties
. Next, we create an
instance of the Listener
class, which will receive
the notification to start the controller. We tell the timer service
to send this notification to Listener
by calling
the timer’s addNotificationListener(
) method.
The second notification to be added is the one to flush any queued
log messages to the log file on disk. Notice that we do not use a
userData object in this case, and we indicate
this by passing null
on the
addNotification( ) call. We indicate to the
timer service that the notifications are to begin immediately by
creating a new Date
object, using
Date
’s default constructor.
Finally, we indicate to the timer service that this notification is
to be sent repeatedly, at an interval indicated by the value of the
logger.flushWaitTime
property. The
MessageLogQueue
class acts as a queue of log
messages, which are not committed to the log file until it receives a
notification to do so. It is implemented as a singleton[3] so that it can
be shared by all other objects in the system. It is the
MessageLogQueue
that will act as the listener for
this notification, and we obtain a reference to the singleton by
calling its instance( ) method.
Once the notifications and listeners have been added, the timer service is started by calling its start( ) method. The timer service is an MBean, so we create an object name for it and register it with the MBean server. This allows the timer service to be monitored and controlled.
When the
controller start notification
is sent, the Listener
class handles the
notification:
package sample.timer; import javax.management.*; public class Listener implements NotificationListener { public void handleNotification(Notification notification, Object obj) { String type = notification.getType( ); if (notification instanceof TimerNotification) { TimerNotification notif = (TimerNotification)notification; if (type.equals("sample.timer.controllerStart")) { Properties props = (Properties)notif.getUserData( ); final String cwf = (String)props.getProperty("controller.consumer.workFactor"); final String swf = (String)props.getProperty("controller.supplier.workFactor"); Thread t = new Thread(new Runnable( ) { public void run( ) { Controller.main(new String[] {cwf, swf}); } } ); t.start( ); } } } }
The code for the Listener
class is relatively
simple, as it has to implement only the handleNotification(
) method of NotificationListener
.
Recall that when the timer service sends a notification, an instance
of the TimerNotification
class is emitted to all
interested listeners. This is the only type of notification in which
Listener
is interested. If the notification is not
of the right type (sample.timer.controllerStart
),
it is ignored. If the notification type string is of the correct
type, the getUserData( ) method of the
Notification
class
(TimerNotification
’s parent
class) is invoked to retrieve the Properties
object we stored there when the controller start notification was
added to the timer service. The Controller
is
started on a separate thread of execution, because
Controller.main( ) does not terminate until all
other threads in Controller
(e.g., consumer and
supplier threads) are finished. If we don’t start
the Controller
on its own thread from
handleNotification( ), it may hang the timer
service’s notification thread if we run the code on
single-processor systems, and other notifications may not be sent by
the timer service until Controller.main( ) is
finished executing!
When the notification
to flush queued messages to
disk is sent by the timer service, the
MessageLogQueue
class handles it:
package sample.utility; import javax.management.*; public class MessageLogQueue extends MessageLog implements NotificationListener { // singleton stuff. . . private static MessageLogQueue _instance = null; public static MessageLogQueue instance( ) { if (_instance == null) _instance = new MessageLogQueue( ); return _instance; } private ArrayList _store = new ArrayList(10); public synchronized void write(String message) { _store.add(message); } public synchronized void write(Throwable t) { _store.add(t); } public synchronized void handleNotification(Notification notification, Object obj) { if (notification instanceof TimerNotification) { String type = notification.getType( ); if (type.equals("sample.timer.flushlog")) { if (_store.size( ) > 0) { Iterator iter = _store.iterator( ); while (iter.hasNext( )) { Object message = iter.next( ); if (message instanceof String) super.write((String)message); else if (message instanceof Throwable) super.write((Throwable)message); } _store.clear( ); } } } } }
The emphasized lines in this code point out some of the features of
the MessageLogQueue
class. First, the class is
implemented using the singleton pattern, so that there is only one
instance of the class for the process (i.e., the JVM)—this
allows all classes in the JVM to write their log messages to the log
file in an asynchronous fashion. The backing store for the
“queue” inside
MessageLogQueue
is an
ArrayList
, and access to the contents of the list
must be synchronized.
As we saw earlier, if the Notification
object that
is sent is not a TimerNotification
, it is simply
ignored. This goes for the notification type as well, which must be
sample.timer.flushlog
. Any messages in the backing
store for the queue are written out one at a time using an
Iterator
. Because both String
and Throwable
message types are allowed, the Java
keyword instanceof
is used to determine the type
of the current entry in the list, so that it can be appropriately
cast when delegating to the superclass’s
write( ) method. Once the contents of the queue
have been written to the log file, the backing store for the queue is
emptied and is ready for the next batch of messages.