GH-305 - Multicaster now honors listener condition.

We now reflectively invoke ApplicationListenerMethodAdapter.shouldHandle(…) when selecting event listeners to make sure that conditions defined in, for example, @TransactionalEventListener are considered before registering an event publication.
This commit is contained in:
Oliver Drotbohm
2023-09-21 08:56:30 +02:00
parent 5b13bce62d
commit 93e9d5489d
2 changed files with 84 additions and 1 deletions

View File

@@ -15,6 +15,7 @@
*/
package org.springframework.modulith.events.support;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.List;
import java.util.function.Consumer;
@@ -29,10 +30,12 @@ import org.springframework.context.ApplicationListener;
import org.springframework.context.PayloadApplicationEvent;
import org.springframework.context.event.AbstractApplicationEventMulticaster;
import org.springframework.context.event.ApplicationEventMulticaster;
import org.springframework.context.event.ApplicationListenerMethodAdapter;
import org.springframework.core.ResolvableType;
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
import org.springframework.core.env.Environment;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.modulith.events.core.EventPublication;
import org.springframework.modulith.events.core.EventPublicationRegistry;
import org.springframework.modulith.events.core.PublicationTargetIdentifier;
@@ -40,6 +43,7 @@ import org.springframework.transaction.event.TransactionPhase;
import org.springframework.transaction.event.TransactionalApplicationListener;
import org.springframework.transaction.event.TransactionalEventListener;
import org.springframework.util.Assert;
import org.springframework.util.ReflectionUtils;
/**
* An {@link ApplicationEventMulticaster} to register {@link EventPublication}s in an {@link EventPublicationRegistry}
@@ -56,11 +60,17 @@ public class PersistentApplicationEventMulticaster extends AbstractApplicationEv
implements SmartInitializingSingleton {
private static final Logger LOGGER = LoggerFactory.getLogger(PersistentApplicationEventMulticaster.class);
private static final Method SUPPORTS_METHOD = ReflectionUtils.findMethod(ApplicationListenerMethodAdapter.class,
"shouldHandle", ApplicationEvent.class, Object[].class);
static final String REPUBLISH_ON_RESTART = "spring.modulith.republish-outstanding-events-on-restart";
private final @NonNull Supplier<EventPublicationRegistry> registry;
private final @NonNull Supplier<Environment> environment;
static {
ReflectionUtils.makeAccessible(SUPPORTS_METHOD);
}
/**
* Creates a new {@link PersistentApplicationEventMulticaster} for the given {@link EventPublicationRegistry}.
*
@@ -92,7 +102,7 @@ public class PersistentApplicationEventMulticaster extends AbstractApplicationEv
*/
@Override
@SuppressWarnings({ "unchecked", "rawtypes" })
public void multicastEvent(ApplicationEvent event, ResolvableType eventType) {
public void multicastEvent(ApplicationEvent event, @Nullable ResolvableType eventType) {
var type = eventType == null ? ResolvableType.forInstance(event) : eventType;
var listeners = getApplicationListeners(event, type);
@@ -109,6 +119,22 @@ public class PersistentApplicationEventMulticaster extends AbstractApplicationEv
}
}
/*
* (non-Javadoc)
* @see org.springframework.context.event.AbstractApplicationEventMulticaster#getApplicationListeners(org.springframework.context.ApplicationEvent, org.springframework.core.ResolvableType)
*/
@Override
protected Collection<ApplicationListener<?>> getApplicationListeners(ApplicationEvent event,
ResolvableType eventType) {
Object eventToPersist = getEventToPersist(event);
return super.getApplicationListeners(event, eventType)
.stream()
.filter(it -> matches(event, eventToPersist, it))
.toList();
}
/*
* (non-Javadoc)
* @see org.springframework.beans.factory.SmartInitializingSingleton#afterSingletonsInstantiated()
@@ -169,6 +195,18 @@ public class PersistentApplicationEventMulticaster extends AbstractApplicationEv
: event;
}
@SuppressWarnings("null")
private static boolean matches(ApplicationEvent event, Object payload, ApplicationListener<?> listener) {
// Verify general listener matching by eagerly evaluating the condition
if (!ApplicationListenerMethodAdapter.class.isInstance(listener)) {
return true;
}
return (boolean) ReflectionUtils.invokeMethod(SUPPORTS_METHOD, listener, event,
new Object[] { payload });
}
/**
* First-class collection to work with transactional event listeners, i.e. {@link ApplicationListener} instances that
* implement {@link TransactionalApplicationListener}.

View File

@@ -15,15 +15,25 @@
*/
package org.springframework.modulith.events.support;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
import lombok.AllArgsConstructor;
import java.util.Map;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.context.PayloadApplicationEvent;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.event.ApplicationEventMulticaster;
import org.springframework.context.event.EventListenerMethodProcessor;
import org.springframework.core.ResolvableType;
import org.springframework.core.env.MapPropertySource;
import org.springframework.core.env.StandardEnvironment;
import org.springframework.modulith.events.core.EventPublicationRegistry;
import org.springframework.stereotype.Component;
import org.springframework.transaction.event.TransactionalEventListener;
/**
* Unit tests for {@link PersistentApplicationEventMulticaster}.
@@ -61,4 +71,39 @@ class PersistentApplicationEventMulticasterUnitTests {
verify(registry).findIncompletePublications();
}
@Test // GH-277
void honorsListenerCondition() throws Exception {
try (var ctx = new AnnotationConfigApplicationContext()) {
ctx.addBeanFactoryPostProcessor(new EventListenerMethodProcessor());
ctx.registerBean("applicationEventMulticaster", ApplicationEventMulticaster.class, () -> multicaster);
ctx.registerBean("conditionalListener", ConditionalListener.class);
ctx.refresh();
assertListenerSelected(new SampleEvent(true), true);
assertListenerSelected(new SampleEvent(false), false);
}
}
private void assertListenerSelected(SampleEvent event, boolean expected) {
var listeners = multicaster.getApplicationListeners(new PayloadApplicationEvent<>(this, event),
ResolvableType.forClass(event.getClass()));
assertThat(listeners).hasSize(expected ? 1 : 0);
}
@Component
static class ConditionalListener {
@TransactionalEventListener(condition = "#event.supported")
void on(SampleEvent event) {}
}
@AllArgsConstructor
static class SampleEvent {
public boolean supported;
}
}