diff --git a/spring-modulith-test/src/main/java/org/springframework/modulith/test/PublishedEventsParameterResolver.java b/spring-modulith-test/src/main/java/org/springframework/modulith/test/PublishedEventsParameterResolver.java index d044d763b..9db0cca4b 100644 --- a/spring-modulith-test/src/main/java/org/springframework/modulith/test/PublishedEventsParameterResolver.java +++ b/spring-modulith-test/src/main/java/org/springframework/modulith/test/PublishedEventsParameterResolver.java @@ -26,6 +26,8 @@ import org.springframework.context.ApplicationEvent; import org.springframework.context.ApplicationListener; import org.springframework.context.support.AbstractApplicationContext; +import org.springframework.core.task.TaskDecorator; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.util.Assert; @@ -38,6 +40,7 @@ class PublishedEventsParameterResolver implements ParameterResolver, AfterEachCa private final Function lookup; private @Nullable ThreadBoundApplicationListenerAdapter listener; + private @Nullable PropagateDelegateFromMainThreadTaskDecorator delegatePropagator; PublishedEventsParameterResolver() { this(ctx -> SpringExtension.getApplicationContext(ctx)); @@ -79,6 +82,9 @@ public PublishedEvents resolveParameter(ParameterContext parameterContext, Exten if (listener != null) { listener.registerDelegate(publishedEvents); } + if (delegatePropagator != null) { + delegatePropagator.publishedEventsListener = publishedEvents; + } return publishedEvents; } @@ -106,6 +112,12 @@ private void initializeListener(ExtensionContext extensionContext) { return adapter; }); + + delegatePropagator = new PropagateDelegateFromMainThreadTaskDecorator(listener); + ThreadPoolTaskExecutor taskExecutor = context.getBeanProvider(ThreadPoolTaskExecutor.class).getIfAvailable(); + if (taskExecutor != null) { + taskExecutor.setTaskDecorator(delegatePropagator); + } } /* @@ -165,4 +177,31 @@ public void onApplicationEvent(ApplicationEvent event) { } } } + + private static final class PropagateDelegateFromMainThreadTaskDecorator implements TaskDecorator { + private final ThreadBoundApplicationListenerAdapter adapter; + @Nullable + private ApplicationListener publishedEventsListener; + + PropagateDelegateFromMainThreadTaskDecorator(ThreadBoundApplicationListenerAdapter adapter) { + this.adapter = adapter; + } + + @Override + public Runnable decorate(Runnable task) { + return () -> { + if (publishedEventsListener == null) { + task.run(); + return; + } + ApplicationListener previous = adapter.delegate.get(); + try { + adapter.delegate.set(publishedEventsListener); + task.run(); + } finally { + adapter.delegate.set(previous); + } + }; + } + } } diff --git a/spring-modulith-test/src/test/java/org/springframework/modulith/test/PublishedEventsIntegrationTests.java b/spring-modulith-test/src/test/java/org/springframework/modulith/test/PublishedEventsIntegrationTests.java index f8076e91e..b3b58790a 100644 --- a/spring-modulith-test/src/test/java/org/springframework/modulith/test/PublishedEventsIntegrationTests.java +++ b/spring-modulith-test/src/test/java/org/springframework/modulith/test/PublishedEventsIntegrationTests.java @@ -24,11 +24,15 @@ import lombok.RequiredArgsConstructor; import org.awaitility.Awaitility; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.context.ApplicationEventPublisher; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; /** * Integration tests for {@link PublishedEvents}. @@ -38,12 +42,25 @@ @RequiredArgsConstructor @ExtendWith(PublishedEventsExtension.class) @SpringBootTest(classes = TestConfiguration.class) +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) class PublishedEventsIntegrationTests { @Autowired ApplicationEventPublisher publisher; @Autowired AsyncEventListener listener; + @Autowired ThreadPoolTaskExecutor taskExecutor; + + @Test // #1513 + @Order(1) + void initializeTaskThreadsBeforePublishedEventsAreInitialized() { + for (int i = 0; i < 20; ++i) { + taskExecutor.execute(() -> { + // no op + }); + } + } @Test // #116 + @Order(2) void capturesEventsTriggeredByAsyncEventListeners(PublishedEvents events) { assertThatNoException().isThrownBy(() -> {