commons-lang
commons-lang copied to clipboard
EventListenerSupport - Allow firing events "quietly"
The current EventListenerSupport
class by default fires events in a fail-fast manner. When a listener throws an exception it prevents any further listeners from being called and propagates an InvocationTargetException
or UndeclaredThrowableException
[^1] out to the caller.
While that logic is appropriate for some applications, there is sometimes a desire to prevent listeners from interfering with one another, particularly in a more decoupled project.
This change allows the caller to use the alternative fireQuietly()
method to retrieve the proxy. This proxy catches and hides all InvocationTargetExceptions from the java.lang.reflect.Method.invoke(...)
method, which effectively silences exceptions from listener invocations, guaranteeing that all listeners receive the event.
See test below for basic operation:
@Test
public void testQuietInvocationHandling() throws Throwable {
final EventListenerSupport<ExceptionThrowingListener> listenerSupport = EventListenerSupport.create(ExceptionThrowingListener.class);
listenerSupport.addListener(new ExceptionThrowingListener() {
public void declaredError() throws Error {
throw new Error();
}
public void declaredRuntime() throws RuntimeException {
throw new RuntimeException();
}
public void declaredThrowable() throws Throwable {
throw new Throwable();
}
public void declaredIo() throws IOException {
throw new IOException();
}
public void declaredException() throws Exception {
throw new Exception();
}
public void undeclaredRuntime() {
throw new RuntimeException();
}
public void undeclaredNotImplemented() {
throw new NotImplementedException();
}
});
listenerSupport.fireQuietly().declaredError();
listenerSupport.fireQuietly().declaredRuntime();
listenerSupport.fireQuietly().declaredThrowable();
listenerSupport.fireQuietly().declaredIo();
listenerSupport.fireQuietly().declaredException();
listenerSupport.fireQuietly().undeclaredRuntime();
listenerSupport.fireQuietly().undeclaredNotImplemented();
assertThrows(UndeclaredThrowableException.class, () -> listenerSupport.fire().declaredError());
assertThrows(UndeclaredThrowableException.class, () -> listenerSupport.fire().declaredRuntime());
assertThrows(InvocationTargetException.class, () -> listenerSupport.fire().declaredThrowable());
assertThrows(UndeclaredThrowableException.class, () -> listenerSupport.fire().declaredIo());
assertThrows(InvocationTargetException.class, () -> listenerSupport.fire().declaredException());
assertThrows(UndeclaredThrowableException.class, () -> listenerSupport.fire().undeclaredRuntime());
assertThrows(UndeclaredThrowableException.class, () -> listenerSupport.fire().undeclaredNotImplemented());
}
Other considerations...
Technically it would be possible to implement the exception handling logic in different ways:
- The above proposal, which hides all exceptions from the caller.
- Collect all exceptions from the listeners and return those somehow to the caller.
- Throw the first encountered exception, after all listeners have received the event
All of those have downsides, but IMO the current code offers very little in the way of exception handling to begin with - the caller doesnt know which listener produced the exception, and must also be prepared to handle the unusual exception wrapping logic. So the tradeoff is pretty break even. Additionally, 2-3 aren't really "quiet" so if those are offered then might need a new term e.g. fireCompletely or fireAll.
[^1]: I'm not exactly sure why the Proxy wraps an UndeclaredThrowableException around the InvocationTargetException for some exceptions even when they are declared. The docs for java.lang.reflect.Proxy
seem to indicate that it wraps only checked exceptions with UndeclaredThrowableException, however in the above code you can see that some checked exceptions are wrapped, even if they are declared, some are not, and unchecked exceptions appear to always be wrapped. This oddness is not specific to this change, and exists in the current implementation, so it's not something that should affect this change.