Handler Caching #37
@ -90,6 +90,16 @@ public final class EventBus {
|
||||
*/
|
||||
private final Map<Class<?>, TreeSet<EventHandler>> bindings = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* A cache mapping an event class to all handlers the event should be dispatched to. This
|
||||
* includes polymorphic handlers that don't reference the event class explicitly. If an event
|
||||
* class is not contained inside this cache, the {@link #bindings} have to be traversed manually
|
||||
* in search of applicable handlers.
|
||||
*
|
||||
* @since 1.3.0
|
||||
*/
|
||||
private final Map<Class<?>, TreeSet<EventHandler>> bindingCache = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* Stores all registered event listeners (which declare event handlers) and prevents them from
|
||||
* being garbage collected.
|
||||
@ -175,24 +185,29 @@ public final class EventBus {
|
||||
* Searches for the event handlers bound to an event class. This includes polymorphic handlers
|
||||
* that are bound to a supertype of the event class.
|
||||
*
|
||||
* @implNote If the given event type was requested in the past, the handlers are retrieved from
|
||||
* the {@link #bindingCache}. If not, the entire {@link #bindings} are traversed in
|
||||
* search of polymorphic handlers compatible with the event type.
|
||||
* @param eventType the event type to use for the search
|
||||
* @return a navigable set containing the applicable handlers in descending order of priority
|
||||
* @since 1.2.0
|
||||
*/
|
||||
private NavigableSet<EventHandler> getHandlersFor(Class<?> eventType) {
|
||||
return bindingCache.computeIfAbsent(eventType, k -> {
|
||||
|
||||
// Get handlers defined for the event class
|
||||
TreeSet<EventHandler> handlers =
|
||||
bindings.getOrDefault(eventType, new TreeSet<>(byPriority));
|
||||
// Get handlers defined for the event class
|
||||
TreeSet<EventHandler> handlers =
|
||||
bindings.getOrDefault(eventType, new TreeSet<>(byPriority));
|
||||
|
||||
// Get polymorphic handlers
|
||||
for (var binding : bindings.entrySet())
|
||||
if (binding.getKey().isAssignableFrom(eventType))
|
||||
for (var handler : binding.getValue())
|
||||
if (handler.isPolymorphic())
|
||||
handlers.add(handler);
|
||||
// Get polymorphic handlers
|
||||
for (var binding : bindings.entrySet())
|
||||
if (binding.getKey().isAssignableFrom(eventType))
|
||||
for (var handler : binding.getValue())
|
||||
if (handler.isPolymorphic())
|
||||
handlers.add(handler);
|
||||
|
||||
return handlers;
|
||||
return handlers;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@ -369,15 +384,28 @@ public final class EventBus {
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts a new handler into the {@link #bindings} map.
|
||||
* Inserts a new handler into the {@link #bindings} map. Additionally, the handler is placed
|
||||
* inside the {@link #bindingCache} where applicable.
|
||||
*
|
||||
* @param handler the handler to bind
|
||||
* @since 1.2.0
|
||||
*/
|
||||
private void bindHandler(EventHandler handler) {
|
||||
bindings.putIfAbsent(handler.getEventType(), new TreeSet<>(byPriority));
|
||||
|
||||
// Bind handler
|
||||
logger.log(Level.DEBUG, "Binding event handler {0}", handler);
|
||||
bindings.get(handler.getEventType()).add(handler);
|
||||
bindings.computeIfAbsent(handler.getEventType(), k -> new TreeSet<>(byPriority))
|
||||
.add(handler);
|
||||
|
||||
// Insert handler into cache
|
||||
bindingCache.computeIfAbsent(handler.getEventType(), k -> new TreeSet<>(byPriority))
|
||||
.add(handler);
|
||||
|
||||
// Handler is polymorphic => insert where applicable
|
||||
if (handler.isPolymorphic())
|
||||
for (var binding : bindingCache.entrySet())
|
||||
if (binding.getKey().isAssignableFrom(handler.getEventType()))
|
||||
binding.getValue().add(handler);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -395,13 +423,25 @@ public final class EventBus {
|
||||
var it = binding.iterator();
|
||||
while (it.hasNext()) {
|
||||
var handler = it.next();
|
||||
if (handler.getListener() == listener) {
|
||||
if (handler.getListener().equals(listener)) {
|
||||
logger.log(Level.DEBUG, "Unbinding event handler {0}", handler);
|
||||
it.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove bindings from cache
|
||||
for (var binding : bindingCache.values()) {
|
||||
var it = binding.iterator();
|
||||
while (it.hasNext()) {
|
||||
var handler = it.next();
|
||||
if (handler.getListener().equals(listener)) {
|
||||
kske marked this conversation as resolved
Outdated
|
||||
logger.log(Level.TRACE, "Removing event handler {0} from cache", handler);
|
||||
it.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the listener itself
|
||||
registeredListeners.remove(listener);
|
||||
}
|
||||
@ -414,6 +454,7 @@ public final class EventBus {
|
||||
public void clearListeners() {
|
||||
logger.log(Level.INFO, "Clearing event listeners");
|
||||
bindings.clear();
|
||||
bindingCache.clear();
|
||||
registeredListeners.clear();
|
||||
}
|
||||
|
||||
|
@ -14,8 +14,7 @@ import org.junit.jupiter.api.*;
|
||||
@Priority(150)
|
||||
class DispatchTest {
|
||||
|
||||
EventBus bus;
|
||||
static int hits;
|
||||
EventBus bus;
|
||||
|
||||
/**
|
||||
* Constructs an event bus and registers this test instance as an event listener.
|
||||
@ -27,8 +26,8 @@ class DispatchTest {
|
||||
bus = new EventBus();
|
||||
bus.registerListener(this);
|
||||
bus.registerListener(SimpleEvent.class, e -> {
|
||||
++hits;
|
||||
assertEquals(4, hits);
|
||||
e.increment();
|
||||
assertEquals(3, e.getCounter());
|
||||
});
|
||||
}
|
||||
|
||||
@ -51,29 +50,39 @@ class DispatchTest {
|
||||
*/
|
||||
@Test
|
||||
void testDebugExecutionOrder() {
|
||||
String executionOrder = bus.debugExecutionOrder(SimpleEvent.class);
|
||||
System.out.println(executionOrder);
|
||||
assertEquals(
|
||||
"Event handler execution order for class dev.kske.eventbus.core.SimpleEvent (3 handler(s)):\n"
|
||||
+ "==========================================================================================\n"
|
||||
+ "ReflectiveEventHandler[eventType=class dev.kske.eventbus.core.SimpleEvent, polymorphic=true, priority=200, method=void dev.kske.eventbus.core.DispatchTest.onSimpleEventFirst(), useParameter=false]\n"
|
||||
+ "ReflectiveEventHandler[eventType=class dev.kske.eventbus.core.SimpleEvent, polymorphic=false, priority=150, method=static void dev.kske.eventbus.core.DispatchTest.onSimpleEventSecond(), useParameter=false]\n"
|
||||
+ "ReflectiveEventHandler[eventType=class dev.kske.eventbus.core.SimpleEvent, polymorphic=true, priority=200, method=void dev.kske.eventbus.core.DispatchTest.onSimpleEventFirst(dev.kske.eventbus.core.SimpleEvent), useParameter=true]\n"
|
||||
+ "ReflectiveEventHandler[eventType=class dev.kske.eventbus.core.SimpleEvent, polymorphic=false, priority=150, method=static void dev.kske.eventbus.core.DispatchTest.onSimpleEventSecond(dev.kske.eventbus.core.SimpleEvent), useParameter=true]\n"
|
||||
+ "CallbackEventHandler[eventType=class dev.kske.eventbus.core.SimpleEvent, polymorphic=false, priority=100]\n"
|
||||
+ "==========================================================================================",
|
||||
executionOrder);
|
||||
bus.debugExecutionOrder(SimpleEvent.class));
|
||||
}
|
||||
|
||||
@Event(SimpleEvent.class)
|
||||
/**
|
||||
* Tests whether the handlers bound to an event type are correct when retrieved from the binding
|
||||
* cache. On the second call of {@link EventBus#debugExecutionOrder(Class)} the cache is used.
|
||||
*
|
||||
* @since 1.3.0
|
||||
*/
|
||||
@Test
|
||||
void testBindingCache() {
|
||||
assertEquals(bus.debugExecutionOrder(SimpleEventSub.class),
|
||||
bus.debugExecutionOrder(SimpleEventSub.class));
|
||||
}
|
||||
|
||||
kske marked this conversation as resolved
delvh
commented
Why? Why?
|
||||
@Event
|
||||
@Priority(200)
|
||||
void onSimpleEventFirst() {
|
||||
++hits;
|
||||
assertTrue(hits == 1 || hits == 2);
|
||||
void onSimpleEventFirst(SimpleEvent event) {
|
||||
event.increment();
|
||||
assertTrue(event.getCounter() == 1 || event.getCounter() == 2);
|
||||
}
|
||||
|
||||
@Event(SimpleEvent.class)
|
||||
@Event
|
||||
@Polymorphic(false)
|
||||
static void onSimpleEventSecond() {
|
||||
++hits;
|
||||
assertEquals(3, hits);
|
||||
static void onSimpleEventSecond(SimpleEvent event) {
|
||||
event.increment();
|
||||
assertEquals(2, event.getCounter());
|
||||
}
|
||||
}
|
||||
|
@ -14,7 +14,7 @@ class SimpleEvent {
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("SimpleEvent[%d]", counter);
|
||||
return String.format("%s[%d]", getClass().getSimpleName(), counter);
|
||||
}
|
||||
|
||||
void increment() {
|
||||
|
Loading…
Reference in New Issue
Block a user
Maybe add a comment on why
==
instead ofequals
.As we implicitly test using
equals
inregisterListener(...)
, I will change this as well.