From 417e1c1e8cc364f92044224da55235c1efd9ca88 Mon Sep 17 00:00:00 2001 From: Oliver Drotbohm Date: Sat, 22 Feb 2025 14:19:17 +0100 Subject: [PATCH] GH-1066 - Provide abstraction to access ApplicationModuleIdentifiers in order of module dependency. Introduce ApplicationModuleIdentifiers as abstraction for an ordered collection of application module identifiers. Introduced ApplicationModuleMetadata as abstraction for the generated metadata (usually located in META-INF/spring-modulith/application-modules.json) to expose the information we currently need to downstream infrastructure components. Migrated the components introduced to execute ApplicationModuleInitializers to AMA and adapt auto-configuration accordingly. --- .../core/ApplicationModuleIdentifiers.java | 88 +++++++++++ .../ApplicationModuleMetadata.java | 139 ++++++++++++++++++ ...ltApplicationModuleInitializerInvoker.java | 7 +- ...ngApplicationModuleInitializerAdapter.java | 4 +- ...ssingRuntimeDependencyFailureAnalyzer.java | 1 - ...edApplicationModuleInitializerInvoker.java | 33 ++--- ...pringModulithRuntimeAutoConfiguration.java | 32 +++- .../java/example/moduleA/ModuleAType.java | 14 +- ...tionModuleInitializerInvokerUnitTests.java | 7 +- ...timeAutoConfigurationIntegrationTests.java | 102 ++++++++++--- .../spring-modulith}/application-modules.json | 3 + 11 files changed, 376 insertions(+), 54 deletions(-) create mode 100644 spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModuleIdentifiers.java create mode 100644 spring-modulith-runtime/src/main/java/org/springframework/modulith/runtime/autoconfigure/ApplicationModuleMetadata.java rename spring-modulith-runtime/src/test/resources/{ => META-INF/spring-modulith}/application-modules.json (69%) diff --git a/spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModuleIdentifiers.java b/spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModuleIdentifiers.java new file mode 100644 index 00000000..9a3a9185 --- /dev/null +++ b/spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModuleIdentifiers.java @@ -0,0 +1,88 @@ +/* + * Copyright 2025 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.core; + +import java.util.Iterator; +import java.util.List; +import java.util.function.Supplier; +import java.util.stream.Stream; + +import org.springframework.util.Assert; + +/** + * {@link ApplicationModuleIdentifier}s that allow iteration in the order provided by the sources. + * + * @author Oliver Drotbohm + * @since 1.4 + */ +public class ApplicationModuleIdentifiers implements Iterable { + + private final Supplier> source; + + /** + * Creates a new {@link ApplicationModuleIdentifiers} instance for the given source. + * + * @param source must not be {@literal null}. + */ + private ApplicationModuleIdentifiers(Supplier> source) { + + Assert.notNull(source, "Source must not be null!"); + + this.source = source; + } + + /** + * Creates a new {@link ApplicationModuleIdentifiers} from the given ApplicationModules. + * + * @param modules must not be {@literal null}. + * @return will never be {@literal null}. + */ + public static ApplicationModuleIdentifiers of(ApplicationModules modules) { + + Assert.notNull(modules, "ApplicationModules must not be null!"); + + return new ApplicationModuleIdentifiers(() -> modules.stream() + .map(ApplicationModule::getIdentifier)); + } + + /** + * Creates a new {@link ApplicationModuleIdentifiers} for the given source identifiers. + * + * @param identifiers will never be {@literal null}. + * @return will never be {@literal null}. + */ + public static ApplicationModuleIdentifiers of(List identifiers) { + return new ApplicationModuleIdentifiers(() -> identifiers.stream()); + } + + /** + * Creates a new {@link Stream} of {@link ApplicationModuleIdentifier}s. + * + * @return will never be {@literal null}. + */ + public Stream stream() { + return source.get(); + } + + /* + * (non-Javadoc) + * @see java.lang.Iterable#iterator() + */ + @Override + public Iterator iterator() { + return stream().iterator(); + } +} diff --git a/spring-modulith-runtime/src/main/java/org/springframework/modulith/runtime/autoconfigure/ApplicationModuleMetadata.java b/spring-modulith-runtime/src/main/java/org/springframework/modulith/runtime/autoconfigure/ApplicationModuleMetadata.java new file mode 100644 index 00000000..b5f7c1ae --- /dev/null +++ b/spring-modulith-runtime/src/main/java/org/springframework/modulith/runtime/autoconfigure/ApplicationModuleMetadata.java @@ -0,0 +1,139 @@ +/* + * Copyright 2025 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.runtime.autoconfigure; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.io.Resource; +import org.springframework.modulith.core.ApplicationModuleIdentifier; +import org.springframework.util.Assert; + +import com.jayway.jsonpath.DocumentContext; +import com.jayway.jsonpath.JsonPath; + +/** + * An abstraction for the data captured in the application module metadata file (typically + * {@value org.springframework.modulith.core.util.ApplicationModulesExporter#DEFAULT_LOCATION}). + * + * @author Oliver Drotbohm + * @since 1.4 + * @see org.springframework.modulith.core.util.ApplicationModulesExporter#DEFAULT_LOCATION + */ +class ApplicationModuleMetadata { + + private static final ApplicationModuleMetadata NONE = new ApplicationModuleMetadata(); + + private static final Logger LOGGER = LoggerFactory.getLogger(ApplicationModuleMetadata.class); + + /** + * Creates a new {@link ApplicationModuleMetadata} for the given {@link Resource}. + * + * @param resource must not be {@literal null}. + * @return will never be {@literal null}. + */ + public static ApplicationModuleMetadata of(Resource resource) { + + Assert.notNull(resource, "Resource must not be null!"); + + if (!resource.exists()) { + LOGGER.debug("Did not find application module metadata in {}.", resource.getDescription()); + return NONE; + } + + return new ResourceBasedApplicationModuleMetadata(resource); + } + + /** + * Returns whether the metadata is present at all. + */ + public boolean isPresent() { + return false; + } + + /** + * Returns all {@link ApplicationModuleIdentifier}s. + * + * @return will never be {@literal null}. + */ + public List getIdentifiers() { + return Collections.emptyList(); + } + + /** + * Returns the names of the types registered as {@link org.springframework.modulith.ApplicationModuleInitializer}. + * + * @return will never be {@literal null}. + */ + public List getInitializerTypeNames() { + return Collections.emptyList(); + } + + private static class ResourceBasedApplicationModuleMetadata extends ApplicationModuleMetadata { + + private final DocumentContext document; + + public ResourceBasedApplicationModuleMetadata(Resource metadata) { + + var description = metadata.getDescription(); + + Assert.isTrue(metadata.exists(), () -> "Resource %s does not exist!".formatted(description)); + + LOGGER.debug("Using application module metadata located in {}.", description); + + try { + this.document = JsonPath.parse(metadata.getFile()); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.modulith.runtime.autoconfigure.ApplicationModuleMetadata#isPresent() + */ + @Override + public boolean isPresent() { + return true; + } + + /* + * (non-Javadoc) + * @see org.springframework.modulith.runtime.autoconfigure.ApplicationModuleMetadata#getIdentifiers() + */ + @Override + public List getIdentifiers() { + + return document.> read("$.keys()").stream() + .map(ApplicationModuleIdentifier::of) + .toList(); + } + + /* + * (non-Javadoc) + * @see org.springframework.modulith.runtime.autoconfigure.ApplicationModuleMetadata#getInitializerTypeNames() + */ + @Override + public List getInitializerTypeNames() { + return document.> read("$..initializers[*]"); + } + } +} diff --git a/spring-modulith-runtime/src/main/java/org/springframework/modulith/runtime/autoconfigure/DefaultApplicationModuleInitializerInvoker.java b/spring-modulith-runtime/src/main/java/org/springframework/modulith/runtime/autoconfigure/DefaultApplicationModuleInitializerInvoker.java index e4fbfb51..b85724ae 100644 --- a/spring-modulith-runtime/src/main/java/org/springframework/modulith/runtime/autoconfigure/DefaultApplicationModuleInitializerInvoker.java +++ b/spring-modulith-runtime/src/main/java/org/springframework/modulith/runtime/autoconfigure/DefaultApplicationModuleInitializerInvoker.java @@ -15,7 +15,6 @@ */ package org.springframework.modulith.runtime.autoconfigure; -import java.util.function.Supplier; import java.util.stream.Stream; import org.springframework.modulith.ApplicationModuleInitializer; @@ -26,12 +25,12 @@ import org.springframework.modulith.core.ApplicationModules; */ class DefaultApplicationModuleInitializerInvoker implements ApplicationModuleInitializerInvoker { - private final Supplier modules; + private final ApplicationModules modules; /** * @param modules */ - DefaultApplicationModuleInitializerInvoker(Supplier modules) { + DefaultApplicationModuleInitializerInvoker(ApplicationModules modules) { this.modules = modules; } @@ -43,8 +42,6 @@ class DefaultApplicationModuleInitializerInvoker implements ApplicationModuleIni @Override public void invokeInitializers(Stream initializers) { - var modules = this.modules.get(); - initializers .sorted(modules.getComparator()) // .map(it -> LoggingApplicationModuleInitializerAdapter.of(it, modules)) diff --git a/spring-modulith-runtime/src/main/java/org/springframework/modulith/runtime/autoconfigure/LoggingApplicationModuleInitializerAdapter.java b/spring-modulith-runtime/src/main/java/org/springframework/modulith/runtime/autoconfigure/LoggingApplicationModuleInitializerAdapter.java index 6bce4f5c..a00dfe71 100644 --- a/spring-modulith-runtime/src/main/java/org/springframework/modulith/runtime/autoconfigure/LoggingApplicationModuleInitializerAdapter.java +++ b/spring-modulith-runtime/src/main/java/org/springframework/modulith/runtime/autoconfigure/LoggingApplicationModuleInitializerAdapter.java @@ -52,7 +52,7 @@ class LoggingApplicationModuleInitializerAdapter implements ApplicationModuleIni public static ApplicationModuleInitializer of(ApplicationModuleInitializer initializer, ApplicationModules modules) { - if (!LoggerFactory.getLogger(initializer.getClass()).isDebugEnabled()) { + if (!LOGGER.isDebugEnabled()) { return initializer; } @@ -68,7 +68,7 @@ class LoggingApplicationModuleInitializerAdapter implements ApplicationModuleIni public static ApplicationModuleInitializer of(ApplicationModuleInitializer initializer) { - if (!LoggerFactory.getLogger(initializer.getClass()).isDebugEnabled()) { + if (!LOGGER.isDebugEnabled()) { return initializer; } diff --git a/spring-modulith-runtime/src/main/java/org/springframework/modulith/runtime/autoconfigure/MissingRuntimeDependencyFailureAnalyzer.java b/spring-modulith-runtime/src/main/java/org/springframework/modulith/runtime/autoconfigure/MissingRuntimeDependencyFailureAnalyzer.java index b412352b..501806f8 100644 --- a/spring-modulith-runtime/src/main/java/org/springframework/modulith/runtime/autoconfigure/MissingRuntimeDependencyFailureAnalyzer.java +++ b/spring-modulith-runtime/src/main/java/org/springframework/modulith/runtime/autoconfigure/MissingRuntimeDependencyFailureAnalyzer.java @@ -17,7 +17,6 @@ package org.springframework.modulith.runtime.autoconfigure; import org.springframework.boot.diagnostics.AbstractFailureAnalyzer; import org.springframework.boot.diagnostics.FailureAnalysis; -import org.springframework.boot.diagnostics.FailureAnalyzer; /** * {@link FailureAnalyzer} for {@link MissingRuntimeDependency}. diff --git a/spring-modulith-runtime/src/main/java/org/springframework/modulith/runtime/autoconfigure/PrecomputedApplicationModuleInitializerInvoker.java b/spring-modulith-runtime/src/main/java/org/springframework/modulith/runtime/autoconfigure/PrecomputedApplicationModuleInitializerInvoker.java index 7f42c095..5abc2182 100644 --- a/spring-modulith-runtime/src/main/java/org/springframework/modulith/runtime/autoconfigure/PrecomputedApplicationModuleInitializerInvoker.java +++ b/spring-modulith-runtime/src/main/java/org/springframework/modulith/runtime/autoconfigure/PrecomputedApplicationModuleInitializerInvoker.java @@ -15,35 +15,24 @@ */ package org.springframework.modulith.runtime.autoconfigure; -import java.io.IOException; -import java.io.UncheckedIOException; import java.util.List; import java.util.Optional; import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; -import org.springframework.core.io.Resource; import org.springframework.modulith.ApplicationModuleInitializer; import org.springframework.util.Assert; -import com.jayway.jsonpath.JsonPath; - class PrecomputedApplicationModuleInitializerInvoker implements ApplicationModuleInitializerInvoker { - private List plan; + private final List initializerTypeNames; - public PrecomputedApplicationModuleInitializerInvoker(Resource metadata) { + public PrecomputedApplicationModuleInitializerInvoker(ApplicationModuleMetadata metadata) { - Assert.isTrue(metadata.exists(), () -> "Resource %s does not exist!".formatted(metadata.getDescription())); + Assert.isTrue(metadata.isPresent(), "ApplicationModuleMetadata not present!"); - try (var stream = metadata.getInputStream()) { - - this.plan = JsonPath.parse(stream).> read("$..initializers[*]"); - - } catch (IOException e) { - throw new UncheckedIOException(e); - } + this.initializerTypeNames = metadata.getInitializerTypeNames(); } /* @@ -56,11 +45,17 @@ class PrecomputedApplicationModuleInitializerInvoker implements ApplicationModul var map = initializers .collect(Collectors.toMap(it -> it.getClass().getName(), Function.identity())); - plan.stream() - .map(map::get) + triggerInitialization(initializerTypeNames.stream() + .map(map::remove) .map(Optional::ofNullable) - .flatMap(Optional::stream) - .map(LoggingApplicationModuleInitializerAdapter::of) + .flatMap(Optional::stream)); + + triggerInitialization(map.values().stream()); + } + + private void triggerInitialization(Stream initializers) { + + initializers.map(LoggingApplicationModuleInitializerAdapter::of) .forEach(ApplicationModuleInitializer::initialize); } } diff --git a/spring-modulith-runtime/src/main/java/org/springframework/modulith/runtime/autoconfigure/SpringModulithRuntimeAutoConfiguration.java b/spring-modulith-runtime/src/main/java/org/springframework/modulith/runtime/autoconfigure/SpringModulithRuntimeAutoConfiguration.java index e6c378bc..b6e1396a 100644 --- a/spring-modulith-runtime/src/main/java/org/springframework/modulith/runtime/autoconfigure/SpringModulithRuntimeAutoConfiguration.java +++ b/spring-modulith-runtime/src/main/java/org/springframework/modulith/runtime/autoconfigure/SpringModulithRuntimeAutoConfiguration.java @@ -36,6 +36,7 @@ import org.springframework.core.task.AsyncTaskExecutor; import org.springframework.core.task.SimpleAsyncTaskExecutor; import org.springframework.modulith.ApplicationModuleInitializer; import org.springframework.modulith.core.ApplicationModule; +import org.springframework.modulith.core.ApplicationModuleIdentifiers; import org.springframework.modulith.core.ApplicationModules; import org.springframework.modulith.core.ApplicationModulesFactory; import org.springframework.modulith.core.util.ApplicationModulesExporter; @@ -83,16 +84,39 @@ class SpringModulithRuntimeAutoConfiguration { return __ -> invoker.invokeInitializers(initializers.stream()); } + /** + * {@link ApplicationModuleMetadata} obtained from the Spring Modulith metadata located at + * {@value ApplicationModulesExporter#DEFAULT_LOCATION}. + * + * @param metadata will never be {@literal null}. + * @return will never be {@literal null}. + */ + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + static ApplicationModuleMetadata applicationModuleMetadata( + @Value("classpath:" + ApplicationModulesExporter.DEFAULT_LOCATION) Resource metadata) { + return ApplicationModuleMetadata.of(metadata); + } + @Bean @Role(BeanDefinition.ROLE_INFRASTRUCTURE) @ConditionalOnBean(ApplicationModuleInitializer.class) - static ApplicationModuleInitializerInvoker applicationModuleInitializerInvoker( - @Value("classpath:" + ApplicationModulesExporter.DEFAULT_LOCATION) Resource metadata, + static ApplicationModuleInitializerInvoker applicationModuleInitializerInvoker(ApplicationModuleMetadata metadata, ObjectProvider runtime) { - return metadata.exists() + return metadata.isPresent() ? new PrecomputedApplicationModuleInitializerInvoker(metadata) - : new DefaultApplicationModuleInitializerInvoker(runtime.getIfAvailable()); + : new DefaultApplicationModuleInitializerInvoker(runtime.getObject().get()); + } + + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + ApplicationModuleIdentifiers applicationModuleIdentifiers(ApplicationModuleMetadata metadata, + ObjectProvider runtime) { + + return metadata.isPresent() + ? ApplicationModuleIdentifiers.of(metadata.getIdentifiers()) + : ApplicationModuleIdentifiers.of(runtime.getObject().get()); } private static class ApplicationModulesBootstrap { diff --git a/spring-modulith-runtime/src/test/java/example/moduleA/ModuleAType.java b/spring-modulith-runtime/src/test/java/example/moduleA/ModuleAType.java index b8dcedfd..ce4ae804 100644 --- a/spring-modulith-runtime/src/test/java/example/moduleA/ModuleAType.java +++ b/spring-modulith-runtime/src/test/java/example/moduleA/ModuleAType.java @@ -15,7 +15,19 @@ */ package example.moduleA; +import java.time.LocalDateTime; + +import org.springframework.modulith.ApplicationModuleInitializer; + /** * @author Oliver Drotbohm */ -public class ModuleAType {} +public class ModuleAType implements ApplicationModuleInitializer { + + public LocalDateTime initialized; + + @Override + public void initialize() { + this.initialized = LocalDateTime.now(); + } +} diff --git a/spring-modulith-runtime/src/test/java/org/springframework/modulith/runtime/autoconfigure/PrecomputedApplicationModuleInitializerInvokerUnitTests.java b/spring-modulith-runtime/src/test/java/org/springframework/modulith/runtime/autoconfigure/PrecomputedApplicationModuleInitializerInvokerUnitTests.java index 00f32284..88e13a2b 100644 --- a/spring-modulith-runtime/src/test/java/org/springframework/modulith/runtime/autoconfigure/PrecomputedApplicationModuleInitializerInvokerUnitTests.java +++ b/spring-modulith-runtime/src/test/java/org/springframework/modulith/runtime/autoconfigure/PrecomputedApplicationModuleInitializerInvokerUnitTests.java @@ -24,6 +24,7 @@ import java.util.stream.Stream; import org.junit.jupiter.api.Test; import org.springframework.core.io.ClassPathResource; import org.springframework.modulith.ApplicationModuleInitializer; +import org.springframework.modulith.core.util.ApplicationModulesExporter; /** * @author Oliver Drotbohm @@ -33,8 +34,10 @@ class PrecomputedApplicationModuleInitializerInvokerUnitTests { @Test void ordersInitializersBasedOnPrecomputedMetadata() throws IOException { - var resource = new ClassPathResource("application-modules.json"); - var invoker = new PrecomputedApplicationModuleInitializerInvoker(resource); + var resource = new ClassPathResource(ApplicationModulesExporter.DEFAULT_LOCATION); + var metadata = ApplicationModuleMetadata.of(resource); + + var invoker = new PrecomputedApplicationModuleInitializerInvoker(metadata); var second = new SecondApplicationModuleInitializer(); var first = new FirstApplicationModuleInitializer(); diff --git a/spring-modulith-runtime/src/test/java/org/springframework/modulith/runtime/autoconfigure/SpringModulithRuntimeAutoConfigurationIntegrationTests.java b/spring-modulith-runtime/src/test/java/org/springframework/modulith/runtime/autoconfigure/SpringModulithRuntimeAutoConfigurationIntegrationTests.java index 8cbd1414..d6131615 100644 --- a/spring-modulith-runtime/src/test/java/org/springframework/modulith/runtime/autoconfigure/SpringModulithRuntimeAutoConfigurationIntegrationTests.java +++ b/spring-modulith-runtime/src/test/java/org/springframework/modulith/runtime/autoconfigure/SpringModulithRuntimeAutoConfigurationIntegrationTests.java @@ -18,6 +18,9 @@ package org.springframework.modulith.runtime.autoconfigure; import static org.assertj.core.api.Assertions.*; import static org.mockito.Mockito.*; +import example.moduleA.ModuleAType; + +import java.time.LocalDateTime; import java.util.HashMap; import java.util.Map; @@ -34,7 +37,11 @@ import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.context.event.ApplicationStartedEvent; import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.core.io.ClassPathResource; import org.springframework.modulith.ApplicationModuleInitializer; +import org.springframework.modulith.core.ApplicationModuleIdentifier; +import org.springframework.modulith.core.ApplicationModuleIdentifiers; +import org.springframework.modulith.core.util.ApplicationModulesExporter; import org.springframework.modulith.runtime.ApplicationModulesRuntime; import org.springframework.modulith.runtime.ApplicationRuntime; @@ -51,12 +58,19 @@ class SpringModulithRuntimeAutoConfigurationIntegrationTests { @SpringBootApplication static class SampleApp {} + InitializationTracker tracker = new InitializationTracker(); + + ApplicationContextRunner runner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(SpringModulithRuntimeAutoConfiguration.class)) + .withBean(InitializationTracker.class, () -> tracker); + + ClassLoader withoutMetadata = new FilteredClassLoader( + new ClassPathResource(ApplicationModulesExporter.DEFAULT_LOCATION)); + @Test // GH-87 void bootstrapRegistersRuntimeInstances() { - new ApplicationContextRunner() - .withUserConfiguration(SampleApp.class) - .withConfiguration(AutoConfigurations.of(SpringModulithRuntimeAutoConfiguration.class)) + runner.withUserConfiguration(SampleApp.class) .run(context -> { assertThat(context.getBean(ApplicationRuntime.class)).isNotNull(); assertThat(context.getBean(ApplicationModulesRuntime.class)).isNotNull(); @@ -66,9 +80,7 @@ class SpringModulithRuntimeAutoConfigurationIntegrationTests { @Test // GH-160 void missingArchUnitRuntimeDependencyEscalatesOnContextStartup() { - new ApplicationContextRunner() - .withUserConfiguration(SampleApp.class) - .withConfiguration(AutoConfigurations.of(SpringModulithRuntimeAutoConfiguration.class)) + runner.withUserConfiguration(SampleApp.class) .withClassLoader(new FilteredClassLoader(ClassFileImporter.class)) .run(context -> { assertThat(context).hasFailed(); @@ -79,29 +91,72 @@ class SpringModulithRuntimeAutoConfigurationIntegrationTests { } @Test // GH-375 - void registersInitializingListenerIfInitializersPresent() { + void registersIntializerInvokerIfInitializersPresent() { - var tracker = new InitializationTracker(); + var unlisted = new ModuleAType(); + var listed = new SampleInitializer(); - var beanName = "applicationModuleInitializingListener"; - var runner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(SpringModulithRuntimeAutoConfiguration.class)) - .withBean(InitializationTracker.class, () -> tracker); + var runner = this.runner.withUserConfiguration(SampleApp.class) + .withBean(ModuleAType.class, () -> unlisted) + .withBean(SampleInitializer.class, () -> listed); - // No initializer -> no listener runner.run(context -> { - assertThat(context).doesNotHaveBean(beanName); + assertThat(context).hasSingleBean(PrecomputedApplicationModuleInitializerInvoker.class); + + context.publishEvent(mock(ApplicationStartedEvent.class)); + + // Initializer invoked + assertThat(unlisted.initialized).isNotNull(); + assertThat(listed.initialized).isNotNull(); + assertThat(listed.initialized).isBefore(unlisted.initialized); + tracker.assertBeanNotInitialized(ApplicationModulesRuntime.class); }); - // Initializer -> listener - runner.withBean(ApplicationModuleInitializer.class, () -> () -> {}) - .withBean(DummyApplication.class) + runner.withClassLoader(withoutMetadata) .run(context -> { + assertThat(context).hasSingleBean(DefaultApplicationModuleInitializerInvoker.class); + context.publishEvent(mock(ApplicationStartedEvent.class)); - assertThat(context).hasBean(beanName); + + tracker.assertBeanInitialized(ApplicationModulesRuntime.class); + }); + } + + @Test // GH-375 + void doesNotRegisterIntializerInvokerIfInitializersPresent() { + + runner.run(context -> { + + assertThat(context).doesNotHaveBean(ApplicationModuleInitializerInvoker.class); + + tracker.assertBeanNotInitialized(ApplicationModulesRuntime.class); + }); + } + + @Test // GH-1066 + void registersApplicationModuleIdentifiersWithoutInstantiatingApplicationModulesIfMetadataPresent() { + + runner.run(context -> { + + tracker.assertBeanNotInitialized(ApplicationModulesRuntime.class); + assertThat(context).hasSingleBean(ApplicationModuleIdentifiers.class); + + assertThat(context.getBean(ApplicationModuleIdentifiers.class)) + .extracting(ApplicationModuleIdentifier::toString) + .containsExactly("a", "b", "c"); + }); + + runner.withUserConfiguration(SampleApp.class) + .withClassLoader(withoutMetadata) + .run(context -> { + + tracker.assertBeanInitialized(ApplicationModulesRuntime.class); + assertThat(context).hasSingleBean(ApplicationModuleIdentifiers.class); + + assertThat(context.getBean(ApplicationModuleIdentifiers.class)).isEmpty(); }); } @@ -144,6 +199,13 @@ class SpringModulithRuntimeAutoConfigurationIntegrationTests { } } - @SpringBootApplication - static class DummyApplication {} + static class SampleInitializer implements ApplicationModuleInitializer { + + LocalDateTime initialized; + + @Override + public void initialize() { + this.initialized = LocalDateTime.now(); + } + } } diff --git a/spring-modulith-runtime/src/test/resources/application-modules.json b/spring-modulith-runtime/src/test/resources/META-INF/spring-modulith/application-modules.json similarity index 69% rename from spring-modulith-runtime/src/test/resources/application-modules.json rename to spring-modulith-runtime/src/test/resources/META-INF/spring-modulith/application-modules.json index 2d00a237..4483a27b 100644 --- a/spring-modulith-runtime/src/test/resources/application-modules.json +++ b/spring-modulith-runtime/src/test/resources/META-INF/spring-modulith/application-modules.json @@ -4,5 +4,8 @@ }, "b" : { "initializers" : [ "org.springframework.modulith.runtime.autoconfigure.PrecomputedApplicationModuleInitializerInvokerUnitTests$SecondApplicationModuleInitializer" ] + }, + "c" : { + "initializers" : [ "org.springframework.modulith.runtime.autoconfigure.SpringModulithRuntimeAutoConfigurationIntegrationTests$SampleInitializer" ] } }