From 6a2cad4ae5874b72a9f9d7147e671b46d2258313 Mon Sep 17 00:00:00 2001 From: kske Date: Sun, 21 Feb 2021 10:36:06 +0100 Subject: [PATCH 1/2] Add ExceptionEvent An exception event wraps an event that caused an exception inside of an event handler while being dispatched and is then dispatched to dedicated handlers. --- .../java/dev/kske/eventbus/core/EventBus.java | 27 +++++--- .../dev/kske/eventbus/core/EventHandler.java | 12 ++-- .../kske/eventbus/core/ExceptionEvent.java | 47 ++++++++++++++ .../java/dev/kske/eventbus/core/DeadTest.java | 2 +- .../dev/kske/eventbus/core/ExceptionTest.java | 62 +++++++++++++++++++ 5 files changed, 138 insertions(+), 12 deletions(-) create mode 100644 event-bus-core/src/main/java/dev/kske/eventbus/core/ExceptionEvent.java create mode 100644 event-bus-core/src/test/java/dev/kske/eventbus/core/ExceptionTest.java 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; + } + } +} -- 2.45.2 From 7c3cd017de8e59235f1199e499a72931cd7d4855 Mon Sep 17 00:00:00 2001 From: kske Date: Sun, 21 Feb 2021 13:50:12 +0100 Subject: [PATCH 2/2] Add system events section to README --- README.md | 32 +++++++++++++++++++ .../java/dev/kske/eventbus/core/EventBus.java | 2 +- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 0f839c5..01fd4c6 100644 --- a/README.md +++ b/README.md @@ -123,6 +123,38 @@ This applies to all event handlers that would have been executed after the one c Avoid cancelling events while using multiple event handlers with the same priority. As event handlers are ordered by priority, it is not defined which of them will be executed after the event has been consumed. +## System Events + +To accommodate for special circumstances in an event distribution, system events have been introduced. +At the moment, there are two system events, which are explained in this section. + +### Detecting Unhandled Events + +When an event is dispatched but not delivered to any handler, a dead event is dispatched that wraps the original event. +You can declare a dead event handler to respond to this situation: + +```java +private void onDeadEvent(DeadEvent deadEvent) { ... } +``` + +### Detecting Exceptions Thrown by Event Handlers + +When an event handler throws an exception, an exception event is dispatched that wraps the original event. +A exception handler is declared as follows: + +```java +private void onExceptionEvent(ExceptionEvent ExceptionEvent) { ... } +``` + +Both system events reference the event bus that caused them and a warning is logged if they are unhandled. + +### What About Endless Recursion Caused By Dead Events and Exception Events? + +As one might imagine, an unhandled dead event would theoretically lead to an endless recursion. +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. + ## Installation Event Bus is available in Maven Central. 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 f1e54b2..a719135 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 @@ -89,7 +89,7 @@ public final class EventBus { if (event instanceof DeadEvent || event instanceof ExceptionEvent) // Warn about system event not being handled - logger.log(Level.WARNING, event + " not handled", e); + logger.log(Level.WARNING, event + " not handled due to exception", e); else // Dispatch exception event -- 2.45.2