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 c327a390cd0..c24aef27a70 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,11 +17,13 @@ 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; import org.jspecify.annotations.Nullable; +import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.InitializingBean; import org.springframework.context.ApplicationEvent; import org.springframework.context.ApplicationEventPublisher; @@ -29,14 +31,19 @@ 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} after each - * successful method invocation. + * {@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 only capable of publishing stateless - * events configured via the - * {@link #setApplicationEventClass "applicationEventClass"} property. + *

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

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. * * @author Dmitriy Kopylenko * @author Juergen Hoeller @@ -50,7 +57,7 @@ import org.springframework.util.Assert; public class EventPublicationInterceptor implements MethodInterceptor, ApplicationEventPublisherAware, InitializingBean { - private @Nullable Constructor applicationEventClassConstructor; + private @Nullable Function applicationEventFactory; private @Nullable ApplicationEventPublisher applicationEventPublisher; @@ -63,14 +70,16 @@ public class EventPublicationInterceptor * @throws IllegalArgumentException if the supplied {@code Class} is * {@code null} or if it is not an {@code ApplicationEvent} subclass or * if it does not expose a constructor that takes a single {@code Object} argument + * @see #setApplicationEventFactory */ - public void setApplicationEventClass(Class applicationEventClass) { + public void setApplicationEventClass(Class applicationEventClass) { if (ApplicationEvent.class == applicationEventClass || !ApplicationEvent.class.isAssignableFrom(applicationEventClass)) { throw new IllegalArgumentException("'applicationEventClass' needs to extend ApplicationEvent"); } try { - this.applicationEventClassConstructor = applicationEventClass.getConstructor(Object.class); + Constructor ctor = applicationEventClass.getConstructor(Object.class); + this.applicationEventFactory = (invocation -> BeanUtils.instantiateClass(ctor, invocation.getThis())); } catch (NoSuchMethodException ex) { throw new IllegalArgumentException("ApplicationEvent class [" + @@ -78,6 +87,16 @@ public class EventPublicationInterceptor } } + /** + * Specify a factory function for {@link ApplicationEvent} instances built from a + * {@link MethodInvocation}, representing a successful method invocation. + * @since 7.0.3 + * @see #setApplicationEventClass + */ + public void setApplicationEventFactory(Function factoryFunction) { + this.applicationEventFactory = factoryFunction; + } + @Override public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { this.applicationEventPublisher = applicationEventPublisher; @@ -85,23 +104,28 @@ public class EventPublicationInterceptor @Override public void afterPropertiesSet() throws Exception { - if (this.applicationEventClassConstructor == null) { - throw new IllegalArgumentException("Property 'applicationEventClass' is required"); + if (this.applicationEventPublisher == null) { + throw new IllegalArgumentException("Property 'applicationEventPublisher' is required"); } } @Override public @Nullable Object invoke(MethodInvocation invocation) throws Throwable { - Object retVal = invocation.proceed(); - - Assert.state(this.applicationEventClassConstructor != null, "No ApplicationEvent class set"); - ApplicationEvent event = (ApplicationEvent) - this.applicationEventClassConstructor.newInstance(invocation.getThis()); - Assert.state(this.applicationEventPublisher != null, "No ApplicationEventPublisher available"); - this.applicationEventPublisher.publishEvent(event); + Object retVal; + try { + retVal = invocation.proceed(); + } + catch (Throwable ex) { + this.applicationEventPublisher.publishEvent(new MethodFailureEvent(invocation, ex)); + throw ex; + } + + if (this.applicationEventFactory != null) { + this.applicationEventPublisher.publishEvent(this.applicationEventFactory.apply(invocation)); + } return retVal; } diff --git a/spring-context/src/main/java/org/springframework/context/event/MethodFailureEvent.java b/spring-context/src/main/java/org/springframework/context/event/MethodFailureEvent.java new file mode 100644 index 00000000000..ae1c0d5a765 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/event/MethodFailureEvent.java @@ -0,0 +1,79 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.event; + +import java.lang.reflect.Method; + +import org.aopalliance.intercept.MethodInvocation; + +import org.springframework.context.ApplicationEvent; +import org.springframework.util.ClassUtils; + +/** + * Event indicating a method invocation that failed. + * + * @author Juergen Hoeller + * @since 7.0.3 + * @see EventPublicationInterceptor + */ +@SuppressWarnings("serial") +public class MethodFailureEvent extends ApplicationEvent { + + private final Throwable failure; + + + /** + * Create a new event for the given method invocation. + * @param invocation the method invocation + * @param failure the exception encountered + */ + public MethodFailureEvent(MethodInvocation invocation, Throwable failure) { + super(invocation); + this.failure = failure; + } + + + /** + * Return the method invocation that triggered this event. + */ + @Override + public MethodInvocation getSource() { + return (MethodInvocation) super.getSource(); + } + + /** + * Return the method that triggered this event. + */ + public Method getMethod() { + return getSource().getMethod(); + } + + /** + * Return the exception encountered. + */ + public Throwable getFailure() { + return this.failure; + } + + + @Override + public String toString() { + return getClass().getSimpleName() + ": " + ClassUtils.getQualifiedMethodName(getMethod()) + + " [" + getFailure() + "]"; + } + +} diff --git a/spring-context/src/main/java/org/springframework/resilience/retry/MethodRetryEvent.java b/spring-context/src/main/java/org/springframework/resilience/retry/MethodRetryEvent.java index 8bbc3d1c097..80e382f24b6 100644 --- a/spring-context/src/main/java/org/springframework/resilience/retry/MethodRetryEvent.java +++ b/spring-context/src/main/java/org/springframework/resilience/retry/MethodRetryEvent.java @@ -16,11 +16,9 @@ package org.springframework.resilience.retry; -import java.lang.reflect.Method; - import org.aopalliance.intercept.MethodInvocation; -import org.springframework.context.ApplicationEvent; +import org.springframework.context.event.MethodFailureEvent; import org.springframework.util.ClassUtils; /** @@ -36,9 +34,7 @@ import org.springframework.util.ClassUtils; * @see org.springframework.context.event.EventListener */ @SuppressWarnings("serial") -public class MethodRetryEvent extends ApplicationEvent { - - private final Throwable failure; +public class MethodRetryEvent extends MethodFailureEvent { private final boolean retryAborted; @@ -50,27 +46,11 @@ public class MethodRetryEvent extends ApplicationEvent { * @param retryAborted whether the current failure led to the retry execution getting aborted */ public MethodRetryEvent(MethodInvocation invocation, Throwable failure, boolean retryAborted) { - super(invocation); - this.failure = failure; + super(invocation, failure); this.retryAborted = retryAborted; } - /** - * Return the method invocation that triggered this event. - */ - @Override - public MethodInvocation getSource() { - return (MethodInvocation) super.getSource(); - } - - /** - * Return the method that triggered this event. - */ - public Method getMethod() { - return getSource().getMethod(); - } - /** * Return the exception encountered. *

This may be an exception thrown by the method or emitted by the reactive @@ -87,7 +67,7 @@ public class MethodRetryEvent extends ApplicationEvent { * @see java.util.concurrent.TimeoutException */ public Throwable getFailure() { - return this.failure; + return super.getFailure(); } /** 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 d4dfafd9fc4..4f04a29ed8b 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 @@ -59,6 +59,7 @@ import org.springframework.scheduling.support.TaskUtils; import org.springframework.util.ReflectionUtils; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; import static org.assertj.core.api.Assertions.assertThatRuntimeException; import static org.mockito.ArgumentMatchers.isA; import static org.mockito.BDDMockito.given; @@ -298,7 +299,7 @@ class ApplicationContextEventTests extends AbstractApplicationEventListenerTests } @Test - void testEventPublicationInterceptor() throws Throwable { + void testEventPublicationInterceptorWithEventClass() throws Throwable { MethodInvocation invocation = mock(); ApplicationContext ctx = mock(); @@ -313,6 +314,36 @@ class ApplicationContextEventTests extends AbstractApplicationEventListenerTests verify(ctx).publishEvent(isA(MyEvent.class)); } + @Test + void testEventPublicationInterceptorWithEventFactory() throws Throwable { + MethodInvocation invocation = mock(); + ApplicationContext ctx = mock(); + + EventPublicationInterceptor interceptor = new EventPublicationInterceptor(); + interceptor.setApplicationEventFactory(inv -> new MyEvent(invocation.getThis())); + interceptor.setApplicationEventPublisher(ctx); + interceptor.afterPropertiesSet(); + + given(invocation.proceed()).willReturn(new Object()); + given(invocation.getThis()).willReturn(new Object()); + interceptor.invoke(invocation); + verify(ctx).publishEvent(isA(MyEvent.class)); + } + + @Test + void testEventPublicationInterceptorWithMethodFailure() throws Throwable { + MethodInvocation invocation = mock(); + ApplicationContext ctx = mock(); + + EventPublicationInterceptor interceptor = new EventPublicationInterceptor(); + interceptor.setApplicationEventPublisher(ctx); + interceptor.afterPropertiesSet(); + + given(invocation.proceed()).willThrow(new IllegalStateException()); + assertThatIllegalStateException().isThrownBy(() -> interceptor.invoke(invocation)); + verify(ctx).publishEvent(isA(MethodFailureEvent.class)); + } + @Test void listenersInApplicationContext() { StaticApplicationContext context = new StaticApplicationContext(); diff --git a/spring-context/src/test/java/org/springframework/context/event/EventPublicationInterceptorTests.java b/spring-context/src/test/java/org/springframework/context/event/EventPublicationInterceptorTests.java index df958daa50a..49bfa2b69ae 100644 --- a/spring-context/src/test/java/org/springframework/context/event/EventPublicationInterceptorTests.java +++ b/spring-context/src/test/java/org/springframework/context/event/EventPublicationInterceptorTests.java @@ -53,14 +53,16 @@ class EventPublicationInterceptorTests { @Test - void withNoApplicationEventClassSupplied() { + void withNoApplicationEventPublisherSupplied() { + this.interceptor.setApplicationEventPublisher(null); assertThatIllegalArgumentException().isThrownBy(interceptor::afterPropertiesSet); } + @SuppressWarnings("unchecked") @Test void withNonApplicationEventClassSupplied() { assertThatIllegalArgumentException().isThrownBy(() -> { - interceptor.setApplicationEventClass(getClass()); + interceptor.setApplicationEventClass((Class) getClass()); interceptor.afterPropertiesSet(); }); }