For a personal project (a 2D scene graph project), I needed to implement some kind of event handling. The usual Java-way to do it is to provide a listener interface, with the appropriate methods. This looks like :
public interface MyEventListener {
void handleEvent1(MyEvent event);
void handleEvent2(MyEvent event);
}
When you want to react to an event (a so-called event handler), you would just implement this interface, and add it to the listeners of the object you want to ‘monitor’:
public class MyHandler implements MyEventListener {
public void handleEvent1(MyEvent event){
//...
}
public void handleEvent2(MyEvent event){
//...
}
}
This is a well known pattern.
But during development, I realized I had to add more kinds of events, and thus more kinds of listeners. Each listeners had more and more methods (the same event object could be fired in many situations).
Moreover, my event handlers required to implement many of the listeners, but not all of their methods.
In such situations, you have to implement several interfaces, implement all the methods
public class MyEventHandler implements MyListener1, MyListener2 {
// implement all the methods...
}
and add an instance to all listener lists:
MyEventHandler handler = new MyEventHandler(); objectToMonitor.addListener1(handler); objectToMonitor.addListener2(handler);
So, in the process of reinventing the wheel just for fun, I looked for another way to achieve this.
I finally found that an event handler could be easily implemented using Java annotations.
An event handler would be a regular Java object (no listener interface to implement). An annotation would indicate which method should be called (based on the list of event names, but could be more complicated like regular expressions):
public class MyHandler { // No interface to implement!
// Process 'event1'.
@HandleEvent("event1")
public void doSomething(MyEvent event) { ... }
// Process 'event1' and 'event2' in the same method.
@HandleEvent({"event1", "event2"})
public void doSomethingElse(MyEvent event) { ... }
}
The nice part is that the handler object can be added using a single method:
MyHandler handler = new MyHandler(); objectToMonitor.addHandler(handler);
That way, all addXXXListener() methods are centralized into a single addHandler() entry point.
The class of ‘objectToMonitor’ must just be provided with the capability to dispatch (named) events to the annotated method of the handler.
Here is the code for such an event dispatcher :
public class EventDispatcher {
private Collection handlers = new ArrayList();
/**
* Adds an event handler.
*/
public void addHandler(Object handler) {
this.handlers.add(handler);
}
/**
* Removes an event handler.
*/
public void removeHandler(Object handler) {
this.handlers.remove(handler);
}
/**
* Dispatch an event to the registered handlers.
*/
public void dispatchEvent(Event event) {
for (Object handler : handlers) {
dispatchEventTo(event, handler);
}
}
protected void dispatchEventTo(Event event, Object handler) {
Collection methods = findMatchingEventHandlerMethods(handler, event.getName());
for (Method method : methods) {
try {
// Make sure the method is accessible (JDK bug ?)
method.setAccessible(true);
if (method.getParameterTypes().length == 0)
method.invoke(handler);
if (method.getParameterTypes().length == 1)
method.invoke(handler, event);
if (method.getParameterTypes().length == 2)
method.invoke(handler, this, event);
} catch (Exception e) {
System.err.println("Could not invoke event handler!");
e.printStackTrace(System.err);
}
}
}
/**
* Find all methods from the <em>handler</em> object that must be called, based on the presence
* of the HandleEvent annotation.
*/
private Collection findMatchingEventHandlerMethods(Object handler, String eventName) {
Method[] methods = handler.getClass().getDeclaredMethods();
Collection result = new ArrayList();
for (Method method : methods) {
if (canHandleEvent(method, eventName)) {
result.add(method);
}
}
return result;
}
/**
* Look for the annotation values.
*/
private boolean canHandleEvent(Method method, String eventName) {
HandleEvent handleEventAnnotation = method.getAnnotation(HandleEvent.class);
if (handleEventAnnotation != null) {
String[] values = handleEventAnnotation.value();
return Arrays.asList(values).contains(eventName);
}
return false;
}
}
The code for the annotation is trivial:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface HandleEvent {
String[] value();
}
You can find the complete source code
here.
What do you think of it ?