Allow EventListenerSupport to handle (and ignore) exception from (#1167)

listeners allowing invocation of all listeners.

This is an alternative to
https://github.com/apache/commons-lang/pull/1156

Co-authored-by: Gary Gregory <gardgregory@gmail.com>
This commit is contained in:
Gary Gregory 2024-01-30 13:34:19 -05:00 committed by GitHub
parent 2bdecd58af
commit f962e7b654
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 71 additions and 3 deletions

View File

@ -33,6 +33,8 @@ import java.util.concurrent.CopyOnWriteArrayList;
import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.Validate; import org.apache.commons.lang3.Validate;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.commons.lang3.function.FailableConsumer;
/** /**
* An EventListenerSupport object can be used to manage a list of event * An EventListenerSupport object can be used to manage a list of event
@ -74,6 +76,25 @@ public class EventListenerSupport<L> implements Serializable {
*/ */
protected class ProxyInvocationHandler implements InvocationHandler { protected class ProxyInvocationHandler implements InvocationHandler {
private final FailableConsumer<Throwable, IllegalAccessException> handler;
/**
* Constructs a new instance.
*/
public ProxyInvocationHandler() {
this(ExceptionUtils::rethrow);
}
/**
* Constructs a new instance.
*
* @param handler Handles Throwables.
* @since 3.15.0
*/
public ProxyInvocationHandler(FailableConsumer<Throwable, IllegalAccessException> handler) {
this.handler = Objects.requireNonNull(handler);
}
/** /**
* Propagates the method call to all registered listeners in place of the proxy listener object. * Propagates the method call to all registered listeners in place of the proxy listener object.
* *
@ -89,10 +110,27 @@ public class EventListenerSupport<L> implements Serializable {
public Object invoke(final Object unusedProxy, final Method method, final Object[] args) public Object invoke(final Object unusedProxy, final Method method, final Object[] args)
throws IllegalAccessException, IllegalArgumentException, InvocationTargetException { throws IllegalAccessException, IllegalArgumentException, InvocationTargetException {
for (final L listener : listeners) { for (final L listener : listeners) {
method.invoke(listener, args); try {
method.invoke(listener, args);
} catch (final Throwable t) {
handle(t);
}
} }
return null; return null;
} }
/**
* Handles an exception thrown by a listener. By default rethrows the given Throwable.
*
* @param t The Throwable
* @throws IllegalAccessException thrown by the listener.
* @throws IllegalArgumentException thrown by the listener.
* @throws InvocationTargetException thrown by the listener.
* @since 3.15.0
*/
protected void handle(final Throwable t) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException {
handler.accept(t);
}
} }
/** Serialization version */ /** Serialization version */

View File

@ -30,6 +30,7 @@ import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.ObjectInputStream; import java.io.ObjectInputStream;
import java.io.ObjectOutputStream; import java.io.ObjectOutputStream;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException; import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.lang.reflect.UndeclaredThrowableException; import java.lang.reflect.UndeclaredThrowableException;
@ -41,6 +42,7 @@ import java.util.function.Function;
import org.apache.commons.lang3.AbstractLangTest; import org.apache.commons.lang3.AbstractLangTest;
import org.apache.commons.lang3.exception.ExceptionUtils; import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.commons.lang3.function.FailableConsumer;
import org.easymock.EasyMock; import org.easymock.EasyMock;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@ -233,14 +235,15 @@ public class EventListenerSupportTest extends AbstractLangTest {
final AtomicInteger count = new AtomicInteger(); final AtomicInteger count = new AtomicInteger();
final EventListenerSupport<VetoableChangeListener> listenerSupport = EventListenerSupport.create(VetoableChangeListener.class); final EventListenerSupport<VetoableChangeListener> listenerSupport = EventListenerSupport.create(VetoableChangeListener.class);
final int vetoLimit = 1; final int vetoLimit = 1;
for (int i = 0; i < 10; ++i) { final int listenerCount = 10;
for (int i = 0; i < listenerCount; ++i) {
listenerSupport.addListener(evt -> { listenerSupport.addListener(evt -> {
if (count.incrementAndGet() > vetoLimit) { if (count.incrementAndGet() > vetoLimit) {
throw new PropertyVetoException(count.toString(), evt); throw new PropertyVetoException(count.toString(), evt);
} }
}); });
} }
assertEquals(10, listenerSupport.getListenerCount()); assertEquals(listenerCount, listenerSupport.getListenerCount());
assertEquals(0, count.get()); assertEquals(0, count.get());
final Exception e = assertThrows(UndeclaredThrowableException.class, final Exception e = assertThrows(UndeclaredThrowableException.class,
() -> listenerSupport.fire().vetoableChange(new PropertyChangeEvent(new Date(), "Day", 0, 1))); () -> listenerSupport.fire().vetoableChange(new PropertyChangeEvent(new Date(), "Day", 0, 1)));
@ -249,4 +252,31 @@ public class EventListenerSupportTest extends AbstractLangTest {
assertEquals(vetoLimit + 1, count.get()); assertEquals(vetoLimit + 1, count.get());
} }
/**
* Tests that throwing an exception from a listener continues calling the remaining listeners.
*/
@Test
public void testThrowingListenerContinues() throws PropertyVetoException {
final AtomicInteger count = new AtomicInteger();
final EventListenerSupport<VetoableChangeListener> listenerSupport = new EventListenerSupport<VetoableChangeListener>(VetoableChangeListener.class) {
@Override
protected InvocationHandler createInvocationHandler() {
return new ProxyInvocationHandler(FailableConsumer.nop());
}
};
final int vetoLimit = 1;
final int listenerCount = 10;
for (int i = 0; i < listenerCount; ++i) {
listenerSupport.addListener(evt -> {
if (count.incrementAndGet() > vetoLimit) {
throw new PropertyVetoException(count.toString(), evt);
}
});
}
assertEquals(listenerCount, listenerSupport.getListenerCount());
assertEquals(0, count.get());
listenerSupport.fire().vetoableChange(new PropertyChangeEvent(new Date(), "Day", 0, 1));
assertEquals(listenerCount, count.get());
}
} }