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.
This commit is contained in:
Oliver Drotbohm
2025-02-22 14:19:17 +01:00
parent 7db3897b0f
commit 417e1c1e8c
11 changed files with 376 additions and 54 deletions

View File

@@ -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<ApplicationModuleIdentifier> {
private final Supplier<Stream<ApplicationModuleIdentifier>> source;
/**
* Creates a new {@link ApplicationModuleIdentifiers} instance for the given source.
*
* @param source must not be {@literal null}.
*/
private ApplicationModuleIdentifiers(Supplier<Stream<ApplicationModuleIdentifier>> 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<ApplicationModuleIdentifier> identifiers) {
return new ApplicationModuleIdentifiers(() -> identifiers.stream());
}
/**
* Creates a new {@link Stream} of {@link ApplicationModuleIdentifier}s.
*
* @return will never be {@literal null}.
*/
public Stream<ApplicationModuleIdentifier> stream() {
return source.get();
}
/*
* (non-Javadoc)
* @see java.lang.Iterable#iterator()
*/
@Override
public Iterator<ApplicationModuleIdentifier> iterator() {
return stream().iterator();
}
}

View File

@@ -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<ApplicationModuleIdentifier> 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<String> 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<ApplicationModuleIdentifier> getIdentifiers() {
return document.<Collection<String>> read("$.keys()").stream()
.map(ApplicationModuleIdentifier::of)
.toList();
}
/*
* (non-Javadoc)
* @see org.springframework.modulith.runtime.autoconfigure.ApplicationModuleMetadata#getInitializerTypeNames()
*/
@Override
public List<String> getInitializerTypeNames() {
return document.<List<String>> read("$..initializers[*]");
}
}
}

View File

@@ -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<ApplicationModules> modules;
private final ApplicationModules modules;
/**
* @param modules
*/
DefaultApplicationModuleInitializerInvoker(Supplier<ApplicationModules> modules) {
DefaultApplicationModuleInitializerInvoker(ApplicationModules modules) {
this.modules = modules;
}
@@ -43,8 +42,6 @@ class DefaultApplicationModuleInitializerInvoker implements ApplicationModuleIni
@Override
public void invokeInitializers(Stream<ApplicationModuleInitializer> initializers) {
var modules = this.modules.get();
initializers
.sorted(modules.getComparator()) //
.map(it -> LoggingApplicationModuleInitializerAdapter.of(it, modules))

View File

@@ -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;
}

View File

@@ -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}.

View File

@@ -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<String> plan;
private final List<String> 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).<List<String>> 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<ApplicationModuleInitializer> initializers) {
initializers.map(LoggingApplicationModuleInitializerAdapter::of)
.forEach(ApplicationModuleInitializer::initialize);
}
}

View File

@@ -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<ApplicationModulesRuntime> 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<ApplicationModulesRuntime> runtime) {
return metadata.isPresent()
? ApplicationModuleIdentifiers.of(metadata.getIdentifiers())
: ApplicationModuleIdentifiers.of(runtime.getObject().get());
}
private static class ApplicationModulesBootstrap {

View File

@@ -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();
}
}

View File

@@ -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();

View File

@@ -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();
}
}
}

View File

@@ -4,5 +4,8 @@
},
"b" : {
"initializers" : [ "org.springframework.modulith.runtime.autoconfigure.PrecomputedApplicationModuleInitializerInvokerUnitTests$SecondApplicationModuleInitializer" ]
},
"c" : {
"initializers" : [ "org.springframework.modulith.runtime.autoconfigure.SpringModulithRuntimeAutoConfigurationIntegrationTests$SampleInitializer" ]
}
}