From 7fb633d69fda9b6d00367dd2480b3e076e198558 Mon Sep 17 00:00:00 2001 From: kske Date: Sun, 9 Jan 2022 14:16:30 +0100 Subject: [PATCH 1/2] Inherit event handlers When registering an event listener, Event Bus recursively walks the entire inheritance tree and looks for event handlers. --- README.md | 6 +++ .../java/dev/kske/eventbus/core/EventBus.java | 48 +++++++++++++++++-- .../dev/kske/eventbus/core/Polymorphic.java | 1 + .../java/dev/kske/eventbus/core/Priority.java | 1 + .../kske/eventbus/core/InheritanceTest.java | 40 ++++++++++++++++ .../dev/kske/eventbus/core/SimpleEvent.java | 26 +++++++++- .../core/SimpleEventListenerBase.java | 20 ++++++++ .../core/SimpleEventListenerInterface.java | 13 +++++ 8 files changed, 149 insertions(+), 6 deletions(-) create mode 100644 core/src/test/java/dev/kske/eventbus/core/InheritanceTest.java create mode 100644 core/src/test/java/dev/kske/eventbus/core/SimpleEventListenerBase.java create mode 100644 core/src/test/java/dev/kske/eventbus/core/SimpleEventListenerInterface.java diff --git a/README.md b/README.md index 1f78bcc..03f8a0d 100644 --- a/README.md +++ b/README.md @@ -221,6 +221,12 @@ The same applies when an exception event handler throws an exception. To avoid this, system events never cause system events and instead just issue a warning to the logger. +## Inheritance + +When a superclass or an interface of an event listener defines event handlers, they will be detected and registered by Event Bus, even if they are `private`. +If an event handler is overridden by the listener, the `@Event` annotation of the overridden method is automatically considered present on the overriding method. +If the overridden method contains an implementation, it is ignored as expected. + ## Debugging In more complex setups, taking a look at the event handler execution order can be helpful for debugging. 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 83bef93..7f40551 100644 --- a/core/src/main/java/dev/kske/eventbus/core/EventBus.java +++ b/core/src/main/java/dev/kske/eventbus/core/EventBus.java @@ -2,10 +2,12 @@ package dev.kske.eventbus.core; import java.lang.System.Logger; import java.lang.System.Logger.Level; -import java.lang.reflect.InvocationTargetException; +import java.lang.annotation.Annotation; +import java.lang.reflect.*; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Consumer; +import java.util.stream.Collectors; import dev.kske.eventbus.core.handler.*; @@ -14,9 +16,8 @@ import dev.kske.eventbus.core.handler.*; *

* A singleton instance of this class can be lazily created and acquired using the * {@link EventBus#getInstance()} method. - *

- * This is a thread-safe implementation. * + * @implNote This is a thread-safe implementation. * @author Kai S. K. Engelbart * @since 0.0.1 * @see Event @@ -237,7 +238,7 @@ public final class EventBus { priority = listener.getClass().getAnnotation(Priority.class).value(); registeredListeners.add(listener); - for (var method : listener.getClass().getDeclaredMethods()) { + for (var method : getHandlerMethods(listener.getClass())) { Event annotation = method.getAnnotation(Event.class); // Skip methods without annotations @@ -257,6 +258,45 @@ public final class EventBus { listener.getClass().getName()); } + /** + * Searches for event handling methods declared inside the inheritance hierarchy of an event + * listener. + * + * @param listenerClass the class to inspect + * @return all event handling methods defined for the given listener + * @since 1.3.0 + */ + private Set getHandlerMethods(Class listenerClass) { + + // Get methods declared by the listener + Set methods = getMethodsAnnotatedWith(listenerClass, Event.class); + + // Recursively add superclass handlers + if (listenerClass.getSuperclass() != null) + methods.addAll(getHandlerMethods(listenerClass.getSuperclass())); + + // Recursively add interface handlers + for (Class iClass : listenerClass.getInterfaces()) + methods.addAll(getHandlerMethods(iClass)); + + return methods; + } + + /** + * Searches for declared methods with a specific annotation inside a class. + * + * @param enclosingClass the class to inspect + * @param annotationClass the annotation to look for + * @return all methods matching the search criteria + * @since 1.3.0 + */ + private Set getMethodsAnnotatedWith(Class enclosingClass, + Class annotationClass) { + return Arrays.stream(enclosingClass.getDeclaredMethods()) + .filter(m -> m.isAnnotationPresent(annotationClass)) + .collect(Collectors.toSet()); + } + /** * Registers a callback listener, which is a consumer that is invoked when an event occurs. The * listener is not polymorphic and has the {@link #DEFAULT_PRIORITY}. diff --git a/core/src/main/java/dev/kske/eventbus/core/Polymorphic.java b/core/src/main/java/dev/kske/eventbus/core/Polymorphic.java index 1f77726..ec39121 100644 --- a/core/src/main/java/dev/kske/eventbus/core/Polymorphic.java +++ b/core/src/main/java/dev/kske/eventbus/core/Polymorphic.java @@ -18,6 +18,7 @@ import java.lang.annotation.*; * @see Event */ @Documented +@Inherited @Retention(RUNTIME) @Target({ METHOD, TYPE }) public @interface Polymorphic { diff --git a/core/src/main/java/dev/kske/eventbus/core/Priority.java b/core/src/main/java/dev/kske/eventbus/core/Priority.java index ca82fa3..e180e0f 100644 --- a/core/src/main/java/dev/kske/eventbus/core/Priority.java +++ b/core/src/main/java/dev/kske/eventbus/core/Priority.java @@ -21,6 +21,7 @@ import java.lang.annotation.*; * @see Event */ @Documented +@Inherited @Retention(RUNTIME) @Target({ METHOD, TYPE }) public @interface Priority { diff --git a/core/src/test/java/dev/kske/eventbus/core/InheritanceTest.java b/core/src/test/java/dev/kske/eventbus/core/InheritanceTest.java new file mode 100644 index 0000000..95c9eac --- /dev/null +++ b/core/src/test/java/dev/kske/eventbus/core/InheritanceTest.java @@ -0,0 +1,40 @@ +package dev.kske.eventbus.core; + +import static org.junit.jupiter.api.Assertions.assertSame; + +import org.junit.jupiter.api.Test; + +/** + * Tests whether event handlers correctly work in the context of an inheritance hierarchy. + * + * @author Kai S. K. Engelbart + * @since 1.3.0 + */ +class InheritanceTest extends SimpleEventListenerBase implements SimpleEventListenerInterface { + + EventBus bus = new EventBus(); + + @Test + void test() { + bus.registerListener(this); + var event = new SimpleEvent(); + + bus.dispatch(event); + assertSame(4, event.getCounter()); + } + + @Override + void onSimpleEventAbstractHandler(SimpleEvent event) { + event.increment(); + } + + @Override + public void onSimpleEventInterfaceHandler(SimpleEvent event) { + event.increment(); + } + + @Event + private void onSimpleEventPrivate(SimpleEvent event) { + event.increment(); + } +} 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 2baf080..1512107 100644 --- a/core/src/test/java/dev/kske/eventbus/core/SimpleEvent.java +++ b/core/src/test/java/dev/kske/eventbus/core/SimpleEvent.java @@ -1,9 +1,31 @@ package dev.kske.eventbus.core; /** - * A simple event for testing purposes. + * A simple event for testing purposes. The event contains a counter that is supposed to be + * incremented when the event is processed by a handler. That way it is possible to test whether all + * handlers that were supposed to be invoked were in fact invoked. * * @author Kai S. K. Engelbart * @since 0.0.1 */ -class SimpleEvent {} +class SimpleEvent { + + private int counter; + + @Override + public String toString() { + return String.format("SimpleEvent[%d]", counter); + } + + void increment() { + ++counter; + } + + int getCounter() { + return counter; + } + + void reset() { + counter = 0; + } +} diff --git a/core/src/test/java/dev/kske/eventbus/core/SimpleEventListenerBase.java b/core/src/test/java/dev/kske/eventbus/core/SimpleEventListenerBase.java new file mode 100644 index 0000000..7ae6de5 --- /dev/null +++ b/core/src/test/java/dev/kske/eventbus/core/SimpleEventListenerBase.java @@ -0,0 +1,20 @@ +package dev.kske.eventbus.core; + +/** + * An abstract class defining a package-private and a private handler for {@link SimpleEvent}. + * + * @author Kai S. K. Engelbart + * @since 1.3.0 + */ +abstract class SimpleEventListenerBase { + + @Event + void onSimpleEventAbstractHandler(SimpleEvent event) { + event.increment(); + } + + @Event + private void onSimpleEventPrivate(SimpleEvent event) { + event.increment(); + } +} diff --git a/core/src/test/java/dev/kske/eventbus/core/SimpleEventListenerInterface.java b/core/src/test/java/dev/kske/eventbus/core/SimpleEventListenerInterface.java new file mode 100644 index 0000000..a75e901 --- /dev/null +++ b/core/src/test/java/dev/kske/eventbus/core/SimpleEventListenerInterface.java @@ -0,0 +1,13 @@ +package dev.kske.eventbus.core; + +/** + * An interface defining a single handler for {@link SimpleEvent}. + * + * @author Kai S. K. Engelbart + * @since 1.3.0 + */ +interface SimpleEventListenerInterface { + + @Event + void onSimpleEventInterfaceHandler(SimpleEvent event); +} From 722bf2b999b39dba0d3df5da591df722d6864279 Mon Sep 17 00:00:00 2001 From: kske Date: Wed, 12 Jan 2022 15:59:45 +0100 Subject: [PATCH 2/2] Test priorities for inheritance --- README.md | 5 ++++- .../java/dev/kske/eventbus/core/EventBus.java | 15 +++++++++------ .../dev/kske/eventbus/core/InheritanceTest.java | 8 +++++--- .../eventbus/core/SimpleEventListenerBase.java | 7 ++++++- .../core/SimpleEventListenerInterface.java | 1 + 5 files changed, 25 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 03f8a0d..1ac86e1 100644 --- a/README.md +++ b/README.md @@ -225,7 +225,10 @@ To avoid this, system events never cause system events and instead just issue a When a superclass or an interface of an event listener defines event handlers, they will be detected and registered by Event Bus, even if they are `private`. If an event handler is overridden by the listener, the `@Event` annotation of the overridden method is automatically considered present on the overriding method. -If the overridden method contains an implementation, it is ignored as expected. +If the overridden method already contains an implementation in the superclass, the superclass implementation is ignored as expected. + +The `@Priority` and `@Polymorphic` annotations are inherited both on a class and on a method level. +If the priority or polymorphism has to be redefined on an inherited handler, the `@Event` annotation has to be added explicitly. ## Debugging 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 7f40551..334dd52 100644 --- a/core/src/main/java/dev/kske/eventbus/core/EventBus.java +++ b/core/src/main/java/dev/kske/eventbus/core/EventBus.java @@ -7,7 +7,6 @@ import java.lang.reflect.*; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Consumer; -import java.util.stream.Collectors; import dev.kske.eventbus.core.handler.*; @@ -272,8 +271,9 @@ public final class EventBus { Set methods = getMethodsAnnotatedWith(listenerClass, Event.class); // Recursively add superclass handlers - if (listenerClass.getSuperclass() != null) - methods.addAll(getHandlerMethods(listenerClass.getSuperclass())); + Class superClass = listenerClass.getSuperclass(); + if (superClass != null && superClass != Object.class) + methods.addAll(getHandlerMethods(superClass)); // Recursively add interface handlers for (Class iClass : listenerClass.getInterfaces()) @@ -292,9 +292,12 @@ public final class EventBus { */ private Set getMethodsAnnotatedWith(Class enclosingClass, Class annotationClass) { - return Arrays.stream(enclosingClass.getDeclaredMethods()) - .filter(m -> m.isAnnotationPresent(annotationClass)) - .collect(Collectors.toSet()); + var methods = new HashSet(); + for (var method : enclosingClass.getDeclaredMethods()) + if (method.isAnnotationPresent(annotationClass)) + methods.add(method); + + return methods; } /** diff --git a/core/src/test/java/dev/kske/eventbus/core/InheritanceTest.java b/core/src/test/java/dev/kske/eventbus/core/InheritanceTest.java index 95c9eac..b72438d 100644 --- a/core/src/test/java/dev/kske/eventbus/core/InheritanceTest.java +++ b/core/src/test/java/dev/kske/eventbus/core/InheritanceTest.java @@ -5,7 +5,8 @@ import static org.junit.jupiter.api.Assertions.assertSame; import org.junit.jupiter.api.Test; /** - * Tests whether event handlers correctly work in the context of an inheritance hierarchy. + * Tests whether event handlers correctly work in the context of an inheritance hierarchy. The + * effect of handler priorities is also accounted for. * * @author Kai S. K. Engelbart * @since 1.3.0 @@ -20,12 +21,12 @@ class InheritanceTest extends SimpleEventListenerBase implements SimpleEventList var event = new SimpleEvent(); bus.dispatch(event); - assertSame(4, event.getCounter()); + assertSame(3, event.getCounter()); } @Override void onSimpleEventAbstractHandler(SimpleEvent event) { - event.increment(); + assertSame(1, event.getCounter()); } @Override @@ -35,6 +36,7 @@ class InheritanceTest extends SimpleEventListenerBase implements SimpleEventList @Event private void onSimpleEventPrivate(SimpleEvent event) { + assertSame(0, event.getCounter()); event.increment(); } } diff --git a/core/src/test/java/dev/kske/eventbus/core/SimpleEventListenerBase.java b/core/src/test/java/dev/kske/eventbus/core/SimpleEventListenerBase.java index 7ae6de5..1540655 100644 --- a/core/src/test/java/dev/kske/eventbus/core/SimpleEventListenerBase.java +++ b/core/src/test/java/dev/kske/eventbus/core/SimpleEventListenerBase.java @@ -1,20 +1,25 @@ package dev.kske.eventbus.core; +import static org.junit.jupiter.api.Assertions.*; + /** * An abstract class defining a package-private and a private handler for {@link SimpleEvent}. * * @author Kai S. K. Engelbart * @since 1.3.0 */ +@Priority(200) abstract class SimpleEventListenerBase { @Event void onSimpleEventAbstractHandler(SimpleEvent event) { - event.increment(); + fail("This handler should not be invoked"); } + @Priority(150) @Event private void onSimpleEventPrivate(SimpleEvent event) { + assertSame(1, event.getCounter()); event.increment(); } } diff --git a/core/src/test/java/dev/kske/eventbus/core/SimpleEventListenerInterface.java b/core/src/test/java/dev/kske/eventbus/core/SimpleEventListenerInterface.java index a75e901..8766e34 100644 --- a/core/src/test/java/dev/kske/eventbus/core/SimpleEventListenerInterface.java +++ b/core/src/test/java/dev/kske/eventbus/core/SimpleEventListenerInterface.java @@ -8,6 +8,7 @@ package dev.kske.eventbus.core; */ interface SimpleEventListenerInterface { + @Priority(120) @Event void onSimpleEventInterfaceHandler(SimpleEvent event); }