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
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
private void onExceptionEvent(ExceptionEvent ExceptionEvent) { ... }
```
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?
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
* @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}
* @since 0.0.1
*/
public void dispatch(Object event) throws EventBusException {
public void dispatch(Object event) {
Objects.requireNonNull(event);
logger.log(Level.INFO, "Dispatching event {0}", event);
@ -140,6 +141,10 @@ public final class EventBus {
// Transparently pass error to the caller
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)
// Warn about system event not being handled
@ -214,7 +219,7 @@ public final class EventBus {
* @since 0.0.1
* @see Event
*/
public void registerListener(Object listener) throws EventBusException {
public void registerListener(Object listener) {
Objects.requireNonNull(listener);
if (registeredListeners.contains(listener))
throw new EventBusException(listener + " already registered!");

View File

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