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.
This commit is contained in:
@@ -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<JavaPackage> 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<JavaPackage> getModuleBasePackages(JavaPackage basePackage) {
|
||||
|
||||
return Stream.of(ApplicationModule.class, JMoleculesTypes.getModuleAnnotationTypeIfPresent())
|
||||
.filter(Objects::nonNull)
|
||||
.flatMap(basePackage::getSubPackagesAnnotatedWith);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<JavaPackage> 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<ApplicationModuleDetectionStrategy> FALLBACK_DETECTION_STRATEGY;
|
||||
|
||||
static {
|
||||
|
||||
FALLBACK_DETECTION_STRATEGY = () -> {
|
||||
|
||||
List<ApplicationModuleDetectionStrategy> 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:
|
||||
* <ol>
|
||||
* <li>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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<ApplicationModule> {
|
||||
|
||||
private static final Map<CacheKey, ApplicationModules> 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<ApplicationModule> {
|
||||
private static final DescribedPredicate<HasName> IS_SPRING_CGLIB_PROXY = nameContaining("$$SpringCGLIB$$");
|
||||
|
||||
static {
|
||||
|
||||
List<ApplicationModuleDetectionStrategy> 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<ApplicationModule> {
|
||||
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()));
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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<JavaPackage> getModuleBasePackages(JavaPackage basePackage) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
spring.modulith.detection-strategy=org.springframework.modulith.core.ApplicationModuleDetectionStrategyLookupTests.TestStrategy
|
||||
@@ -0,0 +1 @@
|
||||
spring.modulith.detection-strategy=direct-sub-packages
|
||||
@@ -0,0 +1 @@
|
||||
spring.modulith.detection-strategy=explicitly-annotated
|
||||
@@ -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<JavaPackage> 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]]
|
||||
|
||||
Reference in New Issue
Block a user