From ed1cb3fc9131ee8177fa7714302d8d6eaccaa8ef Mon Sep 17 00:00:00 2001 From: Oliver Drotbohm Date: Wed, 5 Jun 2024 17:35:45 +0200 Subject: [PATCH] GH-652 - Allow to configure the ApplicationModuleDetectionStrategy via a configuration property. We now expose a configuration property spring.modulith.detection-strategy that can take either of the two prepared values "direct-sub-packages" (default) or "explicitly-annotated", or a fully qualified class name of the strategy to use. Removed ApplicationModuleStrategies enum to avoid exposing the enum values as additional implementations. Those are now held as inline lambda expression in the factory methods on ApplicationModuleStrategy. Extracted the lookup of the strategy to use into ApplicationModuleDetectionStrategyLookup for easier testability. --- .../ApplicationModuleDetectionStrategies.java | 60 ---------- .../ApplicationModuleDetectionStrategy.java | 18 ++- ...licationModuleDetectionStrategyLookup.java | 107 ++++++++++++++++++ .../modulith/core/ApplicationModules.java | 21 +--- .../spring-configuration-metadata.json | 32 ++++++ ...ionModuleDetectionStrategyLookupTests.java | 65 +++++++++++ .../core/ModuleDetectionStrategyUnitTest.java | 14 --- .../detection/custom-type.properties | 1 + .../detection/direct-sub-packages.properties | 1 + .../detection/explicitly-annotated.properties | 1 + .../modules/ROOT/pages/fundamentals.adoc | 18 ++- 11 files changed, 236 insertions(+), 102 deletions(-) delete mode 100644 spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModuleDetectionStrategies.java create mode 100644 spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModuleDetectionStrategyLookup.java create mode 100644 spring-modulith-core/src/main/resources/META-INF/spring-configuration-metadata.json create mode 100644 spring-modulith-core/src/test/java/org/springframework/modulith/core/ApplicationModuleDetectionStrategyLookupTests.java create mode 100644 spring-modulith-core/src/test/resources/detection/custom-type.properties create mode 100644 spring-modulith-core/src/test/resources/detection/direct-sub-packages.properties create mode 100644 spring-modulith-core/src/test/resources/detection/explicitly-annotated.properties diff --git a/spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModuleDetectionStrategies.java b/spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModuleDetectionStrategies.java deleted file mode 100644 index 2c530061..00000000 --- a/spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModuleDetectionStrategies.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright 2020-2024 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.Objects; -import java.util.stream.Stream; - -import org.springframework.modulith.ApplicationModule; -import org.springframework.modulith.core.Types.JMoleculesTypes; - -/** - * Default implementations of {@link ApplicationModuleDetectionStrategy}. - * - * @author Oliver Drotbohm - * @see ApplicationModuleDetectionStrategy#directSubPackage() - * @see ApplicationModuleDetectionStrategy#explictlyAnnotated() - */ -enum ApplicationModuleDetectionStrategies implements ApplicationModuleDetectionStrategy { - - DIRECT_SUB_PACKAGES { - - /* - * (non-Javadoc) - * @see org.springframework.modulith.model.ModuleDetection#getModuleBasePackages(org.springframework.modulith.model.JavaPackage) - */ - @Override - public Stream getModuleBasePackages( - JavaPackage basePackage) { - return basePackage.getDirectSubPackages().stream(); - } - }, - - EXPLICITLY_ANNOTATED { - - /* - * (non-Javadoc) - * @see org.springframework.modulith.model.ModuleDetection#getModuleBasePackages(org.springframework.modulith.model.JavaPackage) - */ - @Override - public Stream getModuleBasePackages(JavaPackage basePackage) { - - return Stream.of(ApplicationModule.class, JMoleculesTypes.getModuleAnnotationTypeIfPresent()) - .filter(Objects::nonNull) - .flatMap(basePackage::getSubPackagesAnnotatedWith); - } - } -} diff --git a/spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModuleDetectionStrategy.java b/spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModuleDetectionStrategy.java index f3d9df68..e3f7ed28 100644 --- a/spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModuleDetectionStrategy.java +++ b/spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModuleDetectionStrategy.java @@ -15,8 +15,12 @@ */ package org.springframework.modulith.core; +import java.util.Objects; import java.util.stream.Stream; +import org.springframework.modulith.ApplicationModule; +import org.springframework.modulith.core.Types.JMoleculesTypes; + /** * Strategy interface to customize which packages are considered module base packages. * @@ -34,22 +38,24 @@ public interface ApplicationModuleDetectionStrategy { Stream getModuleBasePackages(JavaPackage basePackage); /** - * A {@link ApplicationModuleDetectionStrategy} that considers all direct sub-packages of the Moduliths base package to be module - * base packages. + * A {@link ApplicationModuleDetectionStrategy} that considers all direct sub-packages of the Moduliths base package + * to be module base packages. * * @return will never be {@literal null}. */ static ApplicationModuleDetectionStrategy directSubPackage() { - return ApplicationModuleDetectionStrategies.DIRECT_SUB_PACKAGES; + return pkg -> pkg.getDirectSubPackages().stream(); } /** - * A {@link ApplicationModuleDetectionStrategy} that considers packages explicitly annotated with {@link ApplicationModule} module base - * packages. + * A {@link ApplicationModuleDetectionStrategy} that considers packages explicitly annotated with + * {@link ApplicationModule} module base packages. * * @return will never be {@literal null}. */ static ApplicationModuleDetectionStrategy explictlyAnnotated() { - return ApplicationModuleDetectionStrategies.EXPLICITLY_ANNOTATED; + return pkg -> Stream.of(ApplicationModule.class, JMoleculesTypes.getModuleAnnotationTypeIfPresent()) + .filter(Objects::nonNull) + .flatMap(pkg::getSubPackagesAnnotatedWith); } } diff --git a/spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModuleDetectionStrategyLookup.java b/spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModuleDetectionStrategyLookup.java new file mode 100644 index 00000000..5c231ab5 --- /dev/null +++ b/spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModuleDetectionStrategyLookup.java @@ -0,0 +1,107 @@ +/* + * Copyright 2024 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.List; +import java.util.function.Supplier; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.BeanUtils; +import org.springframework.boot.context.config.ConfigDataEnvironmentPostProcessor; +import org.springframework.core.env.StandardEnvironment; +import org.springframework.core.io.support.SpringFactoriesLoader; +import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; + +/** + * A factory for the {@link ApplicationModuleDetectionStrategy} to be used when scanning code for + * {@link ApplicationModule}s. + * + * @author Oliver Drotbohm + */ +class ApplicationModuleDetectionStrategyLookup { + + private static final String DETECTION_STRATEGY_PROPERTY = "spring.modulith.detection-strategy"; + private static final Logger LOG = LoggerFactory.getLogger(ApplicationModuleDetectionStrategyLookup.class); + private static final Supplier FALLBACK_DETECTION_STRATEGY; + + static { + + FALLBACK_DETECTION_STRATEGY = () -> { + + List loadFactories = SpringFactoriesLoader.loadFactories( + ApplicationModuleDetectionStrategy.class, ApplicationModules.class.getClassLoader()); + + var size = loadFactories.size(); + + if (size == 0) { + return ApplicationModuleDetectionStrategy.directSubPackage(); + } + + if (size > 1) { + + throw new IllegalStateException( + "Multiple module detection strategies configured. Only one supported! %s".formatted(loadFactories)); + } + + LOG.warn( + "Configuring the application module detection strategy via spring.factories is deprecated! Please configure {} instead.", + DETECTION_STRATEGY_PROPERTY); + + return loadFactories.get(0); + }; + } + + /** + * Returns the {@link ApplicationModuleDetectionStrategy} to be used to detect {@link ApplicationModule}s. Will use + * the following algorithm: + *
    + *
  1. Use the prepared strategies if + * + * @return + */ + static ApplicationModuleDetectionStrategy getStrategy() { + + var environment = new StandardEnvironment(); + ConfigDataEnvironmentPostProcessor.applyTo(environment); + + var configuredStrategy = environment.getProperty(DETECTION_STRATEGY_PROPERTY, String.class); + + // Nothing configured? Use fallback. + if (!StringUtils.hasText(configuredStrategy)) { + return FALLBACK_DETECTION_STRATEGY.get(); + } + + // Any of the prepared ones? + switch (configuredStrategy) { + case "direct-sub-packages": + return ApplicationModuleDetectionStrategy.directSubPackage(); + case "explicitly-annotated": + return ApplicationModuleDetectionStrategy.explictlyAnnotated(); + } + + try { + + // Lookup configured value as class + var strategyType = ClassUtils.forName(configuredStrategy, ApplicationModules.class.getClassLoader()); + return BeanUtils.instantiateClass(strategyType, ApplicationModuleDetectionStrategy.class); + + } catch (ClassNotFoundException | LinkageError o_O) { + throw new IllegalStateException(o_O); + } + } +} 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 ca7b1635..49d6a93d 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 @@ -37,7 +37,6 @@ import org.jgrapht.traverse.TopologicalOrderIterator; import org.jmolecules.archunit.JMoleculesDddRules; import org.springframework.aot.generate.Generated; import org.springframework.core.annotation.AnnotationAwareOrderComparator; -import org.springframework.core.io.support.SpringFactoriesLoader; import org.springframework.lang.Nullable; import org.springframework.modulith.core.Types.JMoleculesTypes; import org.springframework.modulith.core.Violations.Violation; @@ -65,7 +64,7 @@ import com.tngtech.archunit.library.dependencies.SlicesRuleDefinition; public class ApplicationModules implements Iterable { private static final Map CACHE = new ConcurrentHashMap<>(); - private static final ApplicationModuleDetectionStrategy DETECTION_STRATEGY; + private static final ImportOption IMPORT_OPTION = new ImportOption.DoNotIncludeTests(); private static final boolean JGRAPHT_PRESENT = ClassUtils.isPresent("org.jgrapht.Graph", ApplicationModules.class.getClassLoader()); @@ -73,21 +72,6 @@ public class ApplicationModules implements Iterable { private static final DescribedPredicate IS_SPRING_CGLIB_PROXY = nameContaining("$$SpringCGLIB$$"); static { - - List loadFactories = SpringFactoriesLoader.loadFactories( - ApplicationModuleDetectionStrategy.class, - ApplicationModules.class.getClassLoader()); - - if (loadFactories.size() > 1) { - - throw new IllegalStateException( - String.format("Multiple module detection strategies configured. Only one supported! %s", - loadFactories)); - } - - DETECTION_STRATEGY = loadFactories.isEmpty() ? ApplicationModuleDetectionStrategies.DIRECT_SUB_PACKAGES - : loadFactories.get(0); - IS_AOT_TYPE = ClassUtils.isPresent("org.springframework.aot.generate.Generated", ApplicationModules.class.getClassLoader()) ? getAtGenerated() : DescribedPredicate.alwaysFalse(); } @@ -150,10 +134,11 @@ public class ApplicationModules implements Iterable { Assert.notEmpty(allClasses, () -> "No classes found in packages %s!".formatted(packages)); Classes classes = Classes.of(allClasses); + var strategy = ApplicationModuleDetectionStrategyLookup.getStrategy(); this.modules = packages.stream() // .map(it -> JavaPackage.of(classes, it)) - .flatMap(DETECTION_STRATEGY::getModuleBasePackages) // + .flatMap(strategy::getModuleBasePackages) // .map(it -> new ApplicationModule(it, useFullyQualifiedModuleNames)) // .collect(toMap(ApplicationModule::getName, Function.identity())); diff --git a/spring-modulith-core/src/main/resources/META-INF/spring-configuration-metadata.json b/spring-modulith-core/src/main/resources/META-INF/spring-configuration-metadata.json new file mode 100644 index 00000000..72884367 --- /dev/null +++ b/spring-modulith-core/src/main/resources/META-INF/spring-configuration-metadata.json @@ -0,0 +1,32 @@ +{ + "properties": [ + { + "name": "spring.modulith.detection-strategy", + "type": "java.lang.String", + "description": "The strategy how to detect application modules." + } + ], + "hints": [ + { + "name": "spring.modulith.detection-strategy", + "values": [ + { + "value": "direct-sub-packages", + "description" : "Selects the direct sub-packages underneath the main application class as application module base interfaces." + }, + { + "value": "explicitly-annotated", + "description" : "Only selects explicitly annotated packages as application module base packages (via @ApplicationModules or jMolecules' DDD @Module)." + } + ], + "providers": [ + { + "name": "class-reference", + "parameters": { + "target": "org.springframework.modulith.core.ApplicationModuleDetectionStrategy" + } + } + ] + } + ] +} diff --git a/spring-modulith-core/src/test/java/org/springframework/modulith/core/ApplicationModuleDetectionStrategyLookupTests.java b/spring-modulith-core/src/test/java/org/springframework/modulith/core/ApplicationModuleDetectionStrategyLookupTests.java new file mode 100644 index 00000000..d7c91c94 --- /dev/null +++ b/spring-modulith-core/src/test/java/org/springframework/modulith/core/ApplicationModuleDetectionStrategyLookupTests.java @@ -0,0 +1,65 @@ +/* + * Copyright 2024 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 java.util.stream.Stream; + +import org.junit.jupiter.api.Test; + +/** + * Unit tests for {@link ApplicationModuleDetectionStrategy}. + * + * @author Oliver Drotbohm + */ +class ApplicationModuleDetectionStrategyLookupTests { + + @Test // GH-652 + void usesExplicitlyAnnotatedStrategyIfConfigured() { + + System.setProperty("spring.config.additional-location", "classpath:detection/explicitly-annotated.properties"); + + assertThat(ApplicationModuleDetectionStrategyLookup.getStrategy()) + .isEqualTo(ApplicationModuleDetectionStrategy.explictlyAnnotated()); + } + + @Test // GH-652 + void usesDirectSubPackagesStrategyIfConfigured() { + + System.setProperty("spring.config.additional-location", "classpath:detection/direct-sub-packages.properties"); + + assertThat(ApplicationModuleDetectionStrategyLookup.getStrategy()) + .isEqualTo(ApplicationModuleDetectionStrategy.directSubPackage()); + } + + @Test // GH-652 + void usesCustomStrategyIfConfigured() { + + System.setProperty("spring.config.additional-location", "classpath:detection/custom-type.properties"); + + assertThat(ApplicationModuleDetectionStrategyLookup.getStrategy()) + .isInstanceOf(TestStrategy.class); + } + + static class TestStrategy implements ApplicationModuleDetectionStrategy { + + @Override + public Stream getModuleBasePackages(JavaPackage basePackage) { + return null; + } + } +} diff --git a/spring-modulith-core/src/test/java/org/springframework/modulith/core/ModuleDetectionStrategyUnitTest.java b/spring-modulith-core/src/test/java/org/springframework/modulith/core/ModuleDetectionStrategyUnitTest.java index 93712300..5380b82f 100644 --- a/spring-modulith-core/src/test/java/org/springframework/modulith/core/ModuleDetectionStrategyUnitTest.java +++ b/spring-modulith-core/src/test/java/org/springframework/modulith/core/ModuleDetectionStrategyUnitTest.java @@ -29,20 +29,6 @@ import com.tngtech.archunit.core.importer.ImportOption; */ class ModuleDetectionStrategyUnitTest { - @Test - void usesExplicitlyAnnotatedConstant() { - - assertThat(ApplicationModuleDetectionStrategy.explictlyAnnotated()) - .isEqualTo(ApplicationModuleDetectionStrategies.EXPLICITLY_ANNOTATED); - } - - @Test - void usesDirectSubPackages() { - - assertThat(ApplicationModuleDetectionStrategy.directSubPackage()) - .isEqualTo(ApplicationModuleDetectionStrategies.DIRECT_SUB_PACKAGES); - } - @Test void detectsJMoleculesAnnotatedModule() { diff --git a/spring-modulith-core/src/test/resources/detection/custom-type.properties b/spring-modulith-core/src/test/resources/detection/custom-type.properties new file mode 100644 index 00000000..02034852 --- /dev/null +++ b/spring-modulith-core/src/test/resources/detection/custom-type.properties @@ -0,0 +1 @@ +spring.modulith.detection-strategy=org.springframework.modulith.core.ApplicationModuleDetectionStrategyLookupTests.TestStrategy diff --git a/spring-modulith-core/src/test/resources/detection/direct-sub-packages.properties b/spring-modulith-core/src/test/resources/detection/direct-sub-packages.properties new file mode 100644 index 00000000..c2078b9d --- /dev/null +++ b/spring-modulith-core/src/test/resources/detection/direct-sub-packages.properties @@ -0,0 +1 @@ +spring.modulith.detection-strategy=direct-sub-packages diff --git a/spring-modulith-core/src/test/resources/detection/explicitly-annotated.properties b/spring-modulith-core/src/test/resources/detection/explicitly-annotated.properties new file mode 100644 index 00000000..68be5031 --- /dev/null +++ b/spring-modulith-core/src/test/resources/detection/explicitly-annotated.properties @@ -0,0 +1 @@ +spring.modulith.detection-strategy=explicitly-annotated diff --git a/src/docs/antora/modules/ROOT/pages/fundamentals.adoc b/src/docs/antora/modules/ROOT/pages/fundamentals.adoc index 55aa6c21..18c312b3 100644 --- a/src/docs/antora/modules/ROOT/pages/fundamentals.adoc +++ b/src/docs/antora/modules/ROOT/pages/fundamentals.adoc @@ -348,12 +348,23 @@ package example.inventory [[customizing-modules]] === Customizing Module Detection -If the default application module model does not work for your application, the detection of the modules can be customized by providing an implementation of `ApplicationModuleDetectionStrategy`. +By default, application modules will be expected to be located in direct sub-packages of the package the Spring Boot application class resides in. +An alternative detection strategy can be activated to only consider package explicitly annotated, either via Spring Modulith's `@ApplicationModule` or jMolecules `@Module` annotation. +That strategy can be activated by configuring the `spring.modulith.detection-strategy` to `explicitly-annotated`. + +.Switching the application module detection strategy to only consider annotated packages +[source, text] +---- +spring.modulith.detection-strategy=explicitly-annotated +---- + +If the neither default application module detection strategy nor the manually annotated one does not work for your application, the detection of the modules can be customized by providing an implementation of `ApplicationModuleDetectionStrategy`. That interface exposes a single method `Stream getModuleBasePackages(JavaPackage)` and will be called with the package the Spring Boot application class resides in. You can then inspect the packages residing within that and select the ones to be considered application module base packages based on a naming convention or the like. Assume you declare a custom `ApplicationModuleDetectionStrategy` implementation like this: +.Implementing a custom `ApplicationModuleDetectionStrategy` [tabs] ====== Java:: @@ -385,12 +396,11 @@ class CustomApplicationModuleDetectionStrategy : ApplicationModuleDetectionStrat ---- ====== -This class needs to be registered in `META-INF/spring.factories` as follows: +This class can now be registered as `spring.modulith.detection-strategy` as follows: [source, text] ---- -org.springframework.modulith.core.ApplicationModuleDetectionStrategy=\ - example.CustomApplicationModuleDetectionStrategy +spring.modulith.detection-strategy=example.CustomApplicationModuleDetectionStrategy ---- [[customizing-modules-arrangement]]