Skip to content

Commit 12821b7

Browse files
committed
GH-1287 - Support for runtime application module verification.
The spring-modulith-runtime artifact now exposes a `spring.modulith.runtime.verification-enabled` property to initiate application module arrangement verification on application startup. The ApplicationModules instance is bootstrapped asynchronously and any failure is elevated onto the main application thread in the post-singleton-initialization phase. We also register a FailureAnalyzer implementation to give a bit of additional guidance on how to deal with the problem.
1 parent 81cc4c5 commit 12821b7

File tree

8 files changed

+215
-14
lines changed

8 files changed

+215
-14
lines changed

spring-modulith-runtime/pom.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,12 @@
6868
<scope>test</scope>
6969
</dependency>
7070

71+
<dependency>
72+
<groupId>org.springframework.boot</groupId>
73+
<artifactId>spring-boot-configuration-processor</artifactId>
74+
<optional>true</optional>
75+
</dependency>
76+
7177
</dependencies>
7278

7379
<build>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*
2+
* Copyright 2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.modulith.runtime.autoconfigure;
17+
18+
import org.springframework.boot.diagnostics.AbstractFailureAnalyzer;
19+
import org.springframework.boot.diagnostics.FailureAnalysis;
20+
import org.springframework.modulith.core.Violations;
21+
22+
/**
23+
* A {@link org.springframework.boot.diagnostics.FailureAnalyzer} to give explanation what's wrong when a runtime
24+
* application module verification fails.
25+
*
26+
* @author Oliver Drotbohm
27+
* @since 2.0
28+
* @soundtrack Martin Kohlstedt - ZIN (Flur)
29+
*/
30+
class FailedModuleVerificationFailureAnalyzer extends AbstractFailureAnalyzer<Violations> {
31+
32+
/*
33+
* (non-Javadoc)
34+
* @see org.springframework.boot.diagnostics.AbstractFailureAnalyzer#analyze(java.lang.Throwable, java.lang.Throwable)
35+
*/
36+
@Override
37+
protected FailureAnalysis analyze(Throwable rootFailure, Violations cause) {
38+
39+
var description = """
40+
Spring Modulith application module verification was enabled and failed! The following violations were detected:
41+
42+
%s
43+
""".formatted(cause.getMessage());
44+
45+
var action = """
46+
Please fix the architectural violations or disable the runtime verification by setting spring.modulith.runtime.verification-enabled to false or removing it entirely.
47+
""";
48+
49+
return new FailureAnalysis(description, action, cause);
50+
}
51+
}

spring-modulith-runtime/src/main/java/org/springframework/modulith/runtime/autoconfigure/SpringModulithRuntimeAutoConfiguration.java

Lines changed: 81 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,34 +15,43 @@
1515
*/
1616
package org.springframework.modulith.runtime.autoconfigure;
1717

18+
import java.util.concurrent.CompletableFuture;
19+
import java.util.concurrent.Executor;
20+
import java.util.function.Supplier;
21+
22+
import org.jspecify.annotations.Nullable;
1823
import org.slf4j.Logger;
1924
import org.slf4j.LoggerFactory;
25+
import org.springframework.beans.factory.BeanFactoryInitializer;
26+
import org.springframework.beans.factory.ListableBeanFactory;
2027
import org.springframework.beans.factory.ObjectProvider;
28+
import org.springframework.beans.factory.SmartInitializingSingleton;
2129
import org.springframework.beans.factory.annotation.Value;
2230
import org.springframework.beans.factory.config.BeanDefinition;
31+
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
2332
import org.springframework.boot.autoconfigure.AutoConfiguration;
2433
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
34+
import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty;
2535
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
2636
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
2737
import org.springframework.boot.context.event.ApplicationStartedEvent;
38+
import org.springframework.boot.context.properties.EnableConfigurationProperties;
2839
import org.springframework.context.ApplicationContext;
2940
import org.springframework.context.ApplicationListener;
3041
import org.springframework.context.annotation.Bean;
3142
import org.springframework.context.annotation.Lazy;
3243
import org.springframework.context.annotation.Role;
3344
import org.springframework.core.io.Resource;
3445
import org.springframework.core.io.support.SpringFactoriesLoader;
35-
import org.springframework.core.task.AsyncTaskExecutor;
36-
import org.springframework.core.task.SimpleAsyncTaskExecutor;
3746
import org.springframework.modulith.ApplicationModuleInitializer;
3847
import org.springframework.modulith.core.ApplicationModule;
3948
import org.springframework.modulith.core.ApplicationModuleIdentifiers;
4049
import org.springframework.modulith.core.ApplicationModules;
4150
import org.springframework.modulith.core.ApplicationModulesFactory;
51+
import org.springframework.modulith.core.VerificationOptions;
4252
import org.springframework.modulith.core.util.ApplicationModulesExporter;
4353
import org.springframework.modulith.runtime.ApplicationModulesRuntime;
4454
import org.springframework.modulith.runtime.ApplicationRuntime;
45-
import org.springframework.util.function.ThrowingSupplier;
4655

4756
/**
4857
* Auto-configuration to register an {@link ApplicationRuntime}, a {@link ApplicationModulesRuntime} and an
@@ -51,9 +60,10 @@
5160
* @author Oliver Drotbohm
5261
*/
5362
@AutoConfiguration
63+
@EnableConfigurationProperties(SpringModulithRuntimeProperties.class)
5464
class SpringModulithRuntimeAutoConfiguration {
5565

56-
private static final AsyncTaskExecutor EXECUTOR = new SimpleAsyncTaskExecutor();
66+
private static final Logger LOG = LoggerFactory.getLogger(SpringModulithRuntimeAutoConfiguration.class);
5767

5868
@Bean
5969
@Lazy
@@ -67,13 +77,17 @@ static ApplicationRuntime modulithsApplicationRuntime(ApplicationContext context
6777
@Lazy
6878
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
6979
@ConditionalOnMissingBean
70-
static ApplicationModulesRuntime modulesRuntime(ApplicationRuntime runtime) {
71-
72-
ThrowingSupplier<ApplicationModules> modules = () -> EXECUTOR
73-
.submit(() -> ApplicationModulesBootstrap.initializeApplicationModules(runtime.getMainApplicationClass()))
74-
.get();
80+
static ApplicationModulesRuntime modulesRuntime(ApplicationModulesBootstrap bootstrap, ApplicationRuntime runtime) {
81+
return new ApplicationModulesRuntime(() -> bootstrap.getApplicationModules().join(), runtime);
82+
}
7583

76-
return new ApplicationModulesRuntime(modules, runtime);
84+
@Bean
85+
@Lazy
86+
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
87+
@ConditionalOnMissingBean
88+
static ApplicationModulesBootstrap applicationModulesInitializer(ApplicationRuntime runtime,
89+
ConfigurableBeanFactory factory) {
90+
return new ApplicationModulesBootstrap(runtime.getMainApplicationClass(), factory.getBootstrapExecutor());
7791
}
7892

7993
@Bean
@@ -84,6 +98,15 @@ static ApplicationListener<ApplicationStartedEvent> applicationModuleInitializin
8498
return __ -> invoker.invokeInitializers(initializers.stream());
8599
}
86100

101+
@Bean
102+
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
103+
@ConditionalOnBooleanProperty(value = "spring.modulith.runtime.verification-enabled", matchIfMissing = false)
104+
static RuntimeApplicationModuleVerifier applicationModuleVerifier(ApplicationModulesBootstrap bootstrap,
105+
ObjectProvider<VerificationOptions> verification) {
106+
107+
return new RuntimeApplicationModuleVerifier(bootstrap.getApplicationModules(), verification);
108+
}
109+
87110
/**
88111
* {@link ApplicationModuleMetadata} obtained from the Spring Modulith metadata located at
89112
* {@value ApplicationModulesExporter#DEFAULT_LOCATION}.
@@ -119,11 +142,13 @@ ApplicationModuleIdentifiers applicationModuleIdentifiers(ApplicationModuleMetad
119142
: ApplicationModuleIdentifiers.of(runtime.getObject().get());
120143
}
121144

122-
private static class ApplicationModulesBootstrap {
145+
static class ApplicationModulesBootstrap {
123146

124147
private static final Logger LOGGER = LoggerFactory.getLogger(ApplicationModulesBootstrap.class);
125148
private static final ApplicationModulesFactory BOOTSTRAP;
126149

150+
private final CompletableFuture<ApplicationModules> modules;
151+
127152
static {
128153

129154
var factories = SpringFactoriesLoader.loadFactories(ApplicationModulesFactory.class,
@@ -132,6 +157,19 @@ private static class ApplicationModulesBootstrap {
132157
BOOTSTRAP = !factories.isEmpty() ? factories.get(0) : ApplicationModulesFactory.defaultFactory();
133158
}
134159

160+
ApplicationModulesBootstrap(Class<?> applicationMainClass, @Nullable Executor executor) {
161+
162+
Supplier<ApplicationModules> supplier = () -> initializeApplicationModules(applicationMainClass);
163+
164+
this.modules = executor == null
165+
? CompletableFuture.supplyAsync(supplier)
166+
: CompletableFuture.supplyAsync(supplier, executor);
167+
}
168+
169+
CompletableFuture<ApplicationModules> getApplicationModules() {
170+
return modules;
171+
}
172+
135173
static ApplicationModules initializeApplicationModules(Class<?> applicationMainClass) {
136174

137175
LOGGER.debug("Obtaining Spring Modulith application modules…");
@@ -171,4 +209,36 @@ static class ArchUnitRuntimeDependencyMissingConfiguration {
171209
throw new MissingRuntimeDependency(DESCRIPTION, SUGGESTED_ACTION);
172210
}
173211
}
212+
213+
/**
214+
* A component to verify the application module arrangement at runtime.
215+
*
216+
* @author Oliver Drotbohm
217+
* @since 2.0
218+
*/
219+
static class RuntimeApplicationModuleVerifier
220+
implements BeanFactoryInitializer<ListableBeanFactory>, SmartInitializingSingleton {
221+
222+
private final CompletableFuture<Void> modules;
223+
224+
RuntimeApplicationModuleVerifier(CompletableFuture<ApplicationModules> modules,
225+
ObjectProvider<VerificationOptions> verification) {
226+
227+
this.modules = modules.thenAccept(it -> {
228+
it.verify(verification.getIfAvailable(VerificationOptions::defaults));
229+
LOG.info("Spring Modulith application module verification completed successfully.");
230+
});
231+
}
232+
233+
/*
234+
* (non-Javadoc)
235+
* @see org.springframework.beans.factory.SmartInitializingSingleton#afterSingletonsInstantiated()
236+
*/
237+
@Override
238+
public void afterSingletonsInstantiated() {
239+
modules.join();
240+
}
241+
242+
public void initialize(ListableBeanFactory beanFactory) {};
243+
}
174244
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*
2+
* Copyright 2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.modulith.runtime.autoconfigure;
17+
18+
import org.springframework.boot.context.properties.ConfigurationProperties;
19+
import org.springframework.boot.context.properties.bind.ConstructorBinding;
20+
import org.springframework.boot.context.properties.bind.DefaultValue;
21+
22+
/**
23+
* Configuration properties for Spring modulith's runtime support.
24+
*
25+
* @author Oliver Drotbohm
26+
* @since 2.0
27+
* @soundtrack Phil Siemers - Was wenn doch (Was wenn doch)
28+
*/
29+
@ConfigurationProperties("spring.modulith.runtime")
30+
class SpringModulithRuntimeProperties {
31+
32+
/**
33+
* Whether the application modules arrangement is supposed to be verified on startup. Defaults to {@literal false}.
34+
*/
35+
private final boolean verificationEnabled;
36+
37+
@ConstructorBinding
38+
SpringModulithRuntimeProperties(@DefaultValue("false") boolean verificationEnabled) {
39+
this.verificationEnabled = verificationEnabled;
40+
}
41+
42+
public boolean isVerificationEnabled() {
43+
return verificationEnabled;
44+
}
45+
}
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
# Failure analyzers
22
org.springframework.boot.diagnostics.FailureAnalyzer=\
3-
org.springframework.modulith.runtime.autoconfigure.MissingRuntimeDependencyFailureAnalyzer
3+
org.springframework.modulith.runtime.autoconfigure.MissingRuntimeDependencyFailureAnalyzer,\
4+
org.springframework.modulith.runtime.autoconfigure.FailedModuleVerificationFailureAnalyzer

spring-modulith-runtime/src/test/java/org/springframework/modulith/runtime/autoconfigure/SpringModulithRuntimeAutoConfigurationIntegrationTests.java

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@
4444
import org.springframework.modulith.core.util.ApplicationModulesExporter;
4545
import org.springframework.modulith.runtime.ApplicationModulesRuntime;
4646
import org.springframework.modulith.runtime.ApplicationRuntime;
47+
import org.springframework.modulith.runtime.autoconfigure.SpringModulithRuntimeAutoConfiguration.ApplicationModulesBootstrap;
48+
import org.springframework.modulith.runtime.autoconfigure.SpringModulithRuntimeAutoConfiguration.RuntimeApplicationModuleVerifier;
4749

4850
import com.tngtech.archunit.core.importer.ClassFileImporter;
4951

@@ -72,8 +74,8 @@ void bootstrapRegistersRuntimeInstances() {
7274

7375
runner.withUserConfiguration(SampleApp.class)
7476
.run(context -> {
75-
assertThat(context.getBean(ApplicationRuntime.class)).isNotNull();
76-
assertThat(context.getBean(ApplicationModulesRuntime.class)).isNotNull();
77+
assertThat(context).hasSingleBean(ApplicationRuntime.class);
78+
assertThat(context).hasSingleBean(ApplicationModulesRuntime.class);
7779
});
7880
}
7981

@@ -160,6 +162,27 @@ void registersApplicationModuleIdentifiersWithoutInstantiatingApplicationModules
160162
});
161163
}
162164

165+
@Test // GH-1287
166+
void doesNotActivateVerifyingListenerByDefault() {
167+
168+
runner.run(context -> {
169+
assertThat(context).hasSingleBean(SpringModulithRuntimeProperties.class);
170+
assertThat(context).doesNotHaveBean(RuntimeApplicationModuleVerifier.class);
171+
tracker.assertBeanNotInitialized(ApplicationModulesBootstrap.class);
172+
});
173+
}
174+
175+
@Test // GH-1287
176+
void registersVerifyingApplicationListenerIfConfigured() {
177+
178+
runner.withUserConfiguration(SampleApp.class)
179+
.withPropertyValues("spring.modulith.runtime.verification-enabled=true")
180+
.run(context -> {
181+
assertThat(context).hasSingleBean(RuntimeApplicationModuleVerifier.class);
182+
tracker.assertBeanInitialized(ApplicationModulesBootstrap.class);
183+
});
184+
}
185+
163186
static class InitializationTracker implements BeanPostProcessor, BeanFactoryAware {
164187

165188
private final Map<String, Class<?>> initializedBeans = new HashMap<>();

src/docs/antora/modules/ROOT/pages/appendix.adoc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,10 @@ Usually not recommended in multi-instance deployments as other instances might s
122122
|`false`
123123
|Deprecated as of 1.3. Prefer `spring.modulith.events.republish-outstanding-events-on-restart`.
124124

125+
|`spring.modulith.runtime.verification-enabled`
126+
|`false`
127+
|Whether to verify the application module arrangement during application startup. Requires the `spring-modulith-runtime` artifact on the classpath. For more information, see xref:runtime.adoc#setup[the section on Spring Modulith's runtime support] for details.
128+
125129
|`spring.modulith.test.file-modification-detector`
126130
|none
127131
|This can either be one of the predefined values `uncommitted-changes`, `reference-commit`, `default` or the fully-qualified class name of a `FileModificationDetector` that will be used to inspect which files of the projects have been changed.

src/docs/antora/modules/ROOT/pages/runtime.adoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ Adding this JAR will cause Spring Boot auto-configuration to run that registers
3939

4040
* An `ApplicationModulesRuntime` that allows to access the `ApplicationModules`.
4141
* A `SpringBootApplicationRuntime` to back the former bean to detect the main application class.
42+
* A `RuntimeApplicationModuleVerifier` to verify the application module arrangement on startup and abort it if violations are detected, only if `spring.modulith.runtime.verification-enabled` is configured to `true`.
4243
* An event listener for https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#features.spring-application.application-events-and-listeners[`ApplicationStartedEvent`]s that will invoke xref:runtime.adoc#application-module-initializer[`ApplicationModuleInitializer`] beans defined in the application context.
4344

4445
[[application-module-initializer]]

0 commit comments

Comments
 (0)