diff --git a/core/src/main/java/dev/kske/eventbus/core/EventBus.java b/core/src/main/java/dev/kske/eventbus/core/EventBus.java index 334dd52..957da57 100644 --- a/core/src/main/java/dev/kske/eventbus/core/EventBus.java +++ b/core/src/main/java/dev/kske/eventbus/core/EventBus.java @@ -90,6 +90,16 @@ public final class EventBus { */ private final Map, TreeSet> 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, TreeSet> 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 getHandlersFor(Class eventType) { + return bindingCache.computeIfAbsent(eventType, k -> { - // Get handlers defined for the event class - TreeSet handlers = - bindings.getOrDefault(eventType, new TreeSet<>(byPriority)); + // Get handlers defined for the event class + TreeSet 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)) { + 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(); } diff --git a/core/src/test/java/dev/kske/eventbus/core/DispatchTest.java b/core/src/test/java/dev/kske/eventbus/core/DispatchTest.java index cfd4de1..919bdfc 100644 --- a/core/src/test/java/dev/kske/eventbus/core/DispatchTest.java +++ b/core/src/test/java/dev/kske/eventbus/core/DispatchTest.java @@ -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)); + } + + @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()); } } diff --git a/core/src/test/java/dev/kske/eventbus/core/SimpleEvent.java b/core/src/test/java/dev/kske/eventbus/core/SimpleEvent.java index 1512107..5536698 100644 --- a/core/src/test/java/dev/kske/eventbus/core/SimpleEvent.java +++ b/core/src/test/java/dev/kske/eventbus/core/SimpleEvent.java @@ -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() {