diff --git a/spring-modulith-events/spring-modulith-events-core/pom.xml b/spring-modulith-events/spring-modulith-events-core/pom.xml
index 14b25218..52aeaf21 100644
--- a/spring-modulith-events/spring-modulith-events-core/pom.xml
+++ b/spring-modulith-events/spring-modulith-events-core/pom.xml
@@ -32,15 +32,26 @@
org.springframework
spring-aop
-
+
+
+ org.springframework.boot
+ spring-boot-autoconfigure
+
+
-
+
org.springframework
spring-test
test
+
+ org.springframework.boot
+ spring-boot-test-autoconfigure
+ test
+
+
org.slf4j
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 0cc09d51..54f07746 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
@@ -15,11 +15,22 @@
*/
package org.springframework.modulith.events.config;
+import java.time.Duration;
+import java.util.Arrays;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.BeansException;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.beans.factory.config.BeanDefinition;
+import org.springframework.beans.factory.config.BeanPostProcessor;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.autoconfigure.task.TaskExecutionProperties;
+import org.springframework.boot.autoconfigure.task.TaskExecutionProperties.Shutdown;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Role;
+import org.springframework.core.env.Environment;
import org.springframework.modulith.events.DefaultEventPublicationRegistry;
import org.springframework.modulith.events.EventPublicationRegistry;
import org.springframework.modulith.events.EventPublicationRepository;
@@ -35,15 +46,16 @@ import org.springframework.modulith.events.support.PersistentApplicationEventMul
class EventPublicationConfiguration {
@Bean
+ @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
EventPublicationRegistry eventPublicationRegistry(EventPublicationRepository repository) {
return new DefaultEventPublicationRegistry(repository);
}
@Bean
- PersistentApplicationEventMulticaster applicationEventMulticaster(
- EventPublicationRegistry eventPublicationRegistry) {
-
- return new PersistentApplicationEventMulticaster(() -> eventPublicationRegistry);
+ @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
+ static PersistentApplicationEventMulticaster applicationEventMulticaster(
+ ObjectFactory eventPublicationRegistry) {
+ return new PersistentApplicationEventMulticaster(() -> eventPublicationRegistry.getObject());
}
@Bean
@@ -51,4 +63,58 @@ class EventPublicationConfiguration {
static CompletionRegisteringAdvisor completionRegisteringAdvisor(ObjectFactory registry) {
return new CompletionRegisteringAdvisor(registry::getObject);
}
+
+ @Bean
+ @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
+ @ConditionalOnProperty(
+ name = "spring.modulith.default-async-termination",
+ havingValue = "true",
+ matchIfMissing = true)
+ static AsyncPropertiesDefaulter asyncPropertiesDefaulter(Environment environment) {
+ return new AsyncPropertiesDefaulter(environment);
+ }
+
+ static class AsyncPropertiesDefaulter implements BeanPostProcessor {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(AsyncPropertiesDefaulter.class);
+ private static final String PROPERTY = "spring.task.execution.shutdown.await-termination";
+
+ private final Environment environment;
+
+ AsyncPropertiesDefaulter(Environment environment) {
+ this.environment = environment;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.beans.factory.config.BeanPostProcessor#postProcessAfterInitialization(java.lang.Object, java.lang.String)
+ */
+ @Override
+ public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
+
+ if (!(bean instanceof TaskExecutionProperties p)) {
+ return bean;
+ }
+
+ if (anyPropertyConfigured(PROPERTY, PROPERTY + "-period")) {
+ return bean;
+ }
+
+ LOGGER.debug("Defaulting async shutdown to await termination in 2 seconds.");
+
+ Shutdown shutdown = p.getShutdown();
+
+ shutdown.setAwaitTermination(true);
+ shutdown.setAwaitTerminationPeriod(Duration.ofSeconds(2));
+
+ return p;
+ }
+
+ private boolean anyPropertyConfigured(String... properties) {
+
+ return Arrays.stream(properties)
+ .map(it -> environment.getProperty(it, (String) null))
+ .anyMatch(it -> it != null);
+ }
+ }
}
diff --git a/spring-modulith-events/spring-modulith-events-core/src/main/resources/META-INF/spring-configuration-metadata.json b/spring-modulith-events/spring-modulith-events-core/src/main/resources/META-INF/spring-configuration-metadata.json
new file mode 100644
index 00000000..517150cd
--- /dev/null
+++ b/spring-modulith-events/spring-modulith-events-core/src/main/resources/META-INF/spring-configuration-metadata.json
@@ -0,0 +1,10 @@
+{
+ "properties": [
+ {
+ "name": "spring.modulith.default-async-termination",
+ "type": "java.lang.boolean",
+ "description": "Whether to configure defaults for the async processing termination, namely to wait for task completion for 2 seconds. See TaskExecutionProperties for details.",
+ "defaultValue": "true"
+ }
+ ]
+}
diff --git a/spring-modulith-events/spring-modulith-events-core/src/test/java/org/springframework/modulith/events/config/EventPublicationConfigurationIntegrationTests.java b/spring-modulith-events/spring-modulith-events-core/src/test/java/org/springframework/modulith/events/config/EventPublicationConfigurationIntegrationTests.java
new file mode 100644
index 00000000..91b86c69
--- /dev/null
+++ b/spring-modulith-events/spring-modulith-events-core/src/test/java/org/springframework/modulith/events/config/EventPublicationConfigurationIntegrationTests.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2023 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.events.config;
+
+import static org.assertj.core.api.Assertions.*;
+
+import java.time.Duration;
+import java.util.function.Function;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration;
+import org.springframework.boot.autoconfigure.task.TaskExecutionProperties;
+import org.springframework.boot.autoconfigure.task.TaskExecutionProperties.Shutdown;
+import org.springframework.boot.test.context.assertj.AssertableApplicationContext;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+import org.springframework.boot.test.context.runner.ContextConsumer;
+import org.springframework.modulith.events.EventPublicationRepository;
+import org.springframework.modulith.events.config.EventPublicationConfiguration.AsyncPropertiesDefaulter;
+
+/**
+ * Unit tests for {@link EventPublicationConfiguration}.
+ *
+ * @author Oliver Drotbohm
+ */
+@ExtendWith(MockitoExtension.class)
+class EventPublicationConfigurationIntegrationTests {
+
+ @Mock EventPublicationRepository repository;
+
+ @Test // GH-149
+ void registersAsyncTerminationDefaulterByDefault() {
+
+ basicSetup()
+ .run(context -> assertThat(context).hasSingleBean(AsyncPropertiesDefaulter.class));
+ }
+
+ @Test // GH-149
+ void doesNotRegisterDefaulterIfDisabledExplicitly() {
+
+ basicSetup()
+ .withPropertyValues("spring.modulith.default-async-termination=false")
+ .run(context -> assertThat(context).doesNotHaveBean(AsyncPropertiesDefaulter.class));
+ }
+
+ @Test // GH-149
+ void doesNotApplyDefaultingIfShutdownTerminationPropertyConfigured() {
+
+ basicSetup()
+ .withConfiguration(AutoConfigurations.of(TaskExecutionAutoConfiguration.class))
+ .withPropertyValues("spring.task.execution.shutdown.await-termination=false")
+ .run(expect(Shutdown::isAwaitTermination, false));
+ }
+
+ @Test // GH-149
+ void doesNotApplyDefaultingIfShutdownTerminationPeriodPropertyConfigured() {
+
+ basicSetup()
+ .withConfiguration(AutoConfigurations.of(TaskExecutionAutoConfiguration.class))
+ .withPropertyValues("spring.task.execution.shutdown.await-termination-period=10m")
+ .run(expect(Shutdown::getAwaitTerminationPeriod, Duration.ofMinutes(10)));
+ }
+
+ private ContextConsumer expect(Function extractor,
+ T expected) {
+
+ return context -> assertThat(context.getBean(TaskExecutionProperties.class).getShutdown())
+ .extracting(extractor)
+ .isEqualTo(expected);
+ }
+
+ private ApplicationContextRunner basicSetup() {
+
+ return new ApplicationContextRunner()
+ .withConfiguration(AutoConfigurations.of(EventPublicationConfiguration.class))
+ .withBean(EventPublicationRepository.class, () -> repository);
+ }
+}