diff --git a/spring-context/src/main/java/org/springframework/context/event/EventPublicationInterceptor.java b/spring-context/src/main/java/org/springframework/context/event/EventPublicationInterceptor.java index c24aef27a70..5643eaa30e1 100644 --- a/spring-context/src/main/java/org/springframework/context/event/EventPublicationInterceptor.java +++ b/spring-context/src/main/java/org/springframework/context/event/EventPublicationInterceptor.java @@ -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 successful method invocation. * *

Note that this interceptor is capable of publishing a custom event after each * successful 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. * - *

As of 7.0.3, this interceptor publishes a {@link MethodFailureEvent} for - * every exception encountered from a method invocation. This can be conveniently + *

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} 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 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. *

The event class must 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 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 successful method invocation. + * {@link MethodInvocation}, representing each successful method invocation. * @since 7.0.3 * @see #setApplicationEventClass */ - public void setApplicationEventFactory(Function 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. + *

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. + *

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); + } + } + } diff --git a/spring-context/src/test/java/org/springframework/context/event/ApplicationContextEventTests.java b/spring-context/src/test/java/org/springframework/context/event/ApplicationContextEventTests.java index 4f04a29ed8b..1f2f22efe80 100644 --- a/spring-context/src/test/java/org/springframework/context/event/ApplicationContextEventTests.java +++ b/spring-context/src/test/java/org/springframework/context/event/ApplicationContextEventTests.java @@ -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();