diff --git a/spring-modulith-events/spring-modulith-events-core/src/main/java/org/springframework/modulith/events/config/EventPublicationAutoConfiguration.java b/spring-modulith-events/spring-modulith-events-core/src/main/java/org/springframework/modulith/events/config/EventPublicationAutoConfiguration.java index c40473b3..6b20513b 100644 --- a/spring-modulith-events/spring-modulith-events-core/src/main/java/org/springframework/modulith/events/config/EventPublicationAutoConfiguration.java +++ b/spring-modulith-events/spring-modulith-events-core/src/main/java/org/springframework/modulith/events/config/EventPublicationAutoConfiguration.java @@ -39,6 +39,7 @@ import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Role; import org.springframework.core.env.Environment; +import org.springframework.modulith.events.EventExternalizationConfiguration; import org.springframework.modulith.events.config.EventPublicationAutoConfiguration.AsyncEnablingConfiguration; import org.springframework.modulith.events.core.DefaultEventPublicationRegistry; import org.springframework.modulith.events.core.EventPublicationRegistry; @@ -74,9 +75,11 @@ DefaultEventPublicationRegistry eventPublicationRegistry(EventPublicationReposit @Role(BeanDefinition.ROLE_INFRASTRUCTURE) @ConditionalOnBean(EventPublicationRegistry.class) static PersistentApplicationEventMulticaster applicationEventMulticaster( - ObjectFactory eventPublicationRegistry, ObjectFactory environment) { + ObjectFactory eventPublicationRegistry, ObjectFactory environment, + ObjectProvider externalizationConfiguration) { - return EventPublicationConfiguration.applicationEventMulticaster(eventPublicationRegistry, environment); + return EventPublicationConfiguration.applicationEventMulticaster(eventPublicationRegistry, environment, + externalizationConfiguration); } @Bean diff --git a/spring-modulith-events/spring-modulith-events-core/src/main/java/org/springframework/modulith/events/config/EventPublicationConfiguration.java b/spring-modulith-events/spring-modulith-events-core/src/main/java/org/springframework/modulith/events/config/EventPublicationConfiguration.java index c01929af..4828cf82 100644 --- a/spring-modulith-events/spring-modulith-events-core/src/main/java/org/springframework/modulith/events/config/EventPublicationConfiguration.java +++ b/spring-modulith-events/spring-modulith-events-core/src/main/java/org/springframework/modulith/events/config/EventPublicationConfiguration.java @@ -24,6 +24,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Role; import org.springframework.core.env.Environment; +import org.springframework.modulith.events.EventExternalizationConfiguration; import org.springframework.modulith.events.core.DefaultEventPublicationRegistry; import org.springframework.modulith.events.core.EventPublicationRegistry; import org.springframework.modulith.events.core.EventPublicationRepository; @@ -51,10 +52,12 @@ DefaultEventPublicationRegistry eventPublicationRegistry(EventPublicationReposit @Bean @Role(BeanDefinition.ROLE_INFRASTRUCTURE) static PersistentApplicationEventMulticaster applicationEventMulticaster( - ObjectFactory eventPublicationRegistry, ObjectFactory environment) { + ObjectFactory eventPublicationRegistry, ObjectFactory environment, + ObjectProvider externalizationConfiguration) { return new PersistentApplicationEventMulticaster(() -> eventPublicationRegistry.getObject(), - () -> environment.getObject()); + () -> environment.getObject(), + () -> externalizationConfiguration.getIfAvailable(EventExternalizationConfiguration::disabled)); } @Bean diff --git a/spring-modulith-events/spring-modulith-events-core/src/main/java/org/springframework/modulith/events/support/PersistentApplicationEventMulticaster.java b/spring-modulith-events/spring-modulith-events-core/src/main/java/org/springframework/modulith/events/support/PersistentApplicationEventMulticaster.java index 210d1923..6cf59605 100644 --- a/spring-modulith-events/spring-modulith-events-core/src/main/java/org/springframework/modulith/events/support/PersistentApplicationEventMulticaster.java +++ b/spring-modulith-events/spring-modulith-events-core/src/main/java/org/springframework/modulith/events/support/PersistentApplicationEventMulticaster.java @@ -37,6 +37,7 @@ import org.springframework.core.ResolvableType; import org.springframework.core.annotation.AnnotationAwareOrderComparator; import org.springframework.core.env.Environment; +import org.springframework.modulith.events.EventExternalizationConfiguration; import org.springframework.modulith.events.EventPublication; import org.springframework.modulith.events.FailedEventPublications; import org.springframework.modulith.events.IncompleteEventPublications; @@ -47,6 +48,7 @@ import org.springframework.modulith.events.core.TargetEventPublication; import org.springframework.transaction.event.TransactionPhase; import org.springframework.transaction.event.TransactionalApplicationListener; +import org.springframework.transaction.support.TransactionSynchronizationManager; import org.springframework.util.Assert; /** @@ -58,6 +60,7 @@ * for incomplete publications. * * @author Oliver Drotbohm + * @author Yunho Jung * @see CompletionRegisteringAdvisor */ public class PersistentApplicationEventMulticaster extends AbstractApplicationEventMulticaster @@ -70,21 +73,26 @@ public class PersistentApplicationEventMulticaster extends AbstractApplicationEv private final @NonNull Supplier registry; private final @NonNull Supplier environment; + private final @NonNull Supplier externalizationConfiguration; /** * Creates a new {@link PersistentApplicationEventMulticaster} for the given {@link EventPublicationRegistry}. * * @param registry must not be {@literal null}. * @param environment must not be {@literal null}. + * @param externalizationConfiguration must not be {@literal null}. */ public PersistentApplicationEventMulticaster(Supplier registry, - Supplier environment) { + Supplier environment, + Supplier externalizationConfiguration) { Assert.notNull(registry, "EventPublicationRegistry must not be null!"); Assert.notNull(environment, "Environment must not be null!"); + Assert.notNull(externalizationConfiguration, "EventExternalizationConfiguration must not be null!"); this.registry = registry; this.environment = environment; + this.externalizationConfiguration = externalizationConfiguration; } /* @@ -111,8 +119,13 @@ public void multicastEvent(ApplicationEvent event, @Nullable ResolvableType even return; } - new TransactionalEventListeners(listeners) - .ifPresent(it -> storePublications(it, getEventToPersist(event))); + var eventToPersist = getEventToPersist(event); + var transactionalListeners = new TransactionalEventListeners(listeners); + + // Detect events configured for externalization published outside transaction context + detectEventPublishedOutsideTransaction(transactionalListeners, eventToPersist); + + transactionalListeners.ifPresent(it -> storePublications(it, eventToPersist)); for (ApplicationListener listener : listeners) { listener.onApplicationEvent(event); @@ -273,6 +286,41 @@ private static boolean invokeShouldHandle(ApplicationListener candidate, Appl return true; } + /** + * Detects if an event selected for externalization is published outside a transaction context. + * If detected, logs a warning message to help developers identify the problem. + * + * @param transactionalListeners the transactional event listeners + * @param event the event being published + */ + private void detectEventPublishedOutsideTransaction(TransactionalEventListeners transactionalListeners, + Object event) { + + // Transaction is active, no problem + if (TransactionSynchronizationManager.isActualTransactionActive()) { + return; + } + + // No transactional listeners, nothing to check + if (!transactionalListeners.hasListeners()) { + return; + } + + // Check if the event is configured for externalization + var config = externalizationConfiguration.get(); + if (!config.supports(event)) { + return; + } + + // Issue a warning log hinting at the problem + LOGGER.warn( + "Event {} is configured for externalization but published outside a transaction context. " + + "Event externalization requires a transactional context to work properly. " + + "The event will not be persisted to the event publication registry and externalization will not be triggered. " + + "Consider publishing this event from a @Transactional method.", + event.getClass().getName()); + } + /** * First-class collection to work with transactional event listeners, i.e. {@link ApplicationListener} instances that * implement {@link TransactionalApplicationListener}. diff --git a/spring-modulith-events/spring-modulith-events-core/src/test/java/org/springframework/modulith/events/support/PersistentApplicationEventMulticasterUnitTests.java b/spring-modulith-events/spring-modulith-events-core/src/test/java/org/springframework/modulith/events/support/PersistentApplicationEventMulticasterUnitTests.java index fb83ae35..27588ed2 100644 --- a/spring-modulith-events/spring-modulith-events-core/src/test/java/org/springframework/modulith/events/support/PersistentApplicationEventMulticasterUnitTests.java +++ b/spring-modulith-events/spring-modulith-events-core/src/test/java/org/springframework/modulith/events/support/PersistentApplicationEventMulticasterUnitTests.java @@ -31,6 +31,7 @@ import org.springframework.core.ResolvableType; import org.springframework.core.env.MapPropertySource; import org.springframework.core.env.StandardEnvironment; +import org.springframework.modulith.events.EventExternalizationConfiguration; import org.springframework.modulith.events.core.EventPublicationRegistry; import org.springframework.stereotype.Component; import org.springframework.transaction.event.TransactionPhase; @@ -51,7 +52,8 @@ class PersistentApplicationEventMulticasterUnitTests { @BeforeEach void setUp() { - this.multicaster = new PersistentApplicationEventMulticaster(() -> registry, () -> environment); + this.multicaster = new PersistentApplicationEventMulticaster(() -> registry, () -> environment, + EventExternalizationConfiguration::disabled); } @Test // GH-240, GH-251