Skip to content

Commit 8326923

Browse files
committed
GH-1185 - Support for customizing additional verifications.
ApplicationModules.verify(…) and ….detectViolations(…) now has overloads taking a newly introduced VerificationOptions instance that allows registering additional ArchRules to be executed as part of the verification or disable / replace them entirely.
1 parent dafa45f commit 8326923

File tree

5 files changed

+269
-22
lines changed

5 files changed

+269
-22
lines changed

spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModules.java

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@
3535
import org.springframework.aot.generate.Generated;
3636
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
3737
import org.springframework.lang.Nullable;
38-
import org.springframework.modulith.core.Types.JMoleculesTypes;
3938
import org.springframework.util.Assert;
4039
import org.springframework.util.ClassUtils;
4140
import org.springframework.util.function.SingletonSupplier;
@@ -442,12 +441,23 @@ public Optional<ApplicationModule> getModuleForPackage(String name) {
442441
* @return will never be {@literal null}.
443442
*/
444443
public ApplicationModules verify() {
444+
return verify(VerificationOptions.defaults());
445+
}
446+
447+
/**
448+
* Execute all verifications to be applied considering the given {@link VerificationOptions}, unless the verification
449+
* has been executed before.
450+
*
451+
* @return will never be {@literal null}.
452+
* @since 1.4
453+
*/
454+
public ApplicationModules verify(VerificationOptions options) {
445455

446456
if (verified) {
447457
return this;
448458
}
449459

450-
Violations violations = detectViolations();
460+
Violations violations = detectViolations(options);
451461

452462
this.verified = true;
453463

@@ -464,13 +474,26 @@ public ApplicationModules verify() {
464474
* @see Violations#throwIfPresent()
465475
*/
466476
public Violations detectViolations() {
477+
return detectViolations(VerificationOptions.defaults());
478+
}
479+
480+
/**
481+
* Executes all verifications to be applied considering the given {@link VerificationOptions} and returns
482+
* {@link Violations} if any occured. Will always execute the verifications in contrast to {@link #verify()} which
483+
* just runs once.
484+
*
485+
* @return will never be {@literal null}.
486+
* @see Violations#throwIfPresent()
487+
* @since 1.4
488+
*/
489+
public Violations detectViolations(VerificationOptions options) {
467490

468491
var cycleViolations = rootPackages.stream() //
469492
.map(this::assertNoCyclesFor) //
470493
.flatMap(it -> it.getDetails().stream()) //
471494
.collect(toViolations());
472495

473-
var jMoleculesViolations = JMoleculesTypes.getRules().stream()
496+
var additionalViolations = options.getAdditionalVerifications().stream()
474497
.map(it -> it.evaluate(allClasses))
475498
.map(EvaluationResult::getFailureReport)
476499
.flatMap(it -> it.getDetails().stream())
@@ -480,7 +503,7 @@ public Violations detectViolations() {
480503
.map(it -> it.detectDependencies(this)) //
481504
.reduce(NONE, Violations::and);
482505

483-
return cycleViolations.and(jMoleculesViolations).and(dependencyViolations);
506+
return cycleViolations.and(additionalViolations).and(dependencyViolations);
484507
}
485508

486509
/**

spring-modulith-core/src/main/java/org/springframework/modulith/core/Types.java

Lines changed: 23 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,8 @@ static class JMoleculesTypes {
8282
static final String AT_DOMAIN_EVENT = BASE_PACKAGE + ".event.annotation.DomainEvent";
8383
static final String DOMAIN_EVENT = BASE_PACKAGE + ".event.types.DomainEvent";
8484

85+
private static Collection<ArchRule> RULES;
86+
8587
/**
8688
* Returns whether jMolecules is generally present.
8789
*
@@ -121,31 +123,34 @@ public static ApplicationModuleSourceMetadata getIdentifierSource() {
121123
*/
122124
public static Collection<ArchRule> getRules() {
123125

124-
var classLoader = JMoleculesTypes.class.getClassLoader();
125-
var rules = new ArrayList<ArchRule>();
126+
if (RULES == null) {
126127

127-
if (ClassUtils.isPresent(DDD_RULES, classLoader)) {
128-
rules.add(JMoleculesDddRules.all());
129-
}
128+
var classLoader = JMoleculesTypes.class.getClassLoader();
129+
RULES = new ArrayList<ArchRule>();
130130

131-
if (!ClassUtils.isPresent(ARCHITECTURE_RULES, classLoader)) {
132-
return rules;
133-
}
131+
if (ClassUtils.isPresent(DDD_RULES, classLoader)) {
132+
RULES.add(JMoleculesDddRules.all());
133+
}
134134

135-
if (ClassUtils.isPresent(HEXAGONAL, classLoader)) {
136-
rules.add(JMoleculesArchitectureRules.ensureHexagonal());
137-
}
135+
if (!ClassUtils.isPresent(ARCHITECTURE_RULES, classLoader)) {
136+
return RULES;
137+
}
138138

139-
if (ClassUtils.isPresent(LAYERED, classLoader)) {
140-
rules.add(JMoleculesArchitectureRules.ensureLayering());
141-
}
139+
if (ClassUtils.isPresent(HEXAGONAL, classLoader)) {
140+
RULES.add(JMoleculesArchitectureRules.ensureHexagonal());
141+
}
142+
143+
if (ClassUtils.isPresent(LAYERED, classLoader)) {
144+
RULES.add(JMoleculesArchitectureRules.ensureLayering());
145+
}
142146

143-
if (ClassUtils.isPresent(ONION, classLoader)) {
144-
rules.add(JMoleculesArchitectureRules.ensureOnionClassical());
145-
rules.add(JMoleculesArchitectureRules.ensureOnionSimple());
147+
if (ClassUtils.isPresent(ONION, classLoader)) {
148+
RULES.add(JMoleculesArchitectureRules.ensureOnionClassical());
149+
RULES.add(JMoleculesArchitectureRules.ensureOnionSimple());
150+
}
146151
}
147152

148-
return rules;
153+
return RULES;
149154
}
150155
}
151156

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
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.core;
17+
18+
import java.util.ArrayList;
19+
import java.util.Collection;
20+
import java.util.Collections;
21+
import java.util.List;
22+
23+
import org.springframework.modulith.core.Types.JMoleculesTypes;
24+
import org.springframework.util.Assert;
25+
26+
import com.tngtech.archunit.lang.ArchRule;
27+
28+
/**
29+
* Options to customize application module verifications.
30+
*
31+
* @author Oliver Drotbohm
32+
* @since 1.4
33+
* @see #defaults()
34+
*/
35+
public class VerificationOptions {
36+
37+
private final Collection<ArchRule> additionalVerifications;
38+
39+
/**
40+
* Creates a new {@link VerificationOptions}.
41+
*
42+
* @param additionalVerifications must not be {@literal null}.
43+
*/
44+
private VerificationOptions(Collection<ArchRule> additionalVerifications) {
45+
46+
Assert.notNull(additionalVerifications, "Additional verifications must not be null!");
47+
48+
this.additionalVerifications = additionalVerifications;
49+
}
50+
51+
/**
52+
* Creates a new {@link VerificationOptions} including jMolecules verifications if present on the classpath.
53+
*
54+
* @return will never be {@literal null}.
55+
*/
56+
public static VerificationOptions defaults() {
57+
return new VerificationOptions(JMoleculesTypes.getRules());
58+
}
59+
60+
/**
61+
* Define the additional verifications to be executed. Disables the ones executed by default.
62+
*
63+
* @param verifications must not be {@literal null}.
64+
* @return will never be {@literal null}.
65+
*/
66+
public VerificationOptions withAdditionalVerifications(Collection<ArchRule> verifications) {
67+
68+
Assert.notNull(verifications, "Verifications must not be null!");
69+
70+
return new VerificationOptions(verifications);
71+
}
72+
73+
/**
74+
* Define the additional verifications to be executed. Disables the ones executed by default.
75+
*
76+
* @param verifications must not be {@literal null}.
77+
* @return will never be {@literal null}.
78+
*/
79+
public VerificationOptions withAdditionalVerifications(ArchRule... verifications) {
80+
return withAdditionalVerifications(List.of(verifications));
81+
}
82+
83+
/**
84+
* Registers additional verifications on top of the default ones.
85+
*
86+
* @param verifications must not be {@literal null}.
87+
* @return will never be {@literal null}.
88+
*/
89+
public VerificationOptions andAdditionalVerifications(Collection<ArchRule> verifications) {
90+
91+
Assert.notNull(verifications, "Verifications must not be null!");
92+
93+
var newVerifications = new ArrayList<>(additionalVerifications);
94+
newVerifications.addAll(verifications);
95+
96+
return new VerificationOptions(newVerifications);
97+
}
98+
99+
/**
100+
* Registers additional verifications on top of the default ones.
101+
*
102+
* @param verifications must not be {@literal null}.
103+
* @return will never be {@literal null}.
104+
*/
105+
public VerificationOptions andAdditionalVerifications(ArchRule... verifications) {
106+
return andAdditionalVerifications(List.of(verifications));
107+
}
108+
109+
/**
110+
* Disables the additional verifications registered by default.
111+
*
112+
* @return will never be {@literal null}.
113+
*/
114+
public VerificationOptions withoutAdditionalVerifications() {
115+
return new VerificationOptions(Collections.emptyList());
116+
}
117+
118+
/**
119+
* Returns all additional verifications.
120+
*
121+
* @return will never be {@literal null}.
122+
*/
123+
Collection<ArchRule> getAdditionalVerifications() {
124+
return additionalVerifications;
125+
}
126+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
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.core;
17+
18+
import static org.assertj.core.api.Assertions.*;
19+
import static org.mockito.Mockito.*;
20+
21+
import org.junit.jupiter.api.Test;
22+
import org.springframework.modulith.core.Types.JMoleculesTypes;
23+
24+
import com.tngtech.archunit.lang.ArchRule;
25+
26+
/**
27+
* Unit tests for {@link VerificationOptions}.
28+
*
29+
* @author Oliver Drotbohm
30+
*/
31+
class VerificationOptionsUnitTests {
32+
33+
@Test // GH-1185
34+
void usesJMoleculesVerificationsByDefault() {
35+
36+
var options = VerificationOptions.defaults();
37+
38+
assertThat(options.getAdditionalVerifications()).isEqualTo(JMoleculesTypes.getRules());
39+
}
40+
41+
@Test // GH-1185
42+
void addsVerification() {
43+
44+
var archRule = mock(ArchRule.class);
45+
var options = VerificationOptions.defaults();
46+
47+
assertThat(options.andAdditionalVerifications(archRule).getAdditionalVerifications())
48+
.hasSize(options.getAdditionalVerifications().size() + 1)
49+
.containsAll(JMoleculesTypes.getRules())
50+
.contains(archRule);
51+
}
52+
53+
@Test // GH-1185
54+
void replacesVerification() {
55+
56+
var archRule = mock(ArchRule.class);
57+
var options = VerificationOptions.defaults().withAdditionalVerifications(archRule);
58+
59+
assertThat(options.getAdditionalVerifications())
60+
.hasSize(1)
61+
.containsExactly(archRule);
62+
}
63+
}

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

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,33 @@ If those are configured, dependencies to other application modules are rejected.
3030
See xref:fundamentals.adoc#modules.explicit-dependencies[Explicit Application Module Dependencies] and xref:fundamentals.adoc#modules.named-interfaces[Named Interfaces] for details.
3131

3232
Spring Modulith optionally integrates with the jMolecules ArchUnit library and, if present, automatically triggers its Domain-Driven Design and architectural verification rules described https://github.com/xmolecules/jmolecules-integrations/tree/main/jmolecules-archunit[here].
33+
34+
== Handling Detected Violations
35+
36+
`ApplicationModules.verify()` throws an exception in case of any architectural violation being detected.
37+
You can access the violations for further processing, such as ignoring certain violations, by instead calling `ApplicationModules.detectViolations()`.
38+
39+
[source, java]
40+
----
41+
ApplicationModules.of(…)
42+
.detectViolations()
43+
.filter(violation -> …)
44+
.throwIfPresent();
45+
----
46+
47+
== Customizing the Verifcation
48+
49+
As described xref:verification.adoc#verification[above], by default, both the `ApplicationModules.verify(…)` and `….detectViolations(…)` automatically perform additional verifications depending on the classpath configuration.
50+
51+
To customize these, disable them or register additional verifications, both `verify(…)` and `detectVolations(…)` take a `VerificationOptions` instance.
52+
53+
[source, java]
54+
----
55+
var hexagonal = JMoleculesArchitectureRules.ensureHexagonal(VerificationDepth.LENIENT); <1>
56+
var options = VerificationOptions.defaults().withAdditionalVerifications(hexagonal); <2>
57+
58+
ApplicationModules.of(…).verify(options); <3>
59+
----
60+
<1> Set up the jMolecules Architecture verification for Hexagonal Architecture in lenient mode.
61+
<2> Create a `VerificationOptions` instance replacing the default verification with the one just set up.
62+
<3> Execute the verification using the just configured options.

0 commit comments

Comments
 (0)