I have written about 180 lines of code that implements an Event system.
I would like a general review about the code and I'd love comments about the usability of it. (Is the code useful? Would you like to use it or a similar system like it?)
Summary of the parts involved
IEvent
is a marker interface for event classesEvent
is an annotation that must be added for all methods that are listening to eventsEventListener
is a marker interface for classes that wants to listen to eventsEventHandler
is a class that is responsible for executing an event on a singleEventListener
EventExecutor
is the main class that binds everything together. It is where listeners must go to get registered and it is where IEvents must go to get executed.
Reason for implementing
I don't want to use a whole bunch of different interfaces (one for each type of IEvent
), along with addThisEventHandler
, addThatEventHandler
, dispatchThisEvent
, dispatchThatEvent
and a whole bunch of other repeatedly added stuff.
I want the system to be flexible and useful mainly. Speed is not a primary concern, but if you see something that is having in your opinion a too heavy performance cost then I'd love to hear about it.
The Code (Approximately 180 code lines, not including whitespace and comments)
CustomFacade.getLog...
is my own logging system which I don't think is relevant to the review, therefore the source is not included for this part. Besides this, the code is stand-alone (only depending on the regular Java classes, of course).
Event.java
/**
* Indication that a method should handle an {@link IEvent}<br>
* The method needs to return {@link Void} and expect exactly one parameter of a type that implements {@link IEvent}.<br>
* Only methods in classes that implements {@link EventListener} should use this annotation.
*/
@Retention(value = RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Event {
int priority() default 50;
}
EventExecutor.java
public class EventExecutor {
public static final int PRE = -1;
public static final int ALL = 0;
public static final int POST = 1;
private final Map<Class<? extends IEvent>, Collection<EventHandler>> bindings;
private final Set<EventListener> registeredListeners;
private boolean debug = false;
public void setDebug(boolean debug) {
this.debug = debug;
}
public EventExecutor() {
this.bindings = new HashMap<Class<? extends IEvent>, Collection<EventHandler>>();
this.registeredListeners = new HashSet<EventListener>();
}
public List<EventHandler> getListenersFor(Class<? extends IEvent> clazz) {
if (!this.bindings.containsKey(clazz))
return new ArrayList<EventHandler>(); // No handlers so we return an empty list
return new ArrayList<EventHandler>(this.bindings.get(clazz));
}
public <T extends IEvent> T executeEvent(T event, int i) {
Collection<EventHandler> handlers = this.bindings.get(event.getClass());
if (handlers == null) {
if (this.debug)
CustomFacade.getLog().d("Event " + event.getClass().getSimpleName() + " has no handlers.");
return event;
}
if (this.debug)
CustomFacade.getLog().d("Event " + event.getClass().getSimpleName() + " has " + handlers.size() + " handlers.");
for (EventHandler handler : handlers) {
// Basic support for multi-stage events. More can be added later by specifying exactly which priority to be executed - executeEventPre(event, lessThanPriority) for example
if (i == PRE && handler.getPriority() >= 0)
continue;
if (i == POST && handler.getPriority() < 0)
continue;
handler.execute(event);
}
return event;
}
public <T extends IEvent> T executeEvent(T event) {
return this.executeEvent(event, ALL);
}
public void registerListener(final EventListener listener) {
CustomFacade.getLog().v("Register event listener: " + listener);
if (registeredListeners.contains(listener)) {
CustomFacade.getLog().w("Listener already registred: " + listener);
return;
}
Method[] methods = listener.getClass().getDeclaredMethods();
this.registeredListeners.add(listener);
for (final Method method : methods) {
Event annotation = method.getAnnotation(Event.class);
if (annotation == null)
continue;
Class<?>[] parameters = method.getParameterTypes();
if (parameters.length != 1) // all listener methods should only have one parameter
continue;
Class<?> param = parameters[0];
if (!method.getReturnType().equals(void.class)) {
CustomFacade.getLog().w("Ignoring method due to non-void return: " + method.getName());
continue;
}
if (IEvent.class.isAssignableFrom(param)) {
@SuppressWarnings("unchecked") // Java just doesn't understand that this actually is a safe cast because of the above if-statement
Class<? extends IEvent> realParam = (Class<? extends IEvent>) param;
if (!this.bindings.containsKey(realParam)) {
this.bindings.put(realParam, new TreeSet<EventHandler>());
}
Collection<EventHandler> eventHandlersForEvent = this.bindings.get(realParam);
CustomFacade.getLog().v("Add listener method: " + method.getName() + " for event " + realParam.getSimpleName());
eventHandlersForEvent.add(createEventHandler(listener, method, annotation));
}
}
}
private EventHandler createEventHandler(final EventListener listener, final Method method, final Event annotation) {
return new EventHandler(listener, method, annotation);
}
public void clearListeners() {
this.bindings.clear();
this.registeredListeners.clear();
}
public void removeListener(EventListener listener) {
for (Entry<Class<? extends IEvent>, Collection<EventHandler>> ee : bindings.entrySet()) {
Iterator<EventHandler> it = ee.getValue().iterator();
while (it.hasNext()) {
EventHandler curr = it.next();
if (curr.getListener() == listener)
it.remove();
}
}
this.registeredListeners.remove(listener);
}
public Map<Class<? extends IEvent>, Collection<EventHandler>> getBindings() {
return new HashMap<Class<? extends IEvent>, Collection<EventHandler>>(bindings);
}
public Set<EventListener> getRegisteredListeners() {
return new HashSet<EventListener>(registeredListeners);
}
}
EventHandler.java
public class EventHandler implements Comparable<EventHandler> {
private final EventListener listener;
private final Method method;
private final Event annotation;
public EventHandler(EventListener listener, Method method, Event annotation) {
this.listener = listener;
this.method = method;
this.annotation = annotation;
}
public Event getAnnotation() {
return annotation;
}
public Method getMethod() {
return method;
}
public EventListener getListener() {
return listener;
}
public void execute(IEvent event) {
try {
method.invoke(listener, event);
} catch (IllegalAccessException e1) {
CustomFacade.getLog().e("Exception when performing EventHandler " + this.listener + " for event " + event.toString(), e1);
} catch (IllegalArgumentException e1) {
CustomFacade.getLog().e("Exception when performing EventHandler " + this.listener + " for event " + event.toString(), e1);
} catch (InvocationTargetException e1) {
CustomFacade.getLog().e("Exception when performing EventHandler " + this.listener + " for event " + event.toString(), e1);
}
}
@Override
public String toString() {
return "(EventHandler " + this.listener + ": " + method.getName() + ")";
}
public int getPriority() {
return annotation.priority();
}
@Override
public int compareTo(EventHandler other) {
// Because we are using a TreeSet to store EventHandlers in, compareTo should never return "equal".
int annotation = this.annotation.priority() - other.annotation.priority();
if (annotation == 0)
annotation = this.listener.hashCode() - other.listener.hashCode();
return annotation == 0 ? this.hashCode() - other.hashCode() : annotation;
}
}
EventListener.java
/**
* Marker interface for classes that can be scanned for @Event annotations
* @see Event
*/
public interface EventListener {}
IEvent.java
/**
* Marker interface for events
*/
public interface IEvent {}
A simple JUnit test
Normally the class(es) that implements EventListener
and the class that contains the EventExecutor
are in entirely different packages.
public class EventTest implements EventListener {
private EventExecutor events;
private int messages;
@Before
public void before() {
this.events = new EventExecutor();
this.events.registerListener(this);
}
@Test
public void test() {
assertEquals(0, messages);
this.events.executeEvent(new SimpleEvent("Message"));
assertEquals(1, messages);
}
@Event
public void onSimpleEvent(SimpleEvent event) {
messages++;
}
}