Inherit event handlers
All checks were successful
zdm/event-bus/pipeline/head This commit looks good

When registering an event listener, Event Bus recursively walks the
entire inheritance tree and looks for event handlers.
This commit is contained in:
Kai S. K. Engelbart 2022-01-09 14:16:30 +01:00
parent c5607d12ae
commit 7fb633d69f
Signed by: kske
GPG Key ID: 8BEB13EC5DF7EF13
8 changed files with 149 additions and 6 deletions

View File

@ -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.

View File

@ -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.*;
* <p>
* A singleton instance of this class can be lazily created and acquired using the
* {@link EventBus#getInstance()} method.
* <p>
* 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<Method> getHandlerMethods(Class<?> listenerClass) {
// Get methods declared by the listener
Set<Method> 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<Method> getMethodsAnnotatedWith(Class<?> enclosingClass,
Class<? extends Annotation> 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}.

View File

@ -18,6 +18,7 @@ import java.lang.annotation.*;
* @see Event
*/
@Documented
@Inherited
@Retention(RUNTIME)
@Target({ METHOD, TYPE })
public @interface Polymorphic {

View File

@ -21,6 +21,7 @@ import java.lang.annotation.*;
* @see Event
*/
@Documented
@Inherited
@Retention(RUNTIME)
@Target({ METHOD, TYPE })
public @interface Priority {

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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);
}