Skip to content

Commit 5b6c231

Browse files
committed
Use dedicated ApplicationEventFactory interface in EventPublicationInterceptor
See gh-36072
1 parent aeb0115 commit 5b6c231

File tree

2 files changed

+82
-17
lines changed

2 files changed

+82
-17
lines changed

spring-context/src/main/java/org/springframework/context/event/EventPublicationInterceptor.java

Lines changed: 56 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
package org.springframework.context.event;
1818

1919
import java.lang.reflect.Constructor;
20-
import java.util.function.Function;
2120

2221
import org.aopalliance.intercept.MethodInterceptor;
2322
import org.aopalliance.intercept.MethodInvocation;
@@ -31,19 +30,21 @@
3130
import org.springframework.util.Assert;
3231

3332
/**
34-
* {@link MethodInterceptor Interceptor} that publishes an {@code ApplicationEvent}
35-
* to all {@code ApplicationListeners} registered with an {@code ApplicationEventPublisher}.
33+
* {@link MethodInterceptor Interceptor} that publishes an {@code ApplicationEvent} to
34+
* all {@code ApplicationListeners} registered with an {@code ApplicationEventPublisher}
3635
* after each <i>successful</i> method invocation.
3736
*
3837
* <p>Note that this interceptor is capable of publishing a custom event after each
3938
* <i>successful</i> method invocation, configured via the
4039
* {@link #setApplicationEventClass "applicationEventClass"} property. As of 7.0.3,
41-
* you can configure a {@link #setApplicationEventFactory factory function} instead.
40+
* you can configure a {@link #setApplicationEventFactory factory function} instead,
41+
* implementing the primary {@link ApplicationEventFactory#onSuccess} method there.
4242
*
43-
* <p>As of 7.0.3, this interceptor publishes a {@link MethodFailureEvent} for
44-
* every exception encountered from a method invocation. This can be conveniently
43+
* <p>By default (as of 7.0.3), this interceptor publishes a {@link MethodFailureEvent}
44+
* for every exception encountered from a method invocation. This can be conveniently
4545
* tracked via an {@code ApplicationListener<MethodFailureEvent>} class or an
46-
* {@code @EventListener(MethodFailureEvent.class)} method.
46+
* {@code @EventListener(MethodFailureEvent.class)} method. The failure event can be
47+
* customized through overriding the {@link ApplicationEventFactory#onFailure} method.
4748
*
4849
* @author Dmitriy Kopylenko
4950
* @author Juergen Hoeller
@@ -57,13 +58,13 @@
5758
public class EventPublicationInterceptor
5859
implements MethodInterceptor, ApplicationEventPublisherAware, InitializingBean {
5960

60-
private @Nullable Function<MethodInvocation, ? extends ApplicationEvent> applicationEventFactory;
61+
private ApplicationEventFactory applicationEventFactory = (invocation, returnValue) -> null;
6162

6263
private @Nullable ApplicationEventPublisher applicationEventPublisher;
6364

6465

6566
/**
66-
* Set the application event class to publish.
67+
* Set the application event class to publish after each successful invocation.
6768
* <p>The event class <b>must</b> have a constructor with a single
6869
* {@code Object} argument for the event source. The interceptor
6970
* will pass in the invoked object.
@@ -79,7 +80,8 @@ public void setApplicationEventClass(Class<? extends ApplicationEvent> applicati
7980
}
8081
try {
8182
Constructor<? extends ApplicationEvent> ctor = applicationEventClass.getConstructor(Object.class);
82-
this.applicationEventFactory = (invocation -> BeanUtils.instantiateClass(ctor, invocation.getThis()));
83+
this.applicationEventFactory = ((invocation, returnValue) ->
84+
BeanUtils.instantiateClass(ctor, invocation.getThis()));
8385
}
8486
catch (NoSuchMethodException ex) {
8587
throw new IllegalArgumentException("ApplicationEvent class [" +
@@ -89,12 +91,12 @@ public void setApplicationEventClass(Class<? extends ApplicationEvent> applicati
8991

9092
/**
9193
* Specify a factory function for {@link ApplicationEvent} instances built from a
92-
* {@link MethodInvocation}, representing a <i>successful</i> method invocation.
94+
* {@link MethodInvocation}, representing each <i>successful</i> method invocation.
9395
* @since 7.0.3
9496
* @see #setApplicationEventClass
9597
*/
96-
public void setApplicationEventFactory(Function<MethodInvocation, ? extends ApplicationEvent> factoryFunction) {
97-
this.applicationEventFactory = factoryFunction;
98+
public void setApplicationEventFactory(ApplicationEventFactory applicationEventFactory) {
99+
this.applicationEventFactory = applicationEventFactory;
98100
}
99101

100102
@Override
@@ -119,14 +121,52 @@ public void afterPropertiesSet() throws Exception {
119121
retVal = invocation.proceed();
120122
}
121123
catch (Throwable ex) {
122-
this.applicationEventPublisher.publishEvent(new MethodFailureEvent(invocation, ex));
124+
// Publish event after failed invocation.
125+
ApplicationEvent event = this.applicationEventFactory.onFailure(invocation, ex);
126+
if (event != null) {
127+
this.applicationEventPublisher.publishEvent(event);
128+
}
123129
throw ex;
124130
}
125131

126-
if (this.applicationEventFactory != null) {
127-
this.applicationEventPublisher.publishEvent(this.applicationEventFactory.apply(invocation));
132+
// Publish event after successful invocation.
133+
ApplicationEvent event = this.applicationEventFactory.onSuccess(invocation, retVal);
134+
if (event != null) {
135+
this.applicationEventPublisher.publishEvent(event);
128136
}
129137
return retVal;
130138
}
131139

140+
141+
/**
142+
* Callback interface for building an {@link ApplicationEvent} after a method invocation.
143+
* @since 7.0.3
144+
*/
145+
@FunctionalInterface
146+
public interface ApplicationEventFactory {
147+
148+
/**
149+
* Build an {@link ApplicationEvent} for the given successful method invocation.
150+
* <p>This is the primary method to implement since there is no such default event.
151+
* This may also return {@code null} for not publishing an event on success at all.
152+
* @param invocation the successful method invocation
153+
* @param returnValue the value that the method returned, if any
154+
* @return the event to publish, or {@code null} for none
155+
*/
156+
@Nullable ApplicationEvent onSuccess(MethodInvocation invocation, @Nullable Object returnValue);
157+
158+
/**
159+
* Build an {@link ApplicationEvent} for the given failed method invocation.
160+
* <p>The default implementation builds a common {@link MethodFailureEvent}.
161+
* This can be overridden to build a custom event instead, or to return
162+
* {@code null} for not publishing an event on failure at all.
163+
* @param invocation the failed method invocation
164+
* @param failure the exception thrown from the method
165+
* @return the event to publish, or {@code null} for none
166+
*/
167+
default @Nullable ApplicationEvent onFailure(MethodInvocation invocation, Throwable failure) {
168+
return new MethodFailureEvent(invocation, failure);
169+
}
170+
}
171+
132172
}

spring-context/src/test/java/org/springframework/context/event/ApplicationContextEventTests.java

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import java.util.function.Consumer;
2525

2626
import org.aopalliance.intercept.MethodInvocation;
27+
import org.jspecify.annotations.Nullable;
2728
import org.junit.jupiter.api.Test;
2829
import org.mockito.ArgumentCaptor;
2930

@@ -320,7 +321,7 @@ void testEventPublicationInterceptorWithEventFactory() throws Throwable {
320321
ApplicationContext ctx = mock();
321322

322323
EventPublicationInterceptor interceptor = new EventPublicationInterceptor();
323-
interceptor.setApplicationEventFactory(inv -> new MyEvent(invocation.getThis()));
324+
interceptor.setApplicationEventFactory((inv, retVal) -> new MyEvent(inv.getThis()));
324325
interceptor.setApplicationEventPublisher(ctx);
325326
interceptor.afterPropertiesSet();
326327

@@ -344,6 +345,30 @@ void testEventPublicationInterceptorWithMethodFailure() throws Throwable {
344345
verify(ctx).publishEvent(isA(MethodFailureEvent.class));
345346
}
346347

348+
@Test
349+
void testEventPublicationInterceptorWithCustomFailure() throws Throwable {
350+
MethodInvocation invocation = mock();
351+
ApplicationContext ctx = mock();
352+
353+
EventPublicationInterceptor interceptor = new EventPublicationInterceptor();
354+
interceptor.setApplicationEventFactory(new EventPublicationInterceptor.ApplicationEventFactory() {
355+
@Override
356+
public ApplicationEvent onSuccess(MethodInvocation invocation, @Nullable Object returnValue) {
357+
return new MyEvent(returnValue);
358+
}
359+
@Override
360+
public ApplicationEvent onFailure(MethodInvocation invocation, Throwable failure) {
361+
return new MyOtherEvent(failure);
362+
}
363+
});
364+
interceptor.setApplicationEventPublisher(ctx);
365+
interceptor.afterPropertiesSet();
366+
367+
given(invocation.proceed()).willThrow(new IllegalStateException());
368+
assertThatIllegalStateException().isThrownBy(() -> interceptor.invoke(invocation));
369+
verify(ctx).publishEvent(isA(MyOtherEvent.class));
370+
}
371+
347372
@Test
348373
void listenersInApplicationContext() {
349374
StaticApplicationContext context = new StaticApplicationContext();

0 commit comments

Comments
 (0)