Distribute JavaBean Events Across Java Virtual Machines

Distribute JavaBean Events Across Java Virtual Machines

s a Java developer, sooner or later you will build a Java application that needs to communicate with another Java application. You can write Java network I/O code or use the Java Message Service (JMS) API to facilitate this communication. However, I’ve found both these approaches error prone and time consuming–I often had to rewrite and debug similar I/O or JMS code. So I came up with a better way, one as easy as coding to the JavaBean event model.

This article describes my approach to distributing JavaBean events across Java virtual machines. This approach transparently uses the JMS specification and extends JavaBean events across a LAN?even the Internet?without requiring any JMS-specific code within the event listener or the event source components. Because its main requirement is that both the listeners and sources extend an abstract base class, the approach also is non-intrusive.

For an overview of JMS, along with some sample code, read the sidebar, “The Java Message Service (JMS).”

The Event Distributor
To use JMS, the code you write must be repeated for each JMS application and each publisher, subscriber, producer, and consumer in those applications. To avoid having to copy and paste similar code in multiple places, a good developer would create wrapper code. As a matter of fact, the sample code that comes with Sun Microsystems’ JMS API contains a wrapper class for just this reason.

However, being the purist that I am, Sun’s wrapper class was not good enough for me. My goal was to create a wrapper class that would make JMS communication as easy as sending and receiving JavaBean events. So I created the Event Distributor, which uses the JMS publish/subscribe paradigm to distribute JavaBean events across Java virtual machines without exposing its details (and repeated code). You can even configure it to use persistent or non-persistent message delivery.

I’ve included the Event Distributor, along with a demonstration test application, in this article:

  • Click here to download the Event Distributor code along with a readme file on how to build and use it.
  • Click here to download a test application that demonstrates how to use the Event Distributor.

Applications written with the Event Distributor contain typical JavaBean event code. The only addition is an extended base class. You don’t need any JMS code or knowledge to make this work.Event Listener & Event Source Components
To define a JavaBean event listener, you must implement the java.util.EventListener interface along with the event interfaces in which it is interested. Event source components deliver events by calling the methods defined by the event interface on each event listener component.

Using the Event Distributor from an event listener is simple. Make the following enhancements to your event listener code to receive remote Java events:

  1. Import the com.DistributedEvents package.
  2. Extend the EventListenerDist base class.

These modifications are minor, and they don’t expose any implementation details about how the events will be received remotely.

Once you’ve instantiated the listener object, the Event Distributor automatically calls the EventListenerDist constructor. This constructor gets a reference to the Event Distributor singleton object and calls a method that notifies it about the new listener. The Event Distributor then uses Java reflection to get the name of each interface the listener object implements.

Listing 4 shows how to use reflection to get the names of an object’s implemented interfaces.

Listing 4. Discovering an Object’s Interfaces
Java reflection allows you to discover at runtime which interfaces are implemented on an object. The following code illustrates how easy it is to do this:

public void onNewListener(EventListenerDist listener){   Class listenerClass = listener.getClass();   Class[] interfaces = listenerClass.getInterfaces();   String[] interfaceNames =       new String[interfaces.length];      for ( int n = 0; n < interfaces.length; n++ )      interfaceNames[n] = interfaces[n].getName();     // ...}

For each interface the listener implements, the Event Distributor creates a JMS Topic and Subscriber object if it doesn't already exist. It then adds the listener object to a Vector of listeners for each interface. The Event Distributor stores this Vector in a HashMap where the key is the interface name itself (see Figure 3).

Figure 3: Storing References to Event Listeners

The Event Distributor creates only one JMS Topic and Subscriber object for an interface regardless of how many listeners implement that interface. The Event Distributor multicasts each message it receives to all the listeners it has for that event interface (see Figure 4).

Figure 4: New Listener Sequence Diagram

Event Source Components
The implementation of a listener registration method defines a JavaBean event source. This method accepts references to objects that implement a particular event interface.

Make the following enhancements to your event source code to send remote events:

  1. Import the com.DistributedEvents package.
  2. Extend the EventSourceDist base class.
  3. Define a method named getInterfaceNames that returns a String array. The array should contain the name of each interface for which the event source produces events.
  4. Define a method named addListener, which allows listener objects to register for the events the source component produces. Regardless of whether you use the Event Distributor, your event source components need to define a similar method.

These modifications are similar to those outlined for listener components, with the addition of two methods. Still, no implementation details about sending the events remotely via JMS are exposed.

Again, once you've instantiated the listener object, the Event Distributor automatically calls the EventListenerDist constructor. This constructor gets a reference to the Event Distributor singleton object and calls a method that notifies it about the new listener. The Event Distributor then uses Java reflection to get the name of each interface the listener object implements.

Using Java reflection, the Event Distributor dynamically examines each interface and creates a proxy event listener object (I'll describe this later). For each event interface, the Event Distributor also does the following:

  1. Provides the appropriate proxy listener object to the event source component that is to receive events
  2. Creates a JMS Topic and Producer for the interface

At the end of this process, the Event Distributor registers with the event source as a listener for each event interface, and all the JMS plumbing is in place to remotely distribute the events (see Figure 5).

Figure 5: Creating a Proxy Listener for Each Source

As a result, when an event source component produces an event, one of the listeners it will call is the proxy object that the Event Distributor created. All of the details of the event - the method name, parameters, and parameter types - are wrapped in a serializable object and sent out over JMS (see Figure 6). I describe the details of how this is done further in the section titled "Using Java Reflection."

Figure 6: New Source Sequence Diagram

JMS Configuration
The Event Distributor reads a configuration file when it is instantiated. This file contains all of the details needed to connect to a JMS provider, allowing you to specify the vendor-specific details for the JMS provider you use.Using Java Reflection
While JMS does the actual work in distributing JavaBean events, Java reflection makes it possible and transparent. Using reflection, the Event Distributor "intercepts" events by creating interface proxy classes. Reflection also enables it to receive the events by discovering the interfaces implemented by event listener components. The Event Distributor also dissects and wraps the events themselves, again using reflection.

All of this happens transparently, and it is non-intrusive to the source and listener components. It's a real showcase of the power of Java and reflection.

The Proxy Class
The java.lang.reflect library contains the Proxy class and InvocationHandler interface. With these, your code can at runtime assemble classes that implement one or more interfaces, as shown in Listing 5.

Listing 5. Create an Interface Proxy
With the following code, you can create an object at runtime that implements any valid interface:

protected Object createProxy(String interfaceName){   try {      InvocationHandler handler =          new EventInvocationHandler();            Class eventInterface =          Class.forName( interfaceName );            // Use the Java API's Proxy class      Object prxy = Proxy.newProxyInstance(         eventInterface.getClassLoader(),         new Class[] { eventInterface },         handler );            return prxy;   }   catch ( Exception e ) {      return null;   }}

First, create an instance of a class that implements the interface InvocationHandler (described in the next section). Basically, this class fields the method calls on the interfaces being proxied. Next, create a Class object for the interface being proxied using the interface name. Finally, call the Proxy.newProxyInstance method to create the proxy object itself.

The call to Proxy.newProxyInstance takes the following parameters:

  • A ClassLoader object
  • An array of Class objects, one for each interface being implemented
  • A reference to an object that implements the InvocationHandler interface

The end result is an object that implements the supplied interfaces. In the case of the Event Distributor, this object is given to the event source object via the addListener method. The InvocationHandler object supplied when creating the proxy handles all method calls on the Proxy object.

The InvocationHandler Interface
An invocation handler is an object that implements the InvocationHandler interface. As discussed in the previous section, a reference to such an object is used in creating an interface proxy object. When a proxy object calls methods, the invocation handler's invoke method fields them. The invoke method provides three parameters: the proxy object on which the call was made, the actual method that was called, and the method's arguments.

Listing 6 shows the code for a sample invoke method. The first step is to get the Class object for the proxy by calling proxy.getClass. Next, call getInterfaces on the proxy Class object to retrieve the implemented interfaces as an array of Class objects.

Listing 6. Invoking a Method on a Proxy Object
The following code is an implementation of the invoke method defined in Java's InvocationHandler interface. Java will call this method to provide you with a reference to your proxy object, the method that was called, and the method's arguments:

public Object invoke(Object proxy, Method mthd, Object[] args) throws Throwable{    try {        Class proxyClass = proxy.getClass();        Class[] interfaces = proxyClass.getInterfaces();        String interfaceName = interfaces[0].getName();        String methodName = mthd.getName();        Class[] argTypes = mthd.getParameterTypes();        log("   interface=" + interfaceName );        log("   method=" + methodName );        log("   parameters:");        for ( int p = 0; p < argTypes.length; p++ ) {            log("       Param " + (p + 1) +                ": " + argTypes[p].getName() +                 ", val=" + args[p]);        }    }    catch ( Exception e ) {        // ...    }}

You can prove that the proxy's interface truly implemented the invoked method by calling proxyClass.getMethods. Iterating through the returned Method object array should find a match.

Continuing with this example, the invoked method name is retrieved via a call to Method.getName. A call to Method.getParameterTypes then retrieves the argument types for the invoked method. The result is an array of Class objects, one for each argument of the method call.

By now I'm sure you've noticed a pattern to using Java reflection, which allows you to dive deeper into classes, methods, argument types, and argument values. The remainder of the example uses this pattern to output the details of the method call, the parameters, and the parameter types.The DistEvent Object
The Event Distributor uses an invocation handler to handle calls on each event interface proxy. It collects and wraps all the details of the method call, as outlined in the previous section, in a DistEvent object that it publishes using JMS.

In order for JMS to send this object between Java virtual machines and over the network, DistEvent must implement the interface. This interface simply identifies the implementing class as being serializable.

Calling Listeners
The Event Distributor implements a subscriber that uses JMS asynchronous message notification. When a message arrives, JMS calls the subscriber object's onEvent method. This is where the DistEvent object is extracted from the JMS message, as shown in Listing 7.

Listing 7. Handling a Remote Event
When the Event Distributor receives a JMS message, it must deliver the message to the correct event listener components. The following code calls the correct method on each listener for the applicable event interface:

protected void onEvent( DistEvent distEvent ){  try {    // Deliver the event to each listener     // of this event interface    Vector listeners = (Vector)      listenerMap.get( distEvent.interfaceName );        if ( listeners == null )      return;    int count = listeners.size();    for ( int i = 0; i < count; i++ )    {      EventListenerDist listener =        (EventListenerDist)listeners.elementAt(i);      deliverEvent( listener, distEvent );    }  }  catch ( Exception e ) { }}protected void deliverEvent(EventListenerDist listener, DistEvent distEvent){  try {    // Create the arg type array from the args array    // This array is used to get the called method     int argCount = distEvent.argNames.length;    Class[] argTypes = new Class[argCount];    for ( int p = 0; p < argCount; p++ )      argTypes[p] = distEvent.eventArgs[p].getClass();        Class listenerClass = listener.getClass();        Method eventMethod =      listenerClass.getMethod( distEvent.eventMethod,                               argTypes );    // The following invokes the event method on    // the actual event listener object    eventMethod.invoke( listener, distEvent.eventArgs );  }  catch ( Exception e ) {    e.printStackTrace();  }}

When the Event Distributor receives a JMS message, it performs the following steps:

  1. It locates the correct event listeners. It uses the interface name as a key to a HashMap, where each HashMap bucket points to a Vector containing listener objects. If at least one listener for this event interface exists, the Event Distributor will return and traverse a Vector.
  2. For each listener object in the Vector, the Event Distributor calls its deliverEvent method to deliver the event (see Listing 7).
  3. The Event Distributor uses the DistEvent object to extract the method's arguments. It then creates an array of Class objects and then populates it with each argument's type by calling getClass on each argument object. The Event Distributor uses this array, along with the given method name, to get a reference to the correct Method object.
  4. To invoke the event method on the actual listener object, it calls invoke on the Method object.

Figure 7 illustrates the entire process of sending and receiving a remote event.

Figure 7: Sending and Receiving a Remote Event

Simple and Quick Inter-JVM Communication
Although in some cases you will need to use the JMS API directly, the Event Distributor makes inter-JVM communication simple and quick. Simply put, the Event Distributor is a wrapper class that allows your code to use JMS through the JavaBean event model. As an added benefit, it provides a consistent remote messaging pattern within your code, which makes it easier to maintain and less prone to bugs.


About Our Editorial Process

At DevX, we’re dedicated to tech entrepreneurship. Our team closely follows industry shifts, new products, AI breakthroughs, technology trends, and funding announcements. Articles undergo thorough editing to ensure accuracy and clarity, reflecting DevX’s style and supporting entrepreneurs in the tech sphere.

See our full editorial policy.

About Our Journalist