Skip to content

Commit ed1cb3f

Browse files
committed
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.
1 parent 1aef1b6 commit ed1cb3f

File tree

11 files changed

+236
-102
lines changed

11 files changed

+236
-102
lines changed

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

Lines changed: 0 additions & 60 deletions
This file was deleted.

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

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,12 @@
1515
*/
1616
package org.springframework.modulith.core;
1717

18+
import java.util.Objects;
1819
import java.util.stream.Stream;
1920

21+
import org.springframework.modulith.ApplicationModule;
22+
import org.springframework.modulith.core.Types.JMoleculesTypes;
23+
2024
/**
2125
* Strategy interface to customize which packages are considered module base packages.
2226
*
@@ -34,22 +38,24 @@ public interface ApplicationModuleDetectionStrategy {
3438
Stream<JavaPackage> getModuleBasePackages(JavaPackage basePackage);
3539

3640
/**
37-
* A {@link ApplicationModuleDetectionStrategy} that considers all direct sub-packages of the Moduliths base package to be module
38-
* base packages.
41+
* A {@link ApplicationModuleDetectionStrategy} that considers all direct sub-packages of the Moduliths base package
42+
* to be module base packages.
3943
*
4044
* @return will never be {@literal null}.
4145
*/
4246
static ApplicationModuleDetectionStrategy directSubPackage() {
43-
return ApplicationModuleDetectionStrategies.DIRECT_SUB_PACKAGES;
47+
return pkg -> pkg.getDirectSubPackages().stream();
4448
}
4549

4650
/**
47-
* A {@link ApplicationModuleDetectionStrategy} that considers packages explicitly annotated with {@link ApplicationModule} module base
48-
* packages.
51+
* A {@link ApplicationModuleDetectionStrategy} that considers packages explicitly annotated with
52+
* {@link ApplicationModule} module base packages.
4953
*
5054
* @return will never be {@literal null}.
5155
*/
5256
static ApplicationModuleDetectionStrategy explictlyAnnotated() {
53-
return ApplicationModuleDetectionStrategies.EXPLICITLY_ANNOTATED;
57+
return pkg -> Stream.of(ApplicationModule.class, JMoleculesTypes.getModuleAnnotationTypeIfPresent())
58+
.filter(Objects::nonNull)
59+
.flatMap(pkg::getSubPackagesAnnotatedWith);
5460
}
5561
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
/*
2+
* Copyright 2024 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.List;
19+
import java.util.function.Supplier;
20+
21+
import org.slf4j.Logger;
22+
import org.slf4j.LoggerFactory;
23+
import org.springframework.beans.BeanUtils;
24+
import org.springframework.boot.context.config.ConfigDataEnvironmentPostProcessor;
25+
import org.springframework.core.env.StandardEnvironment;
26+
import org.springframework.core.io.support.SpringFactoriesLoader;
27+
import org.springframework.util.ClassUtils;
28+
import org.springframework.util.StringUtils;
29+
30+
/**
31+
* A factory for the {@link ApplicationModuleDetectionStrategy} to be used when scanning code for
32+
* {@link ApplicationModule}s.
33+
*
34+
* @author Oliver Drotbohm
35+
*/
36+
class ApplicationModuleDetectionStrategyLookup {
37+
38+
private static final String DETECTION_STRATEGY_PROPERTY = "spring.modulith.detection-strategy";
39+
private static final Logger LOG = LoggerFactory.getLogger(ApplicationModuleDetectionStrategyLookup.class);
40+
private static final Supplier<ApplicationModuleDetectionStrategy> FALLBACK_DETECTION_STRATEGY;
41+
42+
static {
43+
44+
FALLBACK_DETECTION_STRATEGY = () -> {
45+
46+
List<ApplicationModuleDetectionStrategy> loadFactories = SpringFactoriesLoader.loadFactories(
47+
ApplicationModuleDetectionStrategy.class, ApplicationModules.class.getClassLoader());
48+
49+
var size = loadFactories.size();
50+
51+
if (size == 0) {
52+
return ApplicationModuleDetectionStrategy.directSubPackage();
53+
}
54+
55+
if (size > 1) {
56+
57+
throw new IllegalStateException(
58+
"Multiple module detection strategies configured. Only one supported! %s".formatted(loadFactories));
59+
}
60+
61+
LOG.warn(
62+
"Configuring the application module detection strategy via spring.factories is deprecated! Please configure {} instead.",
63+
DETECTION_STRATEGY_PROPERTY);
64+
65+
return loadFactories.get(0);
66+
};
67+
}
68+
69+
/**
70+
* Returns the {@link ApplicationModuleDetectionStrategy} to be used to detect {@link ApplicationModule}s. Will use
71+
* the following algorithm:
72+
* <ol>
73+
* <li>Use the prepared strategies if
74+
*
75+
* @return
76+
*/
77+
static ApplicationModuleDetectionStrategy getStrategy() {
78+
79+
var environment = new StandardEnvironment();
80+
ConfigDataEnvironmentPostProcessor.applyTo(environment);
81+
82+
var configuredStrategy = environment.getProperty(DETECTION_STRATEGY_PROPERTY, String.class);
83+
84+
// Nothing configured? Use fallback.
85+
if (!StringUtils.hasText(configuredStrategy)) {
86+
return FALLBACK_DETECTION_STRATEGY.get();
87+
}
88+
89+
// Any of the prepared ones?
90+
switch (configuredStrategy) {
91+
case "direct-sub-packages":
92+
return ApplicationModuleDetectionStrategy.directSubPackage();
93+
case "explicitly-annotated":
94+
return ApplicationModuleDetectionStrategy.explictlyAnnotated();
95+
}
96+
97+
try {
98+
99+
// Lookup configured value as class
100+
var strategyType = ClassUtils.forName(configuredStrategy, ApplicationModules.class.getClassLoader());
101+
return BeanUtils.instantiateClass(strategyType, ApplicationModuleDetectionStrategy.class);
102+
103+
} catch (ClassNotFoundException | LinkageError o_O) {
104+
throw new IllegalStateException(o_O);
105+
}
106+
}
107+
}

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

Lines changed: 3 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@
3737
import org.jmolecules.archunit.JMoleculesDddRules;
3838
import org.springframework.aot.generate.Generated;
3939
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
40-
import org.springframework.core.io.support.SpringFactoriesLoader;
4140
import org.springframework.lang.Nullable;
4241
import org.springframework.modulith.core.Types.JMoleculesTypes;
4342
import org.springframework.modulith.core.Violations.Violation;
@@ -65,29 +64,14 @@
6564
public class ApplicationModules implements Iterable<ApplicationModule> {
6665

6766
private static final Map<CacheKey, ApplicationModules> CACHE = new ConcurrentHashMap<>();
68-
private static final ApplicationModuleDetectionStrategy DETECTION_STRATEGY;
67+
6968
private static final ImportOption IMPORT_OPTION = new ImportOption.DoNotIncludeTests();
7069
private static final boolean JGRAPHT_PRESENT = ClassUtils.isPresent("org.jgrapht.Graph",
7170
ApplicationModules.class.getClassLoader());
7271
private static final DescribedPredicate<CanBeAnnotated> IS_AOT_TYPE;
7372
private static final DescribedPredicate<HasName> IS_SPRING_CGLIB_PROXY = nameContaining("$$SpringCGLIB$$");
7473

7574
static {
76-
77-
List<ApplicationModuleDetectionStrategy> loadFactories = SpringFactoriesLoader.loadFactories(
78-
ApplicationModuleDetectionStrategy.class,
79-
ApplicationModules.class.getClassLoader());
80-
81-
if (loadFactories.size() > 1) {
82-
83-
throw new IllegalStateException(
84-
String.format("Multiple module detection strategies configured. Only one supported! %s",
85-
loadFactories));
86-
}
87-
88-
DETECTION_STRATEGY = loadFactories.isEmpty() ? ApplicationModuleDetectionStrategies.DIRECT_SUB_PACKAGES
89-
: loadFactories.get(0);
90-
9175
IS_AOT_TYPE = ClassUtils.isPresent("org.springframework.aot.generate.Generated",
9276
ApplicationModules.class.getClassLoader()) ? getAtGenerated() : DescribedPredicate.alwaysFalse();
9377
}
@@ -150,10 +134,11 @@ protected ApplicationModules(ModulithMetadata metadata, Collection<String> packa
150134
Assert.notEmpty(allClasses, () -> "No classes found in packages %s!".formatted(packages));
151135

152136
Classes classes = Classes.of(allClasses);
137+
var strategy = ApplicationModuleDetectionStrategyLookup.getStrategy();
153138

154139
this.modules = packages.stream() //
155140
.map(it -> JavaPackage.of(classes, it))
156-
.flatMap(DETECTION_STRATEGY::getModuleBasePackages) //
141+
.flatMap(strategy::getModuleBasePackages) //
157142
.map(it -> new ApplicationModule(it, useFullyQualifiedModuleNames)) //
158143
.collect(toMap(ApplicationModule::getName, Function.identity()));
159144

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{
2+
"properties": [
3+
{
4+
"name": "spring.modulith.detection-strategy",
5+
"type": "java.lang.String",
6+
"description": "The strategy how to detect application modules."
7+
}
8+
],
9+
"hints": [
10+
{
11+
"name": "spring.modulith.detection-strategy",
12+
"values": [
13+
{
14+
"value": "direct-sub-packages",
15+
"description" : "Selects the direct sub-packages underneath the main application class as application module base interfaces."
16+
},
17+
{
18+
"value": "explicitly-annotated",
19+
"description" : "Only selects explicitly annotated packages as application module base packages (via @ApplicationModules or jMolecules' DDD @Module)."
20+
}
21+
],
22+
"providers": [
23+
{
24+
"name": "class-reference",
25+
"parameters": {
26+
"target": "org.springframework.modulith.core.ApplicationModuleDetectionStrategy"
27+
}
28+
}
29+
]
30+
}
31+
]
32+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/*
2+
* Copyright 2024 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+
20+
import java.util.stream.Stream;
21+
22+
import org.junit.jupiter.api.Test;
23+
24+
/**
25+
* Unit tests for {@link ApplicationModuleDetectionStrategy}.
26+
*
27+
* @author Oliver Drotbohm
28+
*/
29+
class ApplicationModuleDetectionStrategyLookupTests {
30+
31+
@Test // GH-652
32+
void usesExplicitlyAnnotatedStrategyIfConfigured() {
33+
34+
System.setProperty("spring.config.additional-location", "classpath:detection/explicitly-annotated.properties");
35+
36+
assertThat(ApplicationModuleDetectionStrategyLookup.getStrategy())
37+
.isEqualTo(ApplicationModuleDetectionStrategy.explictlyAnnotated());
38+
}
39+
40+
@Test // GH-652
41+
void usesDirectSubPackagesStrategyIfConfigured() {
42+
43+
System.setProperty("spring.config.additional-location", "classpath:detection/direct-sub-packages.properties");
44+
45+
assertThat(ApplicationModuleDetectionStrategyLookup.getStrategy())
46+
.isEqualTo(ApplicationModuleDetectionStrategy.directSubPackage());
47+
}
48+
49+
@Test // GH-652
50+
void usesCustomStrategyIfConfigured() {
51+
52+
System.setProperty("spring.config.additional-location", "classpath:detection/custom-type.properties");
53+
54+
assertThat(ApplicationModuleDetectionStrategyLookup.getStrategy())
55+
.isInstanceOf(TestStrategy.class);
56+
}
57+
58+
static class TestStrategy implements ApplicationModuleDetectionStrategy {
59+
60+
@Override
61+
public Stream<JavaPackage> getModuleBasePackages(JavaPackage basePackage) {
62+
return null;
63+
}
64+
}
65+
}

spring-modulith-core/src/test/java/org/springframework/modulith/core/ModuleDetectionStrategyUnitTest.java

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -29,20 +29,6 @@
2929
*/
3030
class ModuleDetectionStrategyUnitTest {
3131

32-
@Test
33-
void usesExplicitlyAnnotatedConstant() {
34-
35-
assertThat(ApplicationModuleDetectionStrategy.explictlyAnnotated())
36-
.isEqualTo(ApplicationModuleDetectionStrategies.EXPLICITLY_ANNOTATED);
37-
}
38-
39-
@Test
40-
void usesDirectSubPackages() {
41-
42-
assertThat(ApplicationModuleDetectionStrategy.directSubPackage())
43-
.isEqualTo(ApplicationModuleDetectionStrategies.DIRECT_SUB_PACKAGES);
44-
}
45-
4632
@Test
4733
void detectsJMoleculesAnnotatedModule() {
4834

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
spring.modulith.detection-strategy=org.springframework.modulith.core.ApplicationModuleDetectionStrategyLookupTests.TestStrategy
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
spring.modulith.detection-strategy=direct-sub-packages
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
spring.modulith.detection-strategy=explicitly-annotated

0 commit comments

Comments
 (0)