Posterous
Brandon is using Posterous to post everything online. Shouldn't you?
Brandon_july_2009_-_hires_headshot_thumb
 

mechanicalSPIRIT

Brandon Franklin's blog

Java Programming Tip: Building Your Own Event Bus

Most of us have worked with various types of event handling in Java. There's of course the basic Observer/Observable pair, but beyond that there are all sorts of MouseEvents, ComponentEvents, PropertyChangeEvents, and many others. These are great, and are a well-established pattern for keeping code loosely coupled. However, they all share the similar attribute of requiring that a listener be attached directly to the source of the events. This has two major downsides.

First of all, it forces the code to remove listeners in order to prevent many types of memory leaks, which is particularly annoying in cases where it's not trivial to institute a "cleanup" pass, essentially mirroring the behavior that would, in C++, be recognized as a "destructor". This sort of cleanup code tends to pollute the code in general, and is difficult to maintain over time. It seems somebody always forgets to null out that one critical field...

Secondly, and perhaps more importantly, this approach can make for some very ugly designs when building a large, interconnected application such as a complex GUI system. In such systems, components more often care about what kind of event is being fired somewhere--anywhere!--than they do about who is firing it. The components don't really want to attach as listeners to a source. They want to attach as listeners to an event type.

This is when it's time to think about using an event bus!

The exact design of your particular event bus can vary depending on your needs. I've written a few over the years and they've been quite different beasts. However, there's one that I have written that I generally use as a template when starting a new event bus. I call it the SmartEventBus. It consists of just 2 classes! Here's the code, with most comments and some minor sections removed for brevity: (NOTE: Uses Java 5 language syntax)

public abstract class SmartEvent<S> {

    private S source;

    private long timestamp;

    public SmartEvent(final S source) {
        this.source = source;

        timestamp = System.nanoTime();
    }

    public S getSource() {
        return source;
    }

    public long getTimestamp() {
        return timestamp;
    }

}


import java.lang.ref.*;
import java.lang.reflect.*;
import java.util.*;

public class SmartEventBus {

    private static SmartEventBus INSTANCE = null;

    private Map<Class, Set<WeakReference<Object>>> listeners = new HashMap<Class, Set<WeakReference<Object>>>();

    static public SmartEventBus getBus() {
        if( INSTANCE == null ) {
            INSTANCE = new SmartEventBus();
        }
        return INSTANCE;
    }

    private SmartEventBus() {
    }

    private Iterator<Class> superclassIterator(final Class clazz) {

        Set<Class> set = new LinkedHashSet<Class>();

        Class<?> superClass = clazz;
        while (superClass != null) {
            set.add(superClass);
            superClass = superClass.getSuperclass();
        }

        return set.iterator();
    }

    public synchronized void addListener(final Object listener) {

        // Pull out all the "hear" methods
        Set<Method> hearMethods = new HashSet<Method>();
        for (Method method : listener.getClass().getMethods()) {
            if (method.getName().equals("hear")) {
                hearMethods.add(method);
            }
        }

        // Examine every "hear" method
        for (Method method : hearMethods) {

            Class[] paramTypes = method.getParameterTypes();

            // Disqualify malformed candidates
            if ((paramTypes.length == 1)
                    && (SmartEvent.class.isAssignableFrom(paramTypes[0]))) {
                addTypeSpecificListener(listener, paramTypes[0]);
            }
        }

    }

    private void addTypeSpecificListener(final Object listener, final Class type) {

        // Get or create the Set of listeners for this type
        Set<WeakReference<Object>> typeListeners = listeners.get(type);
        if (typeListeners == null) {
            typeListeners = new HashSet<WeakReference<Object>>();
            listeners.put(type, typeListeners);
        }

        // Add the listener
        typeListeners.add(new WeakReference<Object>(listener));

    }

    public void fire(final SmartEvent event) {

        for (Iterator<Class> iter = superclassIterator(event.getClass()); iter.hasNext();) {

            Class type = iter.next();
            if (type.equals(Object.class)) {
                continue;
            }

            Set<WeakReference<Object>> typeListeners = listeners.get(type);
            if (typeListeners != null) {

                Collection<WeakReference<Object>> deadRefs = new LinkedList<WeakReference<Object>>();
                for (WeakReference<Object> listenerRef : typeListeners) {
                    Object listener = listenerRef.get();
                    if (listener != null) {

                        Method method = null;
                        try {
                            method = listener.getClass().getMethod("hear", type);

                        }
                        catch (NoSuchMethodException nsme) {
                            // This should theoretically never be true,
                            // but try to handle it gracefully anyway.
                            deadRefs.add(listenerRef);
                            continue;
                        }

                        Object result = null;
                        try {
                            result = method.invoke(listener, event);
                        }
                        catch (IllegalAccessException iae) {
                            iae.printStackTrace();
                        }
                        catch (InvocationTargetException ite) {
                            ite.printStackTrace();
                        }

                    }
                    else {
                        deadRefs.add(listenerRef);
                    }
                }
                typeListeners.removeAll(deadRefs);
            }
        }

    }

}

The SmartEvent class is, obviously, the base class that is used to represent events that get passed around the system. In your own event bus, it could contain just about anything you want, but mine contains simply a source object (parameterized) and a timestamp. This class gets extended into your entire event hierarchy.

The SmartEventBus is a singleton, because you use it like a service, getting a handle to it any time you need to use it. When an object adds itself as a listener, it is examined to find any and all methods beginning with the word "hear". The methods that have exactly one parameter will have the datatype of that parameter recorded in the bus as the type of event that the method cares about. In other words, when a class adds itself as a listener, it adds itself as a listener to everything it listens to. There's no interface to implement, and if the code changes such that the listener shouldn't care about a particular event type anymore, the developer need only delete the associated "hear" method. I'll leave more detailed analysis of the above source code up to the reader.

Since writing this original event bus, I've seen and written some other variations. One notable example is in the open source Buoy GUI framework. It uses a reflective system similar to my SmartEventBus. A more recent example is one that I wrote that uses the newly-added annotations framework in Java 5 syntax to mark the methods, rather than a naming convention. This means your listener methods look more like this:

@EventListener
public void handleSomeEvent(EventType e) {
...
}

While this is appealing in the sense that it frees one from onerous naming conventions and relies upon a more powerful reflective analysis, I find that it can sometimes be a bit frustrating in daily practice to not have a single, easily sorted "name group" of methods. For example, having all of your methods begin with "hear" causes them to be grouped in the outline view of Eclipse. It's a minor annoyance, but worth noting anyway.

In the end, there are as many ways to develop event buses as there are applications that need them, but by understanding some existing approaches and thinking about reflection rather than interface implementation, new doors can be opened to you that you may not have even realized existed.

Loading mentions Retweet
Filed under  //   events   Java   programming   tip  
Posted January 9, 2010
// 3 Comments