12 Commits

Author SHA1 Message Date
897d794b86 Merge pull request 'Handler Execution Order Debugging' (#25) from f/handler-introspection into develop
Reviewed-on: https://git.kske.dev/kske/event-bus/pulls/25
Reviewed-by: delvh <leon@kske.dev>
2021-11-02 09:03:10 +01:00
40d48cb959 Merge pull request 'Improve Documentation in Code' (#24) from f/improved-documentation into develop
Reviewed-on: https://git.kske.dev/kske/event-bus/pulls/24
Reviewed-by: DieGurke <maxi@kske.dev>
2021-11-01 21:48:51 +01:00
b760c58298 Add a handler execution order debugging method 2021-11-01 21:36:24 +01:00
872b395374 Rephrase some Javadoc
As suggested by @delvh.
2021-11-01 20:52:14 +01:00
82c66c45ec Improve EventBus Javadoc, make EventBusException final 2021-11-01 09:42:12 +01:00
866a547114 Merge pull request 'Initialize the Default Event Bus Statically' (#23) from f/static-singleton-initialization into develop
Reviewed-on: https://git.kske.dev/kske/event-bus/pulls/23
Reviewed-by: delvh <leon@kske.dev>
2021-10-16 08:33:08 +02:00
33ebf0302b Initialize the default event bus statically
The previous method that used double checked synchronization offers
little performance benefits over a plain static initialization.

Reported-by @harkle-the-cake
2021-10-16 08:32:28 +02:00
b915a5c490 Merge pull request 'Properly Handle Nested Dispatches' (#19) from b/nested-dispatch into develop
Reviewed-on: https://git.kske.dev/kske/event-bus/pulls/19
Reviewed-by: delvh <leon@kske.dev>
2021-07-12 11:25:04 +02:00
205a183db7 Allow nested dispatches by keeping track of nesting count 2021-07-12 10:24:48 +02:00
74447dea59 Add nested dispatch test
The test performs a nested event dispatch then cancels the dispatch. If
Both operations are successful, the test is successful.

Currently, the test fails, but should be successful once the nested
dispatch bug is fixed.
2021-07-12 10:17:46 +02:00
6eebd3c121 Pass errors caused during system event dispatch to caller
When an error is caused during the dispatch of a system event, a warning
has been logged instead instead of rethrowing the error. This has been
fixed.

This enables failing a JUnit test when an exception event handler is
invoked.
2021-07-07 22:06:07 +02:00
b758f4cef1 Remove obsolete paragraph from README 2021-04-04 10:09:12 +02:00
6 changed files with 200 additions and 44 deletions

View File

@ -45,9 +45,6 @@ public class SimpleEventListener {
} }
``` ```
In this case, an event bus is created and used locally.
In a more sophisticated example the class would acquire an external event bus that is used by multiple classes.
Note that creating static event handlers like this Note that creating static event handlers like this
```java ```java

View File

@ -27,7 +27,21 @@ public final class EventBus {
*/ */
private static final class DispatchState { private static final class DispatchState {
boolean isDispatching, isCancelled; /**
* Indicates that the last event handler invoked has called {@link EventBus#cancel}. In that
* case, the event is not dispatched further.
*
* @since 0.1.0
*/
boolean isCancelled;
/**
* Is incremented when {@link EventBus#dispatch(Object)} is invoked and decremented when it
* finishes. This allows keeping track of nested dispatches.
*
* @since 1.2.0
*/
int nestingCount;
} }
/** /**
@ -38,32 +52,41 @@ public final class EventBus {
*/ */
public static final int DEFAULT_PRIORITY = 100; public static final int DEFAULT_PRIORITY = 100;
private static volatile EventBus singletonInstance; private static final EventBus singletonInstance = new EventBus();
private static final Logger logger = System.getLogger(EventBus.class.getName()); private static final Logger logger = System.getLogger(EventBus.class.getName());
/** /**
* Produces a singleton instance of the event bus. It is lazily initialized on the first call. * Returns the default event bus, which is a statically initialized singleton instance.
* *
* @return a singleton instance of the event bus. * @return the default event bus
* @since 0.0.2 * @since 0.0.2
*/ */
public static EventBus getInstance() { public static EventBus getInstance() {
EventBus instance = singletonInstance; return singletonInstance;
if (instance == null)
synchronized (EventBus.class) {
if ((instance = singletonInstance) == null) {
logger.log(Level.DEBUG, "Initializing singleton event bus instance");
instance = singletonInstance = new EventBus();
}
}
return instance;
} }
private final Map<Class<?>, TreeSet<EventHandler>> bindings = /**
new ConcurrentHashMap<>(); * Event handler bindings (target class to handlers registered for that class), does not contain
private final Set<Object> registeredListeners = * other (polymorphic) handlers.
ConcurrentHashMap.newKeySet(); *
* @since 0.0.1
*/
private final Map<Class<?>, TreeSet<EventHandler>> bindings = new ConcurrentHashMap<>();
/**
* Stores all registered event listeners (which declare event handlers) and prevents them from
* being garbage collected.
*
* @since 0.0.1
*/
private final Set<Object> registeredListeners = ConcurrentHashMap.newKeySet();
/**
* The current event dispatching state, local to each thread.
*
* @since 0.1.0
*/
private final ThreadLocal<DispatchState> dispatchState = private final ThreadLocal<DispatchState> dispatchState =
ThreadLocal.withInitial(DispatchState::new); ThreadLocal.withInitial(DispatchState::new);
@ -73,17 +96,20 @@ public final class EventBus {
* *
* @param event the event to dispatch * @param event the event to dispatch
* @throws EventBusException if an event handler isn't accessible or has an invalid signature * @throws EventBusException if an event handler isn't accessible or has an invalid signature
* @throws NullPointerException if the specified event is {@code null}
* @since 0.0.1 * @since 0.0.1
*/ */
public void dispatch(Object event) throws EventBusException { public void dispatch(Object event) throws EventBusException {
Objects.requireNonNull(event); Objects.requireNonNull(event);
logger.log(Level.INFO, "Dispatching event {0}", event); logger.log(Level.INFO, "Dispatching event {0}", event);
// Set dispatch state // Look up dispatch state
var state = dispatchState.get(); var state = dispatchState.get();
state.isDispatching = true;
Iterator<EventHandler> handlers = getHandlersFor(event.getClass()); // Increment nesting count (becomes > 1 during nested dispatches)
++state.nestingCount;
Iterator<EventHandler> handlers = getHandlersFor(event.getClass()).iterator();
if (handlers.hasNext()) { if (handlers.hasNext()) {
while (handlers.hasNext()) while (handlers.hasNext())
if (state.isCancelled) { if (state.isCancelled) {
@ -94,14 +120,14 @@ public final class EventBus {
try { try {
handlers.next().execute(event); handlers.next().execute(event);
} catch (InvocationTargetException e) { } catch (InvocationTargetException e) {
if (event instanceof DeadEvent || event instanceof ExceptionEvent) if (e.getCause() instanceof Error)
// Warn about system event not being handled
logger.log(Level.WARNING, event + " not handled due to exception", e);
else if (e.getCause() instanceof Error)
// Transparently pass error to the caller // Transparently pass error to the caller
throw (Error) e.getCause(); throw (Error) e.getCause();
else if (event instanceof DeadEvent || event instanceof ExceptionEvent)
// Warn about system event not being handled
logger.log(Level.WARNING, event + " not handled due to exception", e);
else else
// Dispatch exception event // Dispatch exception event
@ -118,8 +144,8 @@ public final class EventBus {
dispatch(new DeadEvent(this, event)); dispatch(new DeadEvent(this, event));
} }
// Reset dispatch state // Decrement nesting count (becomes 0 when all dispatches on the thread are finished)
state.isDispatching = false; --state.nestingCount;
logger.log(Level.DEBUG, "Finished dispatching event {0}", event); logger.log(Level.DEBUG, "Finished dispatching event {0}", event);
} }
@ -129,10 +155,10 @@ public final class EventBus {
* that are bound to a supertype of the event class. * that are bound to a supertype of the event class.
* *
* @param eventClass the event class to use for the search * @param eventClass the event class to use for the search
* @return an iterator over the applicable handlers in descending order of priority * @return a navigable set containing the applicable handlers in descending order of priority
* @since 0.0.1 * @since 1.2.0
*/ */
private Iterator<EventHandler> getHandlersFor(Class<?> eventClass) { private NavigableSet<EventHandler> getHandlersFor(Class<?> eventClass) {
// Get handlers defined for the event class // Get handlers defined for the event class
TreeSet<EventHandler> handlers = bindings.getOrDefault(eventClass, new TreeSet<>()); TreeSet<EventHandler> handlers = bindings.getOrDefault(eventClass, new TreeSet<>());
@ -144,7 +170,7 @@ public final class EventBus {
if (handler.isPolymorphic()) if (handler.isPolymorphic())
handlers.add(handler); handlers.add(handler);
return handlers.iterator(); return handlers;
} }
/** /**
@ -155,7 +181,7 @@ public final class EventBus {
*/ */
public void cancel() { public void cancel() {
var state = dispatchState.get(); var state = dispatchState.get();
if (state.isDispatching && !state.isCancelled) if (state.nestingCount > 0 && !state.isCancelled)
state.isCancelled = true; state.isCancelled = true;
else else
throw new EventBusException("Calling thread not an active dispatching thread!"); throw new EventBusException("Calling thread not an active dispatching thread!");
@ -165,8 +191,9 @@ public final class EventBus {
* Registers an event listener at this event bus. * Registers an event listener at this event bus.
* *
* @param listener the listener to register * @param listener the listener to register
* @throws EventBusException if the listener is already registered or a declared event handler * @throws EventBusException if the listener is already registered or a declared event
* does not comply with the specification * handler does not comply with the specification
* @throws NullPointerException if the specified listener is {@code null}
* @since 0.0.1 * @since 0.0.1
* @see Event * @see Event
*/ */
@ -221,6 +248,7 @@ public final class EventBus {
Objects.requireNonNull(listener); Objects.requireNonNull(listener);
logger.log(Level.INFO, "Removing event listener {0}", listener.getClass().getName()); logger.log(Level.INFO, "Removing event listener {0}", listener.getClass().getName());
// Remove bindings from binding map
for (var binding : bindings.values()) { for (var binding : bindings.values()) {
var it = binding.iterator(); var it = binding.iterator();
while (it.hasNext()) { while (it.hasNext()) {
@ -231,6 +259,8 @@ public final class EventBus {
} }
} }
} }
// Remove the listener itself
registeredListeners.remove(listener); registeredListeners.remove(listener);
} }
@ -245,6 +275,39 @@ public final class EventBus {
registeredListeners.clear(); registeredListeners.clear();
} }
/**
* Generates a string describing the event handlers that would be executed for a specific event
* type, in order and without actually executing them.
*
* @apiNote Using this method is only recommended for debugging purposes, as the output depends
* on implementation internals which may be subject to change.
* @implNote Nested dispatches are not accounted for, as this would require actually executing
* the handlers.
* @param eventType the event type to generate the execution order for
* @return a human-readable event handler list suitable for debugging purposes
* @since 1.2.0
*/
public String printExecutionOrder(Class<?> eventType) {
var handlers = getHandlersFor(eventType);
var sj = new StringJoiner("\n");
// Output header line
sj.add(String.format("Event handler execution order for %s (%d handler(s)):", eventType,
handlers.size()));
sj.add(
"==========================================================================================");
// Individual handlers
for (var handler : handlers)
sj.add(handler.toString());
// Bottom line
sj.add(
"==========================================================================================");
return sj.toString();
}
/** /**
* Provides an unmodifiable view of the event listeners registered at this event bus. * Provides an unmodifiable view of the event listeners registered at this event bus.
* *

View File

@ -1,14 +1,18 @@
package dev.kske.eventbus.core; package dev.kske.eventbus.core;
/** /**
* This runtime exception is thrown when an event bus error occurs. This can * This unchecked exception is specific to the event bus and can be thrown under the following
* either occur while registering event listeners with invalid handlers, or when * circumstances:
* an event handler throws an exception. * <ul>
* <li>An event handler throws an exception (which is stored as the cause)</li>
* <li>An event listener with an invalid event handler is registered</li>
* <li>{@link EventBus#cancel()} is invoked from outside an active dispatch thread</li>
* </ul>
* *
* @author Kai S. K. Engelbart * @author Kai S. K. Engelbart
* @since 0.0.1 * @since 0.0.1
*/ */
public class EventBusException extends RuntimeException { public final class EventBusException extends RuntimeException {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;

View File

@ -40,6 +40,25 @@ class DispatchTest {
bus.dispatch(new SimpleEvent()); bus.dispatch(new SimpleEvent());
} }
/**
* Tests {@link EventBus#printExecutionOrder(Class)} based on the currently registered handlers.
*
* @since 1.2.0
*/
@Test
void testPrintExecutionOrder() {
String executionOrder = bus.printExecutionOrder(SimpleEvent.class);
System.out.println(executionOrder);
assertEquals(
"Event handler execution order for class dev.kske.eventbus.core.SimpleEvent (3 handler(s)):\n"
+ "==========================================================================================\n"
+ "EventHandler[method=void dev.kske.eventbus.core.DispatchTest.onSimpleEventFirst(), eventType=class dev.kske.eventbus.core.SimpleEvent, useParameter=false, polymorphic=true, priority=200]\n"
+ "EventHandler[method=static void dev.kske.eventbus.core.DispatchTest.onSimpleEventSecond(), eventType=class dev.kske.eventbus.core.SimpleEvent, useParameter=false, polymorphic=false, priority=150]\n"
+ "EventHandler[method=void dev.kske.eventbus.core.DispatchTest.onSimpleEventThird(dev.kske.eventbus.core.SimpleEvent), eventType=class dev.kske.eventbus.core.SimpleEvent, useParameter=true, polymorphic=false, priority=100]\n"
+ "==========================================================================================",
executionOrder);
}
@Event(SimpleEvent.class) @Event(SimpleEvent.class)
@Priority(200) @Priority(200)
void onSimpleEventFirst() { void onSimpleEventFirst() {

View File

@ -0,0 +1,73 @@
package dev.kske.eventbus.core;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.*;
/**
* Tests nested event dispatches.
*
* @author Kai S. K. Engelbart
* @since 1.2.0
*/
class NestedTest {
EventBus bus;
boolean nestedHit;
/**
* Constructs an event bus and registers this test instance as an event listener.
*
* @since 1.2.0
*/
@BeforeEach
void registerListener() {
bus = new EventBus();
bus.registerListener(this);
}
/**
* Dispatches a simple event, which should in turn cause a string to be dispatched as a nested
* event. If the corresponding handler sets {@link #nestedHit} to {@code true}, the test is
* successful.
*
* @since 1.2.0
*/
@Test
void testNestedDispatch() {
bus.dispatch(new SimpleEvent());
assertTrue(nestedHit);
}
/**
* Dispatches a string as a nested event and cancels the current dispatch afterwards.
*
* @since 1.2.0
*/
@Event(SimpleEvent.class)
void onSimpleEvent() {
bus.dispatch("Nested event");
bus.cancel();
}
/**
* Sets {@link #nestedHit} to {@code true} indicating that nested dispatches work.
*
* @since 1.2.0
*/
@Event(String.class)
void onString() {
nestedHit = true;
}
/**
* Fails the test if an exception is caused during the dispatch.
*
* @param e the event containing the exception
* @since 1.2.0
*/
@Event
void onException(ExceptionEvent e) {
fail("Exception during dispatch", e.getCause());
}
}

View File

@ -9,7 +9,7 @@
<packaging>pom</packaging> <packaging>pom</packaging>
<name>Event Bus</name> <name>Event Bus</name>
<description>An event handling framework for Java utilizing annotations.</description> <description>An event handling library for Java utilizing annotations.</description>
<url>https://git.kske.dev/kske/event-bus</url> <url>https://git.kske.dev/kske/event-bus</url>
<modules> <modules>