diff --git a/event-bus-core/src/main/java/dev/kske/eventbus/core/EventBus.java b/event-bus-core/src/main/java/dev/kske/eventbus/core/EventBus.java index 88317c6..f1e54b2 100644 --- a/event-bus-core/src/main/java/dev/kske/eventbus/core/EventBus.java +++ b/event-bus-core/src/main/java/dev/kske/eventbus/core/EventBus.java @@ -2,6 +2,7 @@ package dev.kske.eventbus.core; import java.lang.System.Logger; import java.lang.System.Logger.Level; +import java.lang.reflect.InvocationTargetException; import java.util.*; import java.util.concurrent.ConcurrentHashMap; @@ -63,6 +64,7 @@ public final class EventBus { * priority. * * @param event the event to dispatch + * @throws EventBusException if an event handler isn't accessible or has an invalid signature * @since 0.0.1 */ public void dispatch(Object event) { @@ -81,16 +83,27 @@ public final class EventBus { state.isCancelled = false; break; } else { - handlers.next().execute(event); + try { + handlers.next().execute(event); + } catch (InvocationTargetException e) { + if (event instanceof DeadEvent || event instanceof ExceptionEvent) + + // Warn about system event not being handled + logger.log(Level.WARNING, event + " not handled", e); + else + + // Dispatch exception event + dispatch(new ExceptionEvent(this, event, e.getCause())); + } } - } else if (!(event instanceof DeadEvent)) { - - // Dispatch dead event - dispatch(new DeadEvent(this, event)); - } else { - + } else if (event instanceof DeadEvent || event instanceof ExceptionEvent) { + // Warn about the dead event not being handled logger.log(Level.WARNING, "{0} not handled", event); + } else { + + // Dispatch dead event + dispatch(new DeadEvent(this, event)); } // Reset dispatch state diff --git a/event-bus-core/src/main/java/dev/kske/eventbus/core/EventHandler.java b/event-bus-core/src/main/java/dev/kske/eventbus/core/EventHandler.java index 4c89174..60e7e85 100644 --- a/event-bus-core/src/main/java/dev/kske/eventbus/core/EventHandler.java +++ b/event-bus-core/src/main/java/dev/kske/eventbus/core/EventHandler.java @@ -91,17 +91,21 @@ final class EventHandler implements Comparable { * Executes the event handler. * * @param event the event used as the method parameter - * @throws EventBusException if the handler throws an exception + * @throws EventBusException if the event handler isn't accessible or has an invalid + * signature + * @throws InvocationTargetException if the handler throws an exception * @since 0.0.1 */ - void execute(Object event) throws EventBusException { + void execute(Object event) throws EventBusException, InvocationTargetException { try { if (useParameter) method.invoke(listener, event); else method.invoke(listener); - } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { - throw new EventBusException("Failed to invoke event handler!", e); + } catch (IllegalArgumentException e) { + throw new EventBusException("Event handler rejected target / argument!", e); + } catch (IllegalAccessException e) { + throw new EventBusException("Event handler is not accessible!", e); } } diff --git a/event-bus-core/src/main/java/dev/kske/eventbus/core/ExceptionEvent.java b/event-bus-core/src/main/java/dev/kske/eventbus/core/ExceptionEvent.java new file mode 100644 index 0000000..b89784e --- /dev/null +++ b/event-bus-core/src/main/java/dev/kske/eventbus/core/ExceptionEvent.java @@ -0,0 +1,47 @@ +package dev.kske.eventbus.core; + +/** + * Wraps an event that was dispatched but caused an exception in one of its handlers. + *

+ * Handling exception events is useful as it allows the creation of a centralized exception handling + * mechanism for unexpected exceptions. + * + * @author Kai S. K. Engelbart + * @since 1.1.0 + */ +public final class ExceptionEvent { + + private final EventBus eventBus; + private final Object event; + private final Throwable cause; + + ExceptionEvent(EventBus eventBus, Object event, Throwable cause) { + this.eventBus = eventBus; + this.event = event; + this.cause = cause; + } + + @Override + public String toString() { + return String.format("ExceptionEvent[eventBus=%s, event=%s, cause=%s]", eventBus, event, + cause); + } + + /** + * @return the event bus that dispatched this event + * @since 1.1.0 + */ + public EventBus getEventBus() { return eventBus; } + + /** + * @return the event that could not be handled because of an exception + * @since 1.1.0 + */ + public Object getEvent() { return event; } + + /** + * @return the exception that was thrown while handling the event + * @since 1.1.0 + */ + public Throwable getCause() { return cause; } +} diff --git a/event-bus-core/src/test/java/dev/kske/eventbus/core/DeadTest.java b/event-bus-core/src/test/java/dev/kske/eventbus/core/DeadTest.java index 50e1986..d4efa95 100644 --- a/event-bus-core/src/test/java/dev/kske/eventbus/core/DeadTest.java +++ b/event-bus-core/src/test/java/dev/kske/eventbus/core/DeadTest.java @@ -31,7 +31,7 @@ class DeadTest { /** * Tests how the event bus reacts to an unhandled dead event. This should not lead to an - * exception or endless recursion and instead be logged. + * exception or an endless recursion and should be logged instead. * * @since 1.1.0 */ diff --git a/event-bus-core/src/test/java/dev/kske/eventbus/core/ExceptionTest.java b/event-bus-core/src/test/java/dev/kske/eventbus/core/ExceptionTest.java new file mode 100644 index 0000000..e862036 --- /dev/null +++ b/event-bus-core/src/test/java/dev/kske/eventbus/core/ExceptionTest.java @@ -0,0 +1,62 @@ +package dev.kske.eventbus.core; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +/** + * Tests the dispatching of an exception event if an event handler threw an exception. + * + * @author Kai S. K. Engelbart + * @since 1.1.0 + */ +class ExceptionTest { + + EventBus bus = new EventBus(); + String event = "This event will cause an exception"; + RuntimeException exception = new RuntimeException("I failed"); + boolean exceptionEventHandled; + + /** + * Tests exception event delivery. + * + * @since 1.1.0 + */ + @Test + void testExceptionEvent() { + bus.registerListener(this); + bus.registerListener(new ExceptionListener()); + bus.dispatch(event); + assertTrue(exceptionEventHandled); + bus.clearListeners(); + } + + /** + * Tests how the event bus reacts to an unhandled exception event. This should not lead to an + * exception or an endless recursion and should be logged instead. + * + * @since 1.1.0 + */ + @Test + void testUnhandledExceptionEvent() { + bus.registerListener(this); + bus.dispatch(event); + bus.removeListener(this); + } + + @Event(String.class) + void onString() { + throw exception; + } + + class ExceptionListener { + + @Event + void onExceptionEvent(ExceptionEvent exceptionEvent) { + assertEquals(bus, exceptionEvent.getEventBus()); + assertEquals(event, exceptionEvent.getEvent()); + assertEquals(exception, exceptionEvent.getCause()); + exceptionEventHandled = true; + } + } +}