Compare commits
12 Commits
1.1.0
...
897d794b86
Author | SHA1 | Date | |
---|---|---|---|
897d794b86
![]() |
|||
40d48cb959
![]() |
|||
b760c58298
|
|||
872b395374
|
|||
82c66c45ec
|
|||
866a547114
![]() |
|||
33ebf0302b
|
|||
b915a5c490
![]() |
|||
205a183db7
|
|||
74447dea59
|
|||
6eebd3c121
|
|||
b758f4cef1
|
@ -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
|
||||||
|
@ -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.
|
||||||
*
|
*
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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() {
|
||||||
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
2
pom.xml
2
pom.xml
@ -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>
|
||||||
|
Reference in New Issue
Block a user