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:
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user