http://www.developer.com/java/ent/article.php/1000841/The-Java-RMI-Server-Framework.htm
This article will to allow you to understand why an asynchronous process needs
management and the steps necessary to design a custom Asynchronous Process Manager. First, we will set up the queuing and threading environment necessary for an
Asynchronous Process Manager. Next, we will turn this single component environment into a
Request Broker capable of parallel processing multiple queues. (5,400 words) Why use a framework? Why are Java GUI applets and applications
so successful? Because they run inside a container -- a framework that manages the
event queue and the user interface, (the JFrame, Frame, Window, Container, and Component
classes). What does an RMI Server offer applications
outside of basic communications? Nothing! You want it; you build it. Backend application developers know that it is first necessary to build a framework in
which the application lives. Under the hood The RMI Runtime takes care of the two most difficult
issues with networking -- the low level line protocol and the basic application level
protocol, (this is the Marshalling, etc). For most implementations, the RMI Runtime creates a
thread to handle each request from a Client. Once the request finishes, the thread waits
for a brief period for the next Client request. In this way, the RMI-Connection may reuse
threads. If no new request comes in, the RMI Runtime destroys the thread. It is good that the RMI Runtime creates threads to handle requests. However, the
limitations of the RMI Runtime environment quickly hinder our processing. Consider these
two issues: Timing: The Client sends a request to the RMI Server
for information contained in a private resource. If a horde of other users are updating
the private resource, by the time the request completes the original user has gone home.
If the private resource is non-functioning to the point where the request cannot complete,
not only has the original user gone home, the RMI-Connection thread hangs forever. The autonomous request, with callback: That is -- send in a
request, have it processed by a background thread that contacts the original Client when
complete. If we simply create a new application thread for every request, the
create/destroy overhead and the number of application threads will put a severe strain on
the Server, and, the Virtual Machine will eventually run out of resources. Pragmatic For a Client that needs a timed response, the RMI-Connection thread contacts an
application thread. If the application thread does not respond within the time limit, then
the RMI-Connection thread returns to the Client with a timeout message. For an autonomous request, the RMI-Connection thread contacts an application thread and
immediately returns to the Client with an "it's been scheduled" message. Now the concern is how does one design a queuing and application threading environment
so that:
We are going to examine an Asynchronous Process Manager that you can run. The classes
for execution as well as the source code are downloadable in Resources. Logical Processes In a managed asynchronous process environment, requests stack up in queues. Application
threads fetch requests from the queues and act upon them. Developers define queues for the
requests and the maximum number of application threads to service those requests. These
are logical processes. The Client sends a request to the Server. The RMI Runtime creates, or reuses, an
RMI-Connection thread that executes a method within the Implementation Class. The
appropriate method places the request into a prioritized wait list within a queue and uses
an Object-notify method to wake up an application thread to service the request. The timed request: The RMI-Connection thread uses the
Object-wait method to suspend its execution until either the application thread finishes
or the time limit expires. Upon completion, the RMI-Connection thread returns to the
Client with the response from the application or a time out message. The autonomous request: The RMI-Connection thread returns to the
Client with an "it was scheduled" message. All the application processing
happens asynchronously. What do we need for both these scenarios?
; The interface is straightforward and
simple: ; The concrete implementation class
contains the logic for placing the request in a request queue, waking up an application
thread and returning the reply to the Client. ; The parameter class
is very simple. The instance fields are: ; The start up class contains logic for
establishing the persistent environment and exporting the remote object. ; Application Queues contain three
elements: ; The thread class is straightforward. It
has a run method where it waits for work. It checks the queue's wait lists for new
requests. When it finds a request, it calls a method on the user-defined application
processing class to perform the application's work. It sends the return object from the
application processing class back to the RMI-Connection thread. This 'fetch-next-request /
call-work-class' loop continues until there are no more pending requests. Most developers see threading as part of the application class. This is where the
application class extends java.lang.Thread or implements java.lang.Runnable. Handling the
thread logic in addition to the application logic requires two different thought patterns
to merge as one. This framework design separates the thread logic from the application
logic so that any application processing class may easily plug-in to a thread structure. ; A separate application processing class
contains the application logic. The thread class calls the application processing class in
the appropriate way for your framework. The calling may be with reflection or any other
method. For this example, we use an Interface with one method. Any class that implements
this Interface is acceptable. The start up class gets a new instance of the application processing class, (that
implements DemoCall): DemoCall to_call = new YourAppl(); Looking at the parameter to the application doWork() method we see that it contains ; We expound on the support classes as
necessary. The underlying principle behind this framework is the concept of "common
memory". Common Memory An RMI Server is persistent. Any objects the Server creates with a live reference
remains for the life of the Server. When the Server starts, it gets a new instance of a
"common memory" class and assigns a private, static field with the reference.
Since the Server remains and the reference is live, the object never garbage collects.
Other objects the Server creates also get a reference to the "common memory"
class, which increases the number of live references. This includes the RMI Implementation
class. In this way, all threads running on the Server -- the RMI-Connection threads and
the application threads, have access to this "common memory." (There are many other ways of getting access to a common class. Two other ways
are: 1) The Singleton, with its getInstance()
method. 2) Using a class with static fields and each thread gets a new instance
of the class, (Java copies the static class
fields to the new instance.)) The key to modifying variables for use in multiple threads is the synchronized
statement or synchronized method modifier. In Java, there
is shared main memory and thread memory. Every thread gets a copy of the variables it
needs from shared main memory and saves those variables back into shared main memory for
use by other threads. See Chapter 17 of the Java Specification. Putting the access/mutate to an object's variables within a synchronized block or
method accomplishes three functions:
Therefore, to guarantee integrity and to make sure all threads have access to the
latest value of a variable, synchronize. For the ultimate word on memory
access, see Can double-checked locking be fixed? by Brian Goetz.
Also, for an in-depth article on multiple CPU thread synchronization, see Warning! Threading in a multiprocessor world, by Allen
Holub. Wake up call The FrameWorkBase class contains static references to other classes.
For this example, the fields are public. (This is simply one way to establish common
memory. As above, there are many more.) Some of those fields are: The persistent RMI Server instantiates the base of common-memory, FrameWorkBase
and assigns a class field to the reference of the FrameWorkBase object. Since the
reference is live and the Server is persistent, Java does
not garbage collect the object. Think of the start up class fields as anchor points. Since the start up class passes the FrameWorkBase reference to the constructor
of the Implementation class and the Implementation class saves the FrameWorkBase
reference in its instance fields, all RMI-Connection threads have access to the FrameWorkBase
class. When the Client invokes a remote method The Function entry contains a reference, (described later), for the desired Queue
within the Main Array of Queues. The RMI-Connection thread places the "enhanced request", (described later),
into that Queue's wait list, finds a waiting thread and wakes up the thread. Since the
Queue's instance fields contain references to all instantiated threads, the RMI-Connection
thread may use notify(). This requires a little explaining since most books on threads say to use notifyAll().
The notify() and notifyAll() methods are methods of class Object. The question is -- for a
particular Object, how many threads are there? In the case of the Queue, there is only one
thread per instance. By having queues with solid references to each thread, it is very simple to keep
statistics. Statistics form the basis for tuning. The RMI-Connection thread then waits for the application to finish by issuing an
Object-wait (timeout value). The other wake up call The RMI-Connection thread must pass something to the application thread to uniquely
identify itself. Any object the RMI-Connection thread creates and passes to another thread
by reference is available to the other thread. After a synchronization event, each thread
then has access to the current value of that object. For this example, the framework uses
an integer array. As above, the RMI-Connection thread places an "enhanced request" in the
Queue's wait list. This is the Client's Object from the parameter, FrameWorkParm,
and several other fields. The RMI-Connection thread creates a new integer array, Thus, having passed the application thread enough information to inform it how to
indicate request completion, (by assigning pnp[0] = 1), and a reference for the
notifyAll() method, (this), the RMI-Connection thread may now wait. Note below: The spin lock on (pnp[0] == 0) is because when any
application thread issues a notifyAll(), Java wakes up all
the RMI-Connection threads. When the wait completes, the RMI-Connection thread picks up the Object returned from
the application and passes the Object back to the Client. On the other side when the processing completes, the application thread does the
following: Autonomous request As part of the class definition of a Function is a field for the optional
Agent Queue. The application processing class may return an Object to the application
thread. For an autonomous request, when desirable, the application thread may activate a
logical process by creating a new "enhanced request", (with the just-returned
Object), placing that "enhanced request" into the Agent queue's wait list and
waking up an Agent thread. This is very similar to a standard autonomous request that
comes from the Client. The Agent logical process completes asynchronously, without any
return data. The Agent queue application processing class is where you may place the
call-back, call-forward or any other logic necessary for dealing with a request
completion. [This is a little confusing, but hang in there, eventually it becomes clear.] Shut down Critique Both answers to questions one and two are affirmative. This framework separates the RMI threading environment from the application threading
environment. Additionally, it separates the application thread logic from the actual
application logic. This is the greater degree of abstraction so important in
Object Oriented Design. The answer to the third question is: it's nice, but ... The return on investment may
not be worth the effort involved. An additional, critical part of the structure is error
recovery. Sometimes the anomaly code far outweighs the standard code. What this framework
needs is a bigger reason for living. What if we could expand this simple framework to support multiple queues per request?
That is -- when a Client request involves multiple accesses to resources, if we could
split the request and place each component into its own queue, then we could parallel
process the request. Now, the possibilities are endless. This is request brokering and it
is the subject of the next section. The Request Broker A better way to handle a multi-action request is to separate the request into its
component parts and place each component into a separate queue. This is parallel
processing. It is more difficult than linear programming but the benefits far outweigh the
extra work up front. What do we need to support request brokering? We need to understand that the Client
request is no longer the exclusive concern of a single logical process. Therefore, we must
put the Client's request into a common area so that any number of logical processes may
access it. Since we already have a "common memory" environment, we must now
enhance it. We need a common place to put the Object from the Client, (this is the input data
Object within the FrameWorkParm class). A simple array of Objects is
all that is necessary. For the example, the basic class is ObjDetail. The ObjDetail
class contains two fields, the Object from the Client and a status indicator. The array in which the ObjDetail Object resides is a linked-list. (All the
arrays in this framework are linked-lists.) Access to entries within the linked-list is
directly, by subscript. When an RMI-Connection thread puts an Object into the list, all
that the RMI-Connection thread must pass to an application thread is the primitive
integer. We need a common place to hold the request from the Client both for a synchronous and
for an asynchronous request. Once again, simple arrays of Objects are all that is
necessary. Both basic Objects must keep the subscript to the Object from the Client,
(above), and an integer array of subscripts to the Objects from the applications' return
data. The classes for this example are SyncDetail and AsyncDetail. Another common place to hold information for an asynchronous request is an array of
those requests that have stalled. When the synchronous request takes longer than the user
can wait, the connection side of the request terminates. When an asynchronous request
takes longer than is prudent, the processing may be unable to complete and the request
stalls. There must be a place to put the information and a procedure for recovering from
the stall. The place is the StallDetail class. The thread that places the
information there is the Monitor, (we'll get to this shortly). The procedure is the
exclusive needs of the user. The request Function The framework:
For the synchronous request, the framework waits until all queues finish processing.
The framework then concatenates the return objects from all the logical processes into a
single Object array and returns the Object array to the Client. For the autonomous request, the framework returns to the Client with an "its been
scheduled" message. The processing takes place asynchronously. When the last queue's
application finishes processing, the framework optionally concatenates the return
objects from all the logical processes into a single Object array and activates a new
logical process, the Agent. The Monitor Runtime This demonstration requires minimally the Java1.1
platform. Unzip the file into a directory. The structure is as follows: /Doc - Contains a single file, Doc.html, which documents all the classes and the
runtime procedure. Open the /Doc/Doc.html file for directions. Follow the directions for starting the RMI Registry and the FrameWorkServer, (section, Runtime). Follow the directions for starting a single-access Client, DemoClient_3, who's
Function is F3, which comprises three queues, (this is the section, the first time). This is what took place. The Client invoked the syncRequest() method on the FrameWorkServer
remote object passing a FrameWorkParm Object. The syncRequest():
While the syncRequest() was waiting, each application thread:
Load it up Follow the directions for running the visualization tool, FrameWorkThreads. Follow the directions for running the multiple Client threads class, DemoClientMultiBegin,
to put a load on the system. After you are done with the Server, you may shut it down gracefully with a Client
request, DemoClient_Shutdown. Sequel Error recovery: As above, "Sometimes the anomaly code far
outweighs the standard code". With a custom framework, the error recovery depends on
the application. Most detection depends on timing different aspects of the process.
Catching an exception is easy. Spotting a run-a-way thread is difficult. In order to know
what to look for, one must know what the application does. Thresholds: When to instantiate or activate an application thread is
paramount. The way the code example sits, the only time the framework instantiates or
activates a new thread within a logical process is 1) when no thread is alive in the queue
or 2) when a new request into a wait list causes an overflow.
It is usually better to activate another thread when the load on that queue becomes
greater than, "user determined". This is threshold processing. When the
RMI-Connection thread puts a new request into a wait list, the thread can determine the
current load and may start or activate another application thread. Hooks and exits: How does a developer handle connection pools?
How does a developer handle message queuing middleware packages? Remember, the Server is
persistent. You can add a start up hook in which you build a separate memory area where
you keep instantiated classes and private threads for these products. You can add a shut
down hook that gracefully shuts down the separate area. Logging: Anyone who has ever worked with a background process
knows how important it is to log errors. How else can anyone know what happened after a
failure? Any general-purpose log will suffice. Commercial products are available today and
the standard language will support logging in the near future. Custom vs. Generic: This is a custom framework. You build such a
system to support a set of applications. When your requirements are to support a wide
range of applications that do not fit into a set or there is no time to design one
yourself, then the better choice is to purchase a generic, full-feature Asynchronous
Process Manager. Conclusion We separated the RMI logic from the application logic. By doing this, we opened up the
world of application queuing and threading, (which is not restricted to RMI). This world
enabled us to:
Then we enhanced the single-process environment into a request broker capable of
parallel processing. We enriched the common memory environment to:
Henceforth, the RMI Server box is no longer empty. About the Author Acknowledgements This article was first published by IBM developerWorks. Download the zip file,
(~160K), for this article. We provide this software under the GNU GENERAL PUBLIC
LICENSE, Version 2, June 1991. The Java Specification,
Chapter 17 Threads and Locks When is a Singleton not a Singleton, by Joshua Fox Can double-checked locking be fixed? by Brian Goetz Warning! Threading in a multiprocessor world, by Allen
Holub Robust Event Logging with Syslog, by Nate Sammons Log4j delivers control over logging, by Ceki G|lc| AlphaWorks Logging Toolkit for Java © 2002 Cooperative Software Systems, Inc. All rights reserved.
The Java RMI Server Framework
March 29, 2002
An RMI Server runs as a separate process in the computer. Since this process is
without restriction to the temporal concurrence of the Client processes, it is
asynchronous. An asynchronous process requires a degree of management necessary to allow
it to execute independently, a framework.
Why are Enterprise Java Beans so successful? Because
they run inside a container -- a framework that manages persistence, messaging, thread
management, logging and much more.
To understand the need for a framework, it is first necessary to briefly get
under the hood of an RMI Server.
The practical solution to these and many more problems is to separate the RMI-Connection
activity from the application processing. One does this by creating an application queuing
and threading structure. This is the way highly reliable, fully mission-critical software
products work.
The operating system refers to the Java Virtual Machine as
a process. The operating system refers to threads as lightweight processes. What we are
going to create are logical processes.
Figure 1 Logical Processes
public interface FrameworkInterface
extends Remote {
public Object[] syncRequest(FrameWorkParm in)
throws RemoteException;
public Object[] asyncRequest(FrameWorkParm in)
throws RemoteException;
public String shutRequest()
throws RemoteException;
public final class FrameWorkParm
implements java.io.Serializable {
private Object input; // input data
private String func_name; // Function name
private int wait_time; // maximum time to wait
private int priority; // priority of the request
Requests stack up in wait lists when no threads are immediately available to act upon
them. When threads finish processing a request, they look in the wait lists for the next
request. This reduces machine overhead by letting each thread complete multiple requests
between a start/stop processing sequence.
By defining the total number of threads in each queue and only instantiating a thread when
it is actually necessary, we limit contention among threads and curtail the thread
overload problem.
public interface DemoCall {
public Object doWork(Object in, FrameWorkInterface fw)
throws java.lang.Throwable;
The thread class then calls the application processing class, to_call.doWork();
1) the reference to the Object from the Client and
2) a reference to the Server itself. The second reference is so the application may call
the Server, as a Client. This is recursion; one of the most useful techniques in
programming and sometimes the most difficult to implement.
In order for any two threads to talk to each other, they must use memory that is
common between them. No one thread owns this memory. It is updateable and viewable by all
threads.
Figure 2 Common Memory
How can an RMI-Connection thread find an application queue and wake up an application
thread? Then, how can that application thread wake up the RMI-Connection thread? The
answers lie in the structure of the environment.
public final class FrameWorkBase {
// *--- All are class fields ---*
// Main Array of Queues
public static FrameWorkMain main_tbl = null;
// Function Array
public static FuncHeader func_tbl = null;
// async Array
public static AsyncHeader async_tbl = null;
// sync Array
public static SyncHeader sync_tbl = null;
// Remote Object myself
public static FrameWorkInterface Ti = null;
public final class FrameWorkServer {
// The base for all persistent processing
private static FrameWorkBase T = null;
/**
* main entry point - starts the application
* @param args java.lang.String[]
*/
public static void main(java.lang.String[] args){
// the base for all processing
T = new FrameWorkBase();
// now, after initializing the other FrameWorkBase fields
// including the application queues and threads,
// do the Implementation class
// the Implementation class with a ref to FrameWorkBase
FrameWorkImpl fwi = new FrameWorkImpl(T);
public final class FrameWorkImpl
extends UnicastRemoteObject
implements FrameWorkInterface {
// instance field (base of common memory)
private FrameWorkBase Ty;
// constructor
public FrameWorkImpl (FrameWorkBase T)
throws RemoteException {
// set common memory reference
Ty = T;
The RMI-Connection thread, having a reference to the FrameWorkBase class as an
instance field, searches the Function Array, (described later), for a match on the passed
Function Name.
QueueThread qt1 = new QueueThread();
QueueThread qt2 = new QueueThread();
The fields, qt1 and qt2 are references, therefore, qt1.notify(), wakes up an arbitrary
thread. Since there is only one thread for each QueueThread Object, the notify()
works.
When the application thread finishes processing, it has to find the calling
RMI-Connection thread to wake it up. Finding the RMI-Connection thread is not so easy
since the RMI Runtime spawns threads without a reference to the Object,
(i.e., new RMI-Connection-Thread(), instead of with a reference,
conn_obj = new etc.).
So, how does the application thread know who called it? It doesn't. This is why it must
use notifyAll() and why a little more work is necessary.
// requestor obj to cancel wait
Object requestor;
// Created by RMI-Connection thread
int[] pnp;
// passed back object from the appl thread
Object back;
assigns the first integer to 0 and
assigns the Object requestor = this;.
// posted/not posted indicator: 0=not, 1=yes
// This is for the appl thread to post.
// Java passes arrays by reference, so the
// appl thread may have access to this object.
pnp = new int[1];
pnp[0] = 0;
// the reference to this object's monitor
requestor = this;
// Wait for the request to complete, or, time out
// get the monitor for this RMI object
synchronized (this) {
// until work finished
while (pnp[0] == 0) {
// wait for a post or timeout
try {
// max wait time is the time passed
wait(time_wait);
} catch (InterruptedException e) {}
// When not posted
if (pnp[0] == 0) {
// current time
time_now = System.currentTimeMillis();
// decrement wait time
time_wait -= (time_now - start_time);
// When no more seconds remain
if (time_wait < 1) {
// get out of the loop, timed out
break;
}
else {
// new start time
start_time = time_now;
}
}
}
}
// get lock on RMI obj monitor
synchronized (requestor) {
// the object from the application
back = data_object;
// set posted
pnp[0] = 1;
// wake up
requestor.notifyAll();
}
The autonomous request does not require the RMI-Connection thread to wait for completion.
Therefore, the autonomous request may seem very simple, but there is one catch. What
happens to the return data from the doWork() method of the
application processing class? It should not be the concern of an application where its
return data goes. A developer must be able to use the same application for a synchronous
or asynchronous request. Therefore, we need an Agent for the autonomous request.
In order to gracefully shut down the RMI Server, (rather than using something like a kill
-9 <pid>), every implementation should contain a shut down method. However, if the
shut down method simply ends the Java Virtual Machine, the
method's return message never makes it back to the Client. The better way is to start a
shut down thread. The shut down thread sleeps about two seconds, (to give the method's
return message a chance to clear the virtual machine), and then the shut down thread
issues System.exit(0).
That wasn't so bad. Now it is time to ask the three major questions necessary for every
project:
Having set up a basic queuing environment it soon becomes evident that some requests
really contain multiple actions, or components. For instance -- a request may require
accesses to two different databases. We could access each database in a linear fashion but
the second access must wait for the first to complete.
public final class ObjDetail {
private Object obj; // object from Client
private int status; // 0 = available, 1 = busy
public final class SyncDetail {
private int[] output; // output data array pointers
private int input; // input area pointer, if any
private int status; // 0=avail 1=busy
private int nbr_que; // total queue's in function
private int nbr_remaining; // remaining to be processed
private int next_output; // next output in list
private int wait_time; // max wait time in seconds
private Object requestor; // requestor obj to cancel wait
private int[] pnp; // 0 not posted, 1 is posted
public final class AsyncDetail {
private int input; // pointer to input
private int out_agent; // pointer to agent name
private int function; // pointer to function name
private int nbr_que; // nbr of queues in function
private int nbr_remaining; // remaining unprocessed
private int status; // 0 = available, 1 = busy
private int next_output; // next output subscript
private int[] que_names; // subscripts of all the queues
private int[] output; // output array
public final class StallDetail {
private long entered; // time entered
private long at_name; // Async Array generated name
private int gen_name; // Async Array pointer
private int status; // 0 = available, 1 = busy
private int times_checked; // times checked
private int failed_reason; // why it is here
How can a Server know the components of a request? The component structure is information
the developer knows from the beginning. In the basic framework, there is a single queue
for each Function. In the request broker framework, there is a list of queues for each
Function. The list of queues associated with each Function is the component
structure. The class is the FuncDetail array.
public final class FuncDetail {
private String name; // Function name
private long used; // times used
private int agent; // optional agent queue subscript
private int nbr_que; // number of queues in this entry
private int[] qtbl; // array of queues

Figure 3 Common Memory Referencing
An additional requirement for any asynchronous process is a way to monitor the logical
processes. The autonomous requests execute without any task waiting for their completion
and when they stall, detecting that stall is difficult. One way to monitor the environment
is with a daemon thread that scans the environment periodically. Daemon simply means that
it is not part of a particular application or RMI-Connection. When the monitor thread
finds a problem, it may log the problem, send a message to a middleware message queue or
internally notify another remote object. The action depends on the application. A common
function is to place the details of the autonomous request into a stalled array, (StallDetail).
What to do then is also application dependent.
Now that we've talked about threads and queues we're sure it is a little
confusing. It is time to put it all together with a demonstration. If you haven't downloaded the zip file yet, then do so now.
/Source - Contains all the source code for this article
/Classes - Contains all the class files for this article including a policy.all file for
security.
The excitement comes when many Clients hit on the Server simultaneously. Additionally,
without a visualization tool you would have no way of knowing what is going on. Within
this package, there are two classes that do just that, (this is the section, load it
up).
In this brief article, we can only examine the skeleton of an Asynchronous
Process Manager. Some supplemental elements are:
See also: The open source project, Log4j, Nate Sammons' article on Syslog, the AlphaWorks
Logging Toolkit for Java, in Resources.
Ok, there's a lot to it. Nobody claims building backend applications is simple.
Remember, the Java architects put a colossal effort into
building the EBJ and GUI frameworks. What do we now have?
Since his thesis on "Transactional Queuing and Sub-Tasking", Edward Harned has been actively honing his
multi-threading and multi-processing talents. First, leading projects as an employee in
major industries and then as an independent consultant. Today, Ed is a senior developer at
Cooperative Software Systems where, for the last
four years, he has used Java to bring asynchronous-process solutions to a wide range of
tasks. When not talking to threads, he sails, skis and photographs in New England.
Resources
http://java.sun.com/docs/books/jls/second_edition/html/memory.doc.html#30206
http://www.javaworld.com/javaworld/jw-01-2001/jw-0112-singleton.html
http://www.javaworld.com/jw-05-2001/jw-0525-double.html
http://www.javaworld.com/jw-02-2001/jw-0209-toolbox.html
http://www.javaworld.com/jw-04-2001/jw-0406-syslog.html
http://www.javaworld.com/javaworld/jw-11-2000/jw-1122-log4j.html
http://www.alphaworks.ibm.com/tech/loggingtoolkit4j