Use dedicated ApplicationEventFactory interface in EventPublicationInterceptor

See gh-36072
This commit is contained in:
Juergen Hoeller
2025-12-25 11:25:54 +01:00
parent aeb0115605
commit 5b6c231788
2 changed files with 82 additions and 17 deletions
@@ -17,7 +17,6 @@
package org.springframework.context.event;
import java.lang.reflect.Constructor;
import java.util.function.Function;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
@@ -31,19 +30,21 @@ import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.util.Assert;
/**
* {@link MethodInterceptor Interceptor} that publishes an {@code ApplicationEvent}
* to all {@code ApplicationListeners} registered with an {@code ApplicationEventPublisher}.
* {@link MethodInterceptor Interceptor} that publishes an {@code ApplicationEvent} to
* all {@code ApplicationListeners} registered with an {@code ApplicationEventPublisher}
* after each <i>successful</i> method invocation.
*
* <p>Note that this interceptor is capable of publishing a custom event after each
* <i>successful</i> method invocation, configured via the
* {@link #setApplicationEventClass "applicationEventClass"} property. As of 7.0.3,
* you can configure a {@link #setApplicationEventFactory factory function} instead.
* you can configure a {@link #setApplicationEventFactory factory function} instead,
* implementing the primary {@link ApplicationEventFactory#onSuccess} method there.
*
* <p>As of 7.0.3, this interceptor publishes a {@link MethodFailureEvent} for
* every exception encountered from a method invocation. This can be conveniently
* <p>By default (as of 7.0.3), this interceptor publishes a {@link MethodFailureEvent}
* for every exception encountered from a method invocation. This can be conveniently
* tracked via an {@code ApplicationListener<MethodFailureEvent>} class or an
* {@code @EventListener(MethodFailureEvent.class)} method.
* {@code @EventListener(MethodFailureEvent.class)} method. The failure event can be
* customized through overriding the {@link ApplicationEventFactory#onFailure} method.
*
* @author Dmitriy Kopylenko
* @author Juergen Hoeller
@@ -57,13 +58,13 @@ import org.springframework.util.Assert;
public class EventPublicationInterceptor
implements MethodInterceptor, ApplicationEventPublisherAware, InitializingBean {
private @Nullable Function<MethodInvocation, ? extends ApplicationEvent> applicationEventFactory;
private ApplicationEventFactory applicationEventFactory = (invocation, returnValue) -> null;
private @Nullable ApplicationEventPublisher applicationEventPublisher;
/**
* Set the application event class to publish.
* Set the application event class to publish after each successful invocation.
* <p>The event class <b>must</b> have a constructor with a single
* {@code Object} argument for the event source. The interceptor
* will pass in the invoked object.
@@ -79,7 +80,8 @@ public class EventPublicationInterceptor
}
try {
Constructor<? extends ApplicationEvent> ctor = applicationEventClass.getConstructor(Object.class);
this.applicationEventFactory = (invocation -> BeanUtils.instantiateClass(ctor, invocation.getThis()));
this.applicationEventFactory = ((invocation, returnValue) ->
BeanUtils.instantiateClass(ctor, invocation.getThis()));
}
catch (NoSuchMethodException ex) {
throw new IllegalArgumentException("ApplicationEvent class [" +
@@ -89,12 +91,12 @@ public class EventPublicationInterceptor
/**
* Specify a factory function for {@link ApplicationEvent} instances built from a
* {@link MethodInvocation}, representing a <i>successful</i> method invocation.
* {@link MethodInvocation}, representing each <i>successful</i> method invocation.
* @since 7.0.3
* @see #setApplicationEventClass
*/
public void setApplicationEventFactory(Function<MethodInvocation, ? extends ApplicationEvent> factoryFunction) {
this.applicationEventFactory = factoryFunction;
public void setApplicationEventFactory(ApplicationEventFactory applicationEventFactory) {
this.applicationEventFactory = applicationEventFactory;
}
@Override
@@ -119,14 +121,52 @@ public class EventPublicationInterceptor
retVal = invocation.proceed();
}
catch (Throwable ex) {
this.applicationEventPublisher.publishEvent(new MethodFailureEvent(invocation, ex));
// Publish event after failed invocation.
ApplicationEvent event = this.applicationEventFactory.onFailure(invocation, ex);
if (event != null) {
this.applicationEventPublisher.publishEvent(event);
}
throw ex;
}
if (this.applicationEventFactory != null) {
this.applicationEventPublisher.publishEvent(this.applicationEventFactory.apply(invocation));
// Publish event after successful invocation.
ApplicationEvent event = this.applicationEventFactory.onSuccess(invocation, retVal);
if (event != null) {
this.applicationEventPublisher.publishEvent(event);
}
return retVal;
}
/**
* Callback interface for building an {@link ApplicationEvent} after a method invocation.
* @since 7.0.3
*/
@FunctionalInterface
public interface ApplicationEventFactory {
/**
* Build an {@link ApplicationEvent} for the given successful method invocation.
* <p>This is the primary method to implement since there is no such default event.
* This may also return {@code null} for not publishing an event on success at all.
* @param invocation the successful method invocation
* @param returnValue the value that the method returned, if any
* @return the event to publish, or {@code null} for none
*/
@Nullable ApplicationEvent onSuccess(MethodInvocation invocation, @Nullable Object returnValue);
/**
* Build an {@link ApplicationEvent} for the given failed method invocation.
* <p>The default implementation builds a common {@link MethodFailureEvent}.
* This can be overridden to build a custom event instead, or to return
* {@code null} for not publishing an event on failure at all.
* @param invocation the failed method invocation
* @param failure the exception thrown from the method
* @return the event to publish, or {@code null} for none
*/
default @Nullable ApplicationEvent onFailure(MethodInvocation invocation, Throwable failure) {
return new MethodFailureEvent(invocation, failure);
}
}
}
@@ -24,6 +24,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
import org.aopalliance.intercept.MethodInvocation;
import org.jspecify.annotations.Nullable;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
@@ -320,7 +321,7 @@ class ApplicationContextEventTests extends AbstractApplicationEventListenerTests
ApplicationContext ctx = mock();
EventPublicationInterceptor interceptor = new EventPublicationInterceptor();
interceptor.setApplicationEventFactory(inv -> new MyEvent(invocation.getThis()));
interceptor.setApplicationEventFactory((inv, retVal) -> new MyEvent(inv.getThis()));
interceptor.setApplicationEventPublisher(ctx);
interceptor.afterPropertiesSet();
@@ -344,6 +345,30 @@ class ApplicationContextEventTests extends AbstractApplicationEventListenerTests
verify(ctx).publishEvent(isA(MethodFailureEvent.class));
}
@Test
void testEventPublicationInterceptorWithCustomFailure() throws Throwable {
MethodInvocation invocation = mock();
ApplicationContext ctx = mock();
EventPublicationInterceptor interceptor = new EventPublicationInterceptor();
interceptor.setApplicationEventFactory(new EventPublicationInterceptor.ApplicationEventFactory() {
@Override
public ApplicationEvent onSuccess(MethodInvocation invocation, @Nullable Object returnValue) {
return new MyEvent(returnValue);
}
@Override
public ApplicationEvent onFailure(MethodInvocation invocation, Throwable failure) {
return new MyOtherEvent(failure);
}
});
interceptor.setApplicationEventPublisher(ctx);
interceptor.afterPropertiesSet();
given(invocation.proceed()).willThrow(new IllegalStateException());
assertThatIllegalStateException().isThrownBy(() -> interceptor.invoke(invocation));
verify(ctx).publishEvent(isA(MyOtherEvent.class));
}
@Test
void listenersInApplicationContext() {
StaticApplicationContext context = new StaticApplicationContext();