Merge pull request 'Exception Wrapper' (#32) from f/exception-wrapper into develop

Reviewed-on: https://git.kske.dev/kske/event-bus/pulls/32
Reviewed-by: delvh <leon@kske.dev>
Reviewed-by: DieGurke <maxi@kske.dev>
This commit is contained in:
Kai S. K. Engelbart 2022-01-08 16:54:05 +01:00
commit 27d14a844d
Signed by: Käfer & Engelbart Git
GPG Key ID: 70F2F9206EDC1FCE
5 changed files with 92 additions and 5 deletions

View File

@ -183,14 +183,37 @@ private void onDeadEvent(DeadEvent deadEvent) { ... }
### Detecting Exceptions Thrown by Event Handlers ### Detecting Exceptions Thrown by Event Handlers
When an event handler throws an exception, an exception event is dispatched that wraps the original event. When an event handler throws an exception, an exception event is dispatched that wraps the original event.
A exception handler is declared as follows: An exception handler is declared as follows:
```java ```java
private void onExceptionEvent(ExceptionEvent ExceptionEvent) { ... } private void onExceptionEvent(ExceptionEvent ExceptionEvent) { ... }
``` ```
Both system events reference the event bus that caused them and a warning is logged if they are unhandled. Both system events reference the event bus that caused them and a warning is logged if they are unhandled.
#### Yeeting Exceptions Out of an Event Handler
In some cases, a warning about an `Exception` that was thrown in an event handler is not enough, stays unnoticed, or an exception should be catched explicitly.
Event Bus explicitly dispatches no `ExceptionEvent` when an `ExceptionWrapper` exception is thrown and instead simply rethrows it.
`ExceptionWrapper` is an unchecked exception that (as the name says) simply wraps an exception that caused it.
This means the following is possible and results in a normal program exit:
```java
@Event(String.class)
void onString() {
throw new ExceptionWrapper(new RuntimeException("I failed!"));
}
void helloStackTrace() {
EventBus.getInstance().registerListener(this);
try {
EventBus.getInstance().dispatch("A string!");
System.exit(-1);
} catch(ExceptionWrapper e) {
e.getCause().printStackTrace();
System.exit(0);
}
}
```
### What About Endless Recursion Caused By Dead Events and Exception Events? ### 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. As one might imagine, an unhandled dead event would theoretically lead to an endless recursion.

View File

@ -112,10 +112,11 @@ public final class EventBus {
* *
* @param event the event to dispatch * @param event the event to dispatch
* @throws EventBusException if an event handler isn't accessible or has an invalid signature * @throws EventBusException if an event handler isn't accessible or has an invalid signature
* @throws ExceptionWrapper if it is thrown by an event handler
* @throws NullPointerException if the specified event is {@code null} * @throws NullPointerException if the specified event is {@code null}
* @since 0.0.1 * @since 0.0.1
*/ */
public void dispatch(Object event) throws EventBusException { public void dispatch(Object event) {
Objects.requireNonNull(event); Objects.requireNonNull(event);
logger.log(Level.INFO, "Dispatching event {0}", event); logger.log(Level.INFO, "Dispatching event {0}", event);
@ -140,6 +141,10 @@ public final class EventBus {
// Transparently pass error to the caller // Transparently pass error to the caller
throw (Error) e.getCause(); throw (Error) e.getCause();
else if (e.getCause() instanceof ExceptionWrapper)
// Transparently pass exception wrapper to the caller
throw (ExceptionWrapper) e.getCause();
else if (event instanceof DeadEvent || event instanceof ExceptionEvent) else if (event instanceof DeadEvent || event instanceof ExceptionEvent)
// Warn about system event not being handled // Warn about system event not being handled
@ -214,7 +219,7 @@ public final class EventBus {
* @since 0.0.1 * @since 0.0.1
* @see Event * @see Event
*/ */
public void registerListener(Object listener) throws EventBusException { public void registerListener(Object listener) {
Objects.requireNonNull(listener); Objects.requireNonNull(listener);
if (registeredListeners.contains(listener)) if (registeredListeners.contains(listener))
throw new EventBusException(listener + " already registered!"); throw new EventBusException(listener + " already registered!");

View File

@ -14,13 +14,14 @@ package dev.kske.eventbus.core;
*/ */
public final class EventBusException extends RuntimeException { public final class EventBusException extends RuntimeException {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 7254445250300604449L;
/** /**
* Creates a new event bus exception. * Creates a new event bus exception.
* *
* @param message the message to display * @param message the message to display
* @param cause the cause of this exception * @param cause the cause of this exception
* @since 0.0.1
*/ */
public EventBusException(String message, Throwable cause) { public EventBusException(String message, Throwable cause) {
super(message, cause); super(message, cause);
@ -30,6 +31,7 @@ public final class EventBusException extends RuntimeException {
* Creates a new event bus exception. * Creates a new event bus exception.
* *
* @param message the message to display * @param message the message to display
* @since 0.0.1
*/ */
public EventBusException(String message) { public EventBusException(String message) {
super(message); super(message);

View File

@ -0,0 +1,24 @@
package dev.kske.eventbus.core;
/**
* This unchecked exception acts as a wrapper for an arbitrary exception to prevent an
* {@link ExceptionEvent} from being dispatched. Instead, the wrapped exception is rethrown by
* {@link EventBus#dispatch(Object)}.
*
* @author Kai S. K. Engelbart
* @since 1.2.1
*/
public final class ExceptionWrapper extends RuntimeException {
private static final long serialVersionUID = -2016681140617308788L;
/**
* Creates a new exception wrapper.
*
* @param cause the exception to wrap
* @since 1.2.1
*/
public ExceptionWrapper(Exception cause) {
super(cause);
}
}

View File

@ -0,0 +1,33 @@
package dev.kske.eventbus.core;
import static org.junit.jupiter.api.Assertions.assertThrows;
import org.junit.jupiter.api.Test;
/**
* Tests the behavior of the event bus when an {@link ExceptionWrapper} is thrown.
*
* @author Kai S. K. Engelbart
* @since 1.2.1
*/
public class ExceptionWrapperTest {
EventBus bus = new EventBus();
String event = "This event will cause an exception";
/**
* Tests transparent rethrowing of an exception wrapper by {@link EventBus#dispatch(Object)}.
*
* @since 1.2.1
*/
@Test
public void testExceptionWrapper() {
bus.registerListener(this);
assertThrows(ExceptionWrapper.class, () -> bus.dispatch(event));
}
@Event(String.class)
void onString() {
throw new ExceptionWrapper(new RuntimeException("I failed!"));
}
}