3737import org .springframework .core .ResolvableType ;
3838import org .springframework .core .annotation .AnnotationAwareOrderComparator ;
3939import org .springframework .core .env .Environment ;
40+ import org .springframework .modulith .events .EventExternalizationConfiguration ;
4041import org .springframework .modulith .events .EventPublication ;
4142import org .springframework .modulith .events .FailedEventPublications ;
4243import org .springframework .modulith .events .IncompleteEventPublications ;
4748import org .springframework .modulith .events .core .TargetEventPublication ;
4849import org .springframework .transaction .event .TransactionPhase ;
4950import org .springframework .transaction .event .TransactionalApplicationListener ;
51+ import org .springframework .transaction .support .TransactionSynchronizationManager ;
5052import org .springframework .util .Assert ;
5153
5254/**
5860 * for incomplete publications.
5961 *
6062 * @author Oliver Drotbohm
63+ * @author Yunho Jung
6164 * @see CompletionRegisteringAdvisor
6265 */
6366public class PersistentApplicationEventMulticaster extends AbstractApplicationEventMulticaster
@@ -70,21 +73,26 @@ public class PersistentApplicationEventMulticaster extends AbstractApplicationEv
7073
7174 private final @ NonNull Supplier <EventPublicationRegistry > registry ;
7275 private final @ NonNull Supplier <Environment > environment ;
76+ private final @ NonNull Supplier <EventExternalizationConfiguration > externalizationConfiguration ;
7377
7478 /**
7579 * Creates a new {@link PersistentApplicationEventMulticaster} for the given {@link EventPublicationRegistry}.
7680 *
7781 * @param registry must not be {@literal null}.
7882 * @param environment must not be {@literal null}.
83+ * @param externalizationConfiguration must not be {@literal null}.
7984 */
8085 public PersistentApplicationEventMulticaster (Supplier <EventPublicationRegistry > registry ,
81- Supplier <Environment > environment ) {
86+ Supplier <Environment > environment ,
87+ Supplier <EventExternalizationConfiguration > externalizationConfiguration ) {
8288
8389 Assert .notNull (registry , "EventPublicationRegistry must not be null!" );
8490 Assert .notNull (environment , "Environment must not be null!" );
91+ Assert .notNull (externalizationConfiguration , "EventExternalizationConfiguration must not be null!" );
8592
8693 this .registry = registry ;
8794 this .environment = environment ;
95+ this .externalizationConfiguration = externalizationConfiguration ;
8896 }
8997
9098 /*
@@ -111,8 +119,13 @@ public void multicastEvent(ApplicationEvent event, @Nullable ResolvableType even
111119 return ;
112120 }
113121
114- new TransactionalEventListeners (listeners )
115- .ifPresent (it -> storePublications (it , getEventToPersist (event )));
122+ var eventToPersist = getEventToPersist (event );
123+ var transactionalListeners = new TransactionalEventListeners (listeners );
124+
125+ // Detect events configured for externalization published outside transaction context
126+ detectEventPublishedOutsideTransaction (transactionalListeners , eventToPersist );
127+
128+ transactionalListeners .ifPresent (it -> storePublications (it , eventToPersist ));
116129
117130 for (ApplicationListener listener : listeners ) {
118131 listener .onApplicationEvent (event );
@@ -273,6 +286,41 @@ private static boolean invokeShouldHandle(ApplicationListener<?> candidate, Appl
273286 return true ;
274287 }
275288
289+ /**
290+ * Detects if an event selected for externalization is published outside a transaction context.
291+ * If detected, logs a warning message to help developers identify the problem.
292+ *
293+ * @param transactionalListeners the transactional event listeners
294+ * @param event the event being published
295+ */
296+ private void detectEventPublishedOutsideTransaction (TransactionalEventListeners transactionalListeners ,
297+ Object event ) {
298+
299+ // Transaction is active, no problem
300+ if (TransactionSynchronizationManager .isActualTransactionActive ()) {
301+ return ;
302+ }
303+
304+ // No transactional listeners, nothing to check
305+ if (!transactionalListeners .hasListeners ()) {
306+ return ;
307+ }
308+
309+ // Check if the event is configured for externalization
310+ var config = externalizationConfiguration .get ();
311+ if (!config .supports (event )) {
312+ return ;
313+ }
314+
315+ // Issue a warning log hinting at the problem
316+ LOGGER .warn (
317+ "Event {} is configured for externalization but published outside a transaction context. "
318+ + "Event externalization requires a transactional context to work properly. "
319+ + "The event will not be persisted to the event publication registry and externalization will not be triggered. "
320+ + "Consider publishing this event from a @Transactional method." ,
321+ event .getClass ().getName ());
322+ }
323+
276324 /**
277325 * First-class collection to work with transactional event listeners, i.e. {@link ApplicationListener} instances that
278326 * implement {@link TransactionalApplicationListener}.
0 commit comments