From 8326923c24011b455c4ad4cbf91e1589728cb4ab Mon Sep 17 00:00:00 2001 From: Oliver Drotbohm Date: Fri, 2 May 2025 15:12:19 +0200 Subject: [PATCH] GH-1185 - Support for customizing additional verifications. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../modulith/core/ApplicationModules.java | 31 ++++- .../springframework/modulith/core/Types.java | 49 ++++--- .../modulith/core/VerificationOptions.java | 126 ++++++++++++++++++ .../core/VerificationOptionsUnitTests.java | 63 +++++++++ .../modules/ROOT/pages/verification.adoc | 30 +++++ 5 files changed, 273 insertions(+), 26 deletions(-) create mode 100644 spring-modulith-core/src/main/java/org/springframework/modulith/core/VerificationOptions.java create mode 100644 spring-modulith-core/src/test/java/org/springframework/modulith/core/VerificationOptionsUnitTests.java diff --git a/spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModules.java b/spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModules.java index d9e55a77..390b0ccc 100644 --- a/spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModules.java +++ b/spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModules.java @@ -35,7 +35,6 @@ import java.util.stream.StreamSupport; import org.springframework.aot.generate.Generated; import org.springframework.core.annotation.AnnotationAwareOrderComparator; import org.springframework.lang.Nullable; -import org.springframework.modulith.core.Types.JMoleculesTypes; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.function.SingletonSupplier; @@ -442,12 +441,23 @@ public class ApplicationModules implements Iterable { * @return will never be {@literal null}. */ public ApplicationModules verify() { + return verify(VerificationOptions.defaults()); + } + + /** + * Execute all verifications to be applied considering the given {@link VerificationOptions}, unless the verification + * has been executed before. + * + * @return will never be {@literal null}. + * @since 1.4 + */ + public ApplicationModules verify(VerificationOptions options) { if (verified) { return this; } - Violations violations = detectViolations(); + Violations violations = detectViolations(options); this.verified = true; @@ -464,13 +474,26 @@ public class ApplicationModules implements Iterable { * @see Violations#throwIfPresent() */ public Violations detectViolations() { + return detectViolations(VerificationOptions.defaults()); + } + + /** + * Executes all verifications to be applied considering the given {@link VerificationOptions} and returns + * {@link Violations} if any occured. Will always execute the verifications in contrast to {@link #verify()} which + * just runs once. + * + * @return will never be {@literal null}. + * @see Violations#throwIfPresent() + * @since 1.4 + */ + public Violations detectViolations(VerificationOptions options) { var cycleViolations = rootPackages.stream() // .map(this::assertNoCyclesFor) // .flatMap(it -> it.getDetails().stream()) // .collect(toViolations()); - var jMoleculesViolations = JMoleculesTypes.getRules().stream() + var additionalViolations = options.getAdditionalVerifications().stream() .map(it -> it.evaluate(allClasses)) .map(EvaluationResult::getFailureReport) .flatMap(it -> it.getDetails().stream()) @@ -480,7 +503,7 @@ public class ApplicationModules implements Iterable { .map(it -> it.detectDependencies(this)) // .reduce(NONE, Violations::and); - return cycleViolations.and(jMoleculesViolations).and(dependencyViolations); + return cycleViolations.and(additionalViolations).and(dependencyViolations); } /** diff --git a/spring-modulith-core/src/main/java/org/springframework/modulith/core/Types.java b/spring-modulith-core/src/main/java/org/springframework/modulith/core/Types.java index 87bb948e..70b4c372 100644 --- a/spring-modulith-core/src/main/java/org/springframework/modulith/core/Types.java +++ b/spring-modulith-core/src/main/java/org/springframework/modulith/core/Types.java @@ -82,6 +82,8 @@ public class Types { static final String AT_DOMAIN_EVENT = BASE_PACKAGE + ".event.annotation.DomainEvent"; static final String DOMAIN_EVENT = BASE_PACKAGE + ".event.types.DomainEvent"; + private static Collection RULES; + /** * Returns whether jMolecules is generally present. * @@ -121,31 +123,34 @@ public class Types { */ public static Collection getRules() { - var classLoader = JMoleculesTypes.class.getClassLoader(); - var rules = new ArrayList(); + if (RULES == null) { - if (ClassUtils.isPresent(DDD_RULES, classLoader)) { - rules.add(JMoleculesDddRules.all()); + var classLoader = JMoleculesTypes.class.getClassLoader(); + RULES = new ArrayList(); + + if (ClassUtils.isPresent(DDD_RULES, classLoader)) { + RULES.add(JMoleculesDddRules.all()); + } + + if (!ClassUtils.isPresent(ARCHITECTURE_RULES, classLoader)) { + return RULES; + } + + if (ClassUtils.isPresent(HEXAGONAL, classLoader)) { + RULES.add(JMoleculesArchitectureRules.ensureHexagonal()); + } + + if (ClassUtils.isPresent(LAYERED, classLoader)) { + RULES.add(JMoleculesArchitectureRules.ensureLayering()); + } + + if (ClassUtils.isPresent(ONION, classLoader)) { + RULES.add(JMoleculesArchitectureRules.ensureOnionClassical()); + RULES.add(JMoleculesArchitectureRules.ensureOnionSimple()); + } } - if (!ClassUtils.isPresent(ARCHITECTURE_RULES, classLoader)) { - return rules; - } - - if (ClassUtils.isPresent(HEXAGONAL, classLoader)) { - rules.add(JMoleculesArchitectureRules.ensureHexagonal()); - } - - if (ClassUtils.isPresent(LAYERED, classLoader)) { - rules.add(JMoleculesArchitectureRules.ensureLayering()); - } - - if (ClassUtils.isPresent(ONION, classLoader)) { - rules.add(JMoleculesArchitectureRules.ensureOnionClassical()); - rules.add(JMoleculesArchitectureRules.ensureOnionSimple()); - } - - return rules; + return RULES; } } diff --git a/spring-modulith-core/src/main/java/org/springframework/modulith/core/VerificationOptions.java b/spring-modulith-core/src/main/java/org/springframework/modulith/core/VerificationOptions.java new file mode 100644 index 00000000..3f3520fd --- /dev/null +++ b/spring-modulith-core/src/main/java/org/springframework/modulith/core/VerificationOptions.java @@ -0,0 +1,126 @@ +/* + * 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.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import org.springframework.modulith.core.Types.JMoleculesTypes; +import org.springframework.util.Assert; + +import com.tngtech.archunit.lang.ArchRule; + +/** + * Options to customize application module verifications. + * + * @author Oliver Drotbohm + * @since 1.4 + * @see #defaults() + */ +public class VerificationOptions { + + private final Collection additionalVerifications; + + /** + * Creates a new {@link VerificationOptions}. + * + * @param additionalVerifications must not be {@literal null}. + */ + private VerificationOptions(Collection additionalVerifications) { + + Assert.notNull(additionalVerifications, "Additional verifications must not be null!"); + + this.additionalVerifications = additionalVerifications; + } + + /** + * Creates a new {@link VerificationOptions} including jMolecules verifications if present on the classpath. + * + * @return will never be {@literal null}. + */ + public static VerificationOptions defaults() { + return new VerificationOptions(JMoleculesTypes.getRules()); + } + + /** + * Define the additional verifications to be executed. Disables the ones executed by default. + * + * @param verifications must not be {@literal null}. + * @return will never be {@literal null}. + */ + public VerificationOptions withAdditionalVerifications(Collection verifications) { + + Assert.notNull(verifications, "Verifications must not be null!"); + + return new VerificationOptions(verifications); + } + + /** + * Define the additional verifications to be executed. Disables the ones executed by default. + * + * @param verifications must not be {@literal null}. + * @return will never be {@literal null}. + */ + public VerificationOptions withAdditionalVerifications(ArchRule... verifications) { + return withAdditionalVerifications(List.of(verifications)); + } + + /** + * Registers additional verifications on top of the default ones. + * + * @param verifications must not be {@literal null}. + * @return will never be {@literal null}. + */ + public VerificationOptions andAdditionalVerifications(Collection verifications) { + + Assert.notNull(verifications, "Verifications must not be null!"); + + var newVerifications = new ArrayList<>(additionalVerifications); + newVerifications.addAll(verifications); + + return new VerificationOptions(newVerifications); + } + + /** + * Registers additional verifications on top of the default ones. + * + * @param verifications must not be {@literal null}. + * @return will never be {@literal null}. + */ + public VerificationOptions andAdditionalVerifications(ArchRule... verifications) { + return andAdditionalVerifications(List.of(verifications)); + } + + /** + * Disables the additional verifications registered by default. + * + * @return will never be {@literal null}. + */ + public VerificationOptions withoutAdditionalVerifications() { + return new VerificationOptions(Collections.emptyList()); + } + + /** + * Returns all additional verifications. + * + * @return will never be {@literal null}. + */ + Collection getAdditionalVerifications() { + return additionalVerifications; + } +} diff --git a/spring-modulith-core/src/test/java/org/springframework/modulith/core/VerificationOptionsUnitTests.java b/spring-modulith-core/src/test/java/org/springframework/modulith/core/VerificationOptionsUnitTests.java new file mode 100644 index 00000000..9c0dbec9 --- /dev/null +++ b/spring-modulith-core/src/test/java/org/springframework/modulith/core/VerificationOptionsUnitTests.java @@ -0,0 +1,63 @@ +/* + * 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 static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import org.junit.jupiter.api.Test; +import org.springframework.modulith.core.Types.JMoleculesTypes; + +import com.tngtech.archunit.lang.ArchRule; + +/** + * Unit tests for {@link VerificationOptions}. + * + * @author Oliver Drotbohm + */ +class VerificationOptionsUnitTests { + + @Test // GH-1185 + void usesJMoleculesVerificationsByDefault() { + + var options = VerificationOptions.defaults(); + + assertThat(options.getAdditionalVerifications()).isEqualTo(JMoleculesTypes.getRules()); + } + + @Test // GH-1185 + void addsVerification() { + + var archRule = mock(ArchRule.class); + var options = VerificationOptions.defaults(); + + assertThat(options.andAdditionalVerifications(archRule).getAdditionalVerifications()) + .hasSize(options.getAdditionalVerifications().size() + 1) + .containsAll(JMoleculesTypes.getRules()) + .contains(archRule); + } + + @Test // GH-1185 + void replacesVerification() { + + var archRule = mock(ArchRule.class); + var options = VerificationOptions.defaults().withAdditionalVerifications(archRule); + + assertThat(options.getAdditionalVerifications()) + .hasSize(1) + .containsExactly(archRule); + } +} diff --git a/src/docs/antora/modules/ROOT/pages/verification.adoc b/src/docs/antora/modules/ROOT/pages/verification.adoc index 52e396ca..a258f3cc 100644 --- a/src/docs/antora/modules/ROOT/pages/verification.adoc +++ b/src/docs/antora/modules/ROOT/pages/verification.adoc @@ -30,3 +30,33 @@ If those are configured, dependencies to other application modules are rejected. See xref:fundamentals.adoc#modules.explicit-dependencies[Explicit Application Module Dependencies] and xref:fundamentals.adoc#modules.named-interfaces[Named Interfaces] for details. 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]. + +== Handling Detected Violations + +`ApplicationModules.verify()` throws an exception in case of any architectural violation being detected. +You can access the violations for further processing, such as ignoring certain violations, by instead calling `ApplicationModules.detectViolations()`. + +[source, java] +---- +ApplicationModules.of(…) + .detectViolations() + .filter(violation -> …) + .throwIfPresent(); +---- + +== Customizing the Verifcation + +As described xref:verification.adoc#verification[above], by default, both the `ApplicationModules.verify(…)` and `….detectViolations(…)` automatically perform additional verifications depending on the classpath configuration. + +To customize these, disable them or register additional verifications, both `verify(…)` and `detectVolations(…)` take a `VerificationOptions` instance. + +[source, java] +---- +var hexagonal = JMoleculesArchitectureRules.ensureHexagonal(VerificationDepth.LENIENT); <1> +var options = VerificationOptions.defaults().withAdditionalVerifications(hexagonal); <2> + +ApplicationModules.of(…).verify(options); <3> +---- +<1> Set up the jMolecules Architecture verification for Hexagonal Architecture in lenient mode. +<2> Create a `VerificationOptions` instance replacing the default verification with the one just set up. +<3> Execute the verification using the just configured options.