GH-80 - Introduce ApplicationModuleIntegrationListener.

We now provide @ApplicationModuleIntegrationListener as shortcut for the combination of @Async @Transactional @TransactionalEventListener to provide a dedicated annotation for the recommended integration arrangement.
This commit is contained in:
Oliver Drotbohm
2022-11-24 21:24:27 +01:00
parent 486f7e0c86
commit 9463b598c3
5 changed files with 112 additions and 8 deletions

View File

@@ -23,6 +23,20 @@
<optional>true</optional>
</dependency>
<!-- For @ModuleIntegrationListener -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,57 @@
/*
* Copyright 2022 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.modulith;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;
import org.springframework.scheduling.annotation.Async;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.event.TransactionalEventListener;
/**
* An {@link ApplicationModuleIntegrationListener} is an {@link Async} Spring {@link TransactionalEventListener} that
* runs in a transaction itself. Thus, the annotation serves as syntactic sugar for the generally recommend setup to
* integrate application modules via events. The setup makes sure that an original business transaction completes
* successfully and the integration asynchronously runs in a transaction itself to decouple the integration as much as
* possible from the original unit of work.
* <p>
* It is advisable that you use these integration listeners in combination with the Spring Modulith Event Publication
* Registry to make sure that the event publication does not get lost in case of an application or listener failure.
*
* @author Oliver Drotbohm
* @see <a href="https://docs.spring.io/spring-modulith/docs/current/reference/html/#events.publication-registry">Spring
* Modulith Event Publication Registry - Reference Documentation</a>
*/
@Async
@Transactional(propagation = Propagation.REQUIRES_NEW)
@TransactionalEventListener
@Documented
@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface ApplicationModuleIntegrationListener {
/**
* Whether the transaction to be run for the event listener is supposed to be read-only (default {@literal false}).
*/
@AliasFor(annotation = Transactional.class, attribute = "readOnly")
boolean readOnlyTransaction() default false;
}

View File

@@ -19,9 +19,8 @@ import example.order.OrderCompleted;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.modulith.ApplicationModuleIntegrationListener;
import org.springframework.stereotype.Service;
import org.springframework.transaction.event.TransactionalEventListener;
/**
* A Spring {@link Service} exposed by the inventory module.
@@ -35,8 +34,7 @@ public class InventoryManagement {
private final InventoryInternal dependency;
@Async
@TransactionalEventListener
@ApplicationModuleIntegrationListener
void on(OrderCompleted event) throws InterruptedException {
var orderId = event.getOrderId();

View File

@@ -22,11 +22,10 @@ import lombok.RequiredArgsConstructor;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.Import;
import org.springframework.modulith.ApplicationModuleIntegrationListener;
import org.springframework.modulith.events.EventPublicationRegistry;
import org.springframework.modulith.test.ApplicationModuleTest;
import org.springframework.scheduling.annotation.Async;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.transaction.event.TransactionalEventListener;
/**
* A show case for how the Spring Modulith application event publication registry keeps track of incomplete publications
@@ -57,8 +56,7 @@ class EventPublicationRegistryTests {
static class FailingAsyncTransactionalEventListener {
@Async
@TransactionalEventListener
@ApplicationModuleIntegrationListener
void foo(OrderCompleted event) {
throw new IllegalStateException();
}

View File

@@ -78,6 +78,43 @@ This now effectively decouples the original transaction from the execution of th
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.
[[events.amil]]
== Application Module Integration Listener
To run a transactional event listener in a transaction itself, it would need to be annotated with `@Transactional` itself.
.An async, transactional event listener running in a transaction itself
[source, java]
----
@Service
@RequiredArgsConstructor
public class InventoryManagement {
@Async
@Transactional
@TransactionalEventListener
void on(OrderCompleted event) {
}
}
----
To ease the declaration of what is supposed to describe the default way of integrating modules via events, Spring Modulith provides `@ApplicationModuleIntegrationListener` to shortcut the declaration
.An application module integration listener
[source, java]
----
@Service
@RequiredArgsConstructor
public class InventoryManagement {
@ApplicationModuleIntegrationListener
void on(OrderCompleted event) {
}
}
----
[[events.publication-registry]]
== The Event Publication Registry