diff --git a/spring-modulith-bom/pom.xml b/spring-modulith-bom/pom.xml index 0a5abdf5..e0f753df 100644 --- a/spring-modulith-bom/pom.xml +++ b/spring-modulith-bom/pom.xml @@ -94,6 +94,11 @@ spring-modulith-events-kafka 1.3.0-SNAPSHOT + + org.springframework.modulith + spring-modulith-events-messaging + 1.3.0-SNAPSHOT + org.springframework.modulith spring-modulith-events-mongodb diff --git a/spring-modulith-events/pom.xml b/spring-modulith-events/pom.xml index 8ab5f661..70d54691 100644 --- a/spring-modulith-events/pom.xml +++ b/spring-modulith-events/pom.xml @@ -24,9 +24,9 @@ spring-modulith-events-jms spring-modulith-events-jpa spring-modulith-events-kafka + spring-modulith-events-messaging spring-modulith-events-mongodb spring-modulith-events-neo4j - spring-modulith-events-messaging diff --git a/spring-modulith-events/spring-modulith-events-api/src/main/java/org/springframework/modulith/events/RoutingTarget.java b/spring-modulith-events/spring-modulith-events-api/src/main/java/org/springframework/modulith/events/RoutingTarget.java index 696afb5b..d4eaa7cf 100644 --- a/spring-modulith-events/spring-modulith-events-api/src/main/java/org/springframework/modulith/events/RoutingTarget.java +++ b/spring-modulith-events/spring-modulith-events-api/src/main/java/org/springframework/modulith/events/RoutingTarget.java @@ -174,7 +174,7 @@ RoutingTarget verify() { */ @Override public String toString() { - return target + "::" + (key == null ? "" : key); + return target + (key == null ? "" : "::" + key); } /* diff --git a/spring-modulith-events/spring-modulith-events-messaging/src/main/java/org/springframework/modulith/events/messaging/SpringMessagingEventExternalizerConfiguration.java b/spring-modulith-events/spring-modulith-events-messaging/src/main/java/org/springframework/modulith/events/messaging/SpringMessagingEventExternalizerConfiguration.java index 9cd1dde7..46f64fa3 100644 --- a/spring-modulith-events/spring-modulith-events-messaging/src/main/java/org/springframework/modulith/events/messaging/SpringMessagingEventExternalizerConfiguration.java +++ b/spring-modulith-events/spring-modulith-events-messaging/src/main/java/org/springframework/modulith/events/messaging/SpringMessagingEventExternalizerConfiguration.java @@ -31,30 +31,28 @@ import org.springframework.messaging.support.MessageBuilder; import org.springframework.modulith.events.EventExternalizationConfiguration; import org.springframework.modulith.events.config.EventExternalizationAutoConfiguration; -import org.springframework.modulith.events.support.BrokerRouting; import org.springframework.modulith.events.support.DelegatingEventExternalizer; /** * Auto-configuration to set up a {@link DelegatingEventExternalizer} to externalize events to a Spring Messaging - * {@link MessageChannel message channel}. + * {@link MessageChannel}. * * @author Josh Long + * @author Oliver Drotbohm */ @AutoConfiguration @AutoConfigureAfter(EventExternalizationAutoConfiguration.class) @ConditionalOnClass(MessageChannel.class) -@ConditionalOnProperty(name = "spring.modulith.events.externalization.enabled", - havingValue = "true", +@ConditionalOnProperty(name = "spring.modulith.events.externalization.enabled", havingValue = "true", matchIfMissing = true) class SpringMessagingEventExternalizerConfiguration { private static final Logger logger = LoggerFactory.getLogger(SpringMessagingEventExternalizerConfiguration.class); - public static final String MODULITH_ROUTING_HEADER = "modulithRouting"; + public static final String MODULITH_ROUTING_HEADER = "springModulith_routingTarget"; @Bean - DelegatingEventExternalizer springMessagingEventExternalizer( - EventExternalizationConfiguration configuration, + DelegatingEventExternalizer springMessagingEventExternalizer(EventExternalizationConfiguration configuration, BeanFactory factory) { logger.debug("Registering domain event externalization for Spring Messaging…"); @@ -63,16 +61,19 @@ DelegatingEventExternalizer springMessagingEventExternalizer( context.setBeanResolver(new BeanFactoryResolver(factory)); return new DelegatingEventExternalizer(configuration, (target, payload) -> { - var routing = BrokerRouting.of(target, context); + + var targetChannel = target.getTarget(); var message = MessageBuilder .withPayload(payload) - .setHeader(MODULITH_ROUTING_HEADER, routing) + .setHeader(MODULITH_ROUTING_HEADER, target.toString()) .build(); + if (logger.isDebugEnabled()) { - logger.info("trying to find a {} with name {}", MessageChannel.class.getName(), routing.getTarget()); + logger.debug("trying to find a {} with name {}", MessageChannel.class.getName(), targetChannel); } - var bean = factory.getBean(routing.getTarget(), MessageChannel.class); - bean.send(message); + + factory.getBean(targetChannel, MessageChannel.class).send(message); + return CompletableFuture.completedFuture(null); }); } diff --git a/spring-modulith-events/spring-modulith-events-messaging/src/test/java/org/springframework/modulith/events/messaging/SpringMessagingEventPublicationIntegrationTests.java b/spring-modulith-events/spring-modulith-events-messaging/src/test/java/org/springframework/modulith/events/messaging/SpringMessagingEventPublicationIntegrationTests.java index 8d33b897..0a044ac7 100644 --- a/spring-modulith-events/spring-modulith-events-messaging/src/test/java/org/springframework/modulith/events/messaging/SpringMessagingEventPublicationIntegrationTests.java +++ b/spring-modulith-events/spring-modulith-events-messaging/src/test/java/org/springframework/modulith/events/messaging/SpringMessagingEventPublicationIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2023-2024 the original author or authors. + * Copyright 2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,7 +29,6 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.annotation.Bean; -import org.springframework.integration.core.GenericHandler; import org.springframework.integration.dsl.DirectChannelSpec; import org.springframework.integration.dsl.IntegrationFlow; import org.springframework.integration.dsl.MessageChannels; @@ -42,12 +41,14 @@ * Integration tests for Spring Messaging-based event publication. * * @author Josh Long + * @author Oliver Drotbohm + * @since 1.3 */ @SpringBootTest class SpringMessagingEventPublicationIntegrationTests { + private static final String TARGET = "target::#{someExpression}"; private static final String CHANNEL_NAME = "target"; - private static final AtomicInteger COUNTER = new AtomicInteger(); @Autowired TestPublisher publisher; @@ -62,12 +63,15 @@ TestPublisher testPublisher(ApplicationEventPublisher publisher) { } @Bean - IntegrationFlow inboundIntegrationFlow( - @Qualifier(CHANNEL_NAME) MessageChannel inbound) { + IntegrationFlow inboundIntegrationFlow(@Qualifier(CHANNEL_NAME) MessageChannel inbound) { return IntegrationFlow .from(inbound) - .handle((GenericHandler) (payload, headers) -> { + .handle((__, headers) -> { + + assertThat(headers.get(SpringMessagingEventExternalizerConfiguration.MODULITH_ROUTING_HEADER)) + .isEqualTo(TARGET); + COUNTER.incrementAndGet(); return null; }) @@ -83,16 +87,20 @@ DirectChannelSpec target() { @Test void publishesEventToSpringMessaging() throws Exception { + var publishes = 2; + for (var i = 0; i < publishes; i++) { publisher.publishEvent(); } + Thread.sleep(200); + assertThat(COUNTER.get()).isEqualTo(publishes); assertThat(completed.findAll()).hasSize(publishes); } - @Externalized(CHANNEL_NAME) + @Externalized(TARGET) static class TestEvent {} @RequiredArgsConstructor diff --git a/spring-modulith-events/spring-modulith-events-messaging/src/test/resources/application.properties b/spring-modulith-events/spring-modulith-events-messaging/src/test/resources/application.properties index 79d64cb2..46ffe05e 100644 --- a/spring-modulith-events/spring-modulith-events-messaging/src/test/resources/application.properties +++ b/spring-modulith-events/spring-modulith-events-messaging/src/test/resources/application.properties @@ -1 +1,2 @@ spring.modulith.events.jdbc.schema-initialization.enabled=true +spring.main.banner-mode=OFF diff --git a/src/docs/antora/modules/ROOT/pages/appendix.adoc b/src/docs/antora/modules/ROOT/pages/appendix.adoc index cc41f57d..1cd75316 100644 --- a/src/docs/antora/modules/ROOT/pages/appendix.adoc +++ b/src/docs/antora/modules/ROOT/pages/appendix.adoc @@ -167,6 +167,7 @@ a|* `spring-modulith-docs` |`spring-modulith-events-jms`|`runtime`|Event externalization support for JMS. |`spring-modulith-events-jpa`|`runtime`|A JPA-based implementation of the `EventPublicationRegistry`. |`spring-modulith-events-kafka`|`runtime`|Event externalization support for Kafka. +|`spring-modulith-events-messaging`|`runtime`|Event externalization support into Spring Messaging ``MessageChannel``s. |`spring-modulith-events-mongodb`|`runtime`|A MongoDB-based implementation of the `EventPublicationRegistry`. |`spring-modulith-events-neo4j`|`runtime`|A Neo4j-based implementation of the `EventPublicationRegistry`. |`spring-modulith-junit`|`test`|Test execution optimizations based on the application module structure. Find more details xref:testing.adoc#change-aware-test-execution[here]. diff --git a/src/docs/antora/modules/ROOT/pages/events.adoc b/src/docs/antora/modules/ROOT/pages/events.adoc index 8838d8f0..55a3758e 100644 --- a/src/docs/antora/modules/ROOT/pages/events.adoc +++ b/src/docs/antora/modules/ROOT/pages/events.adoc @@ -7,6 +7,7 @@ To keep application modules as decoupled as possible from each other, their prim This avoids the originating module to know about all potentially interested parties, which is a key aspect to enable application module integration testing (see xref:testing.adoc[Integration Testing Application Modules]). Often we will find application components defined like this: + [tabs] ====== Java:: @@ -124,6 +125,7 @@ class InventoryManagement { } ---- ====== + This now effectively decouples the original transaction from the execution of the listener. While this avoids the expansion of the original business transaction, it also creates a risk: if the listener fails for whatever reason, the event publication is lost, unless each listener actually implements its own safety net. Even worse, that doesn't even fully work, as the system might fail before the method is even invoked. @@ -212,7 +214,9 @@ image::event-publication-registry-end.png[] [[publication-registry.starters]] === Spring Boot Event Registry Starters -Using the transactional event publication log requires a combination of artifacts added to your application. To ease that task, Spring Modulith provides starter POMs that are centered around the <> to be used and default to the Jackson-based <> implementation. The following starters are available: +Using the transactional event publication log requires a combination of artifacts added to your application. +To ease that task, Spring Modulith provides starter POMs that are centered around the <> to be used and default to the Jackson-based <> implementation. +The following starters are available: [%header,cols="1,3,6"] |=== @@ -306,6 +310,7 @@ Spring Modulith provides a Jackson-based JSON implementation through the `spring [[publication-registry.customize-publication-date]] === Customizing the Event Publication Date + By default, the Event Publication Registry will use the date returned by the `Clock.systemUTC()` as event publication date. If you want to customize this, register a bean of type clock with the application context: @@ -372,7 +377,8 @@ When routing key is set, requires SNS to be configured as a FIFO topic with cont |Spring Messaging |`spring-modulith-events-messaging` |Uses Spring's core `Message` and `MessageChannel` support. -Resolves the target `MessageChannel` by its bean name given the `target` in the `Externalized` annotation. Forwards routing information as a header - called `modulithRouting` - to be processed in whatever way by downstream components, typically in a Spring Integration `IntegrationFlow`. +Resolves the target `MessageChannel` by its bean name given the `target` in the `Externalized` annotation. +Forwards routing information as a header - called `springModulith_routingTarget` - to be processed in whatever way by downstream components, typically in a Spring Integration `IntegrationFlow`. |=== @@ -453,7 +459,6 @@ Kotlin:: [[externalization.api]] === Programmatic Event Externalization Configuration - The `spring-modulith-events-api` artifact contains `EventExternalizationConfiguration` that allows developers to customize all of the above mentioned steps. .Programmatically configuring event externalization