Skip to content

Commit

Permalink
GH-277 - Multicaster now honors listener condition.
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
odrotbohm committed Sep 21, 2023
1 parent f6a45c0 commit 2148328
Show file tree
Hide file tree
Showing 2 changed files with 73 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/
package org.springframework.modulith.events.support;

import java.lang.reflect.Method;
import java.time.Duration;
import java.util.Collection;
import java.util.List;
Expand All @@ -31,6 +32,7 @@
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;
Expand All @@ -46,6 +48,7 @@
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}
Expand All @@ -62,11 +65,17 @@ public class PersistentApplicationEventMulticaster extends AbstractApplicationEv
implements IncompleteEventPublications, 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}.
*
Expand Down Expand Up @@ -127,14 +136,14 @@ protected Collection<ApplicationListener<?>> getApplicationListeners(Application

return super.getApplicationListeners(event, eventType)
.stream()
.filter(it -> matches(eventToPersist, it))
.filter(it -> matches(event, eventToPersist, it))
.toList();
}

/*
* (non-Javadoc)
* @see org.springframework.modulith.events.IncompleteEventPublications#resubmitIncompletePublications(java.util.function.Predicate)
*/
/*
* (non-Javadoc)
* @see org.springframework.modulith.events.IncompleteEventPublications#resubmitIncompletePublications(java.util.function.Predicate)
*/
@Override
public void resubmitIncompletePublications(Predicate<EventPublication> filter) {
doResubmitUncompletedPublicationsOlderThan(null);
Expand Down Expand Up @@ -219,10 +228,22 @@ private static Object getEventToPersist(ApplicationEvent event) {
: event;
}

private static boolean matches(Object event, ApplicationListener<?> listener) {
@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)) {

boolean result = (boolean) ReflectionUtils.invokeMethod(SUPPORTS_METHOD, listener, event,
new Object[] { payload });

if (!result) {
return false;
}
}

return ConditionalEventListener.class.isInstance(listener)
? ConditionalEventListener.class.cast(listener).supports(event)
? ConditionalEventListener.class.cast(listener).supports(payload)
: true;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}.
Expand Down Expand Up @@ -61,4 +71,39 @@ void triggersRepublicationIfExplicitlyEnabled() {

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;
}
}

0 comments on commit 2148328

Please sign in to comment.