Skip to content

Commit 1062f53

Browse files
committed
GH-613 - Add SPI to support external ApplicationModuleSource contributions.
We now expose ApplicationModuleSourceFactory as Spring Factories-based SPI interface to further contribute ApplicationModuleSource instances either from a provided root package subject for module detection through a (potentially customized) ApplicationModuleDetectionStrategy or by explicitly listing particular module base packages.
1 parent b370d5a commit 1062f53

File tree

19 files changed

+689
-70
lines changed

19 files changed

+689
-70
lines changed

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ public class ApplicationModule implements Comparable<ApplicationModule> {
102102
Assert.notNull(source, "Base package must not be null!");
103103
Assert.notNull(exclusions, "Exclusions must not be null!");
104104

105-
JavaPackage basePackage = source.moduleBasePackage();
105+
JavaPackage basePackage = source.getModuleBasePackage();
106106

107107
this.source = source;
108108
this.basePackage = basePackage;
@@ -144,7 +144,7 @@ public NamedInterfaces getNamedInterfaces() {
144144
* @return will never be {@literal null} or empty.
145145
*/
146146
public String getName() {
147-
return source.moduleName();
147+
return source.getModuleName();
148148
}
149149

150150
/**

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

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

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

2021
import org.springframework.util.Assert;
@@ -29,9 +30,25 @@
2930
* @author Oliver Drotbohm
3031
* @since 1.3
3132
*/
32-
record ApplicationModuleSource(
33-
JavaPackage moduleBasePackage,
34-
String moduleName) {
33+
public class ApplicationModuleSource {
34+
35+
private final JavaPackage moduleBasePackage;
36+
private final String moduleName;
37+
38+
/**
39+
* Creates a new {@link ApplicationModuleSource} for the given module base package and module name.
40+
*
41+
* @param moduleBasePackage must not be {@literal null}.
42+
* @param moduleName must not be {@literal null} or empty.
43+
*/
44+
private ApplicationModuleSource(JavaPackage moduleBasePackage, String moduleName) {
45+
46+
Assert.notNull(moduleBasePackage, "JavaPackage must not be null!");
47+
Assert.hasText(moduleName, "Module name must not be null or empty!");
48+
49+
this.moduleBasePackage = moduleBasePackage;
50+
this.moduleName = moduleName;
51+
}
3552

3653
/**
3754
* Returns a {@link Stream} of {@link ApplicationModuleSource}s by applying the given
@@ -66,4 +83,46 @@ public static ApplicationModuleSource from(JavaPackage pkg, String name) {
6683

6784
return new ApplicationModuleSource(pkg, name);
6885
}
86+
87+
/**
88+
* @return will never be {@literal null}.
89+
*/
90+
public JavaPackage getModuleBasePackage() {
91+
return moduleBasePackage;
92+
}
93+
94+
/**
95+
* @return will never be {@literal null} or empty.
96+
*/
97+
public String getModuleName() {
98+
return moduleName;
99+
}
100+
101+
/*
102+
* (non-Javadoc)
103+
* @see java.lang.Object#equals(java.lang.Object)
104+
*/
105+
@Override
106+
public boolean equals(Object obj) {
107+
108+
if (obj == this) {
109+
return true;
110+
}
111+
112+
if (!(obj instanceof ApplicationModuleSource that)) {
113+
return false;
114+
}
115+
116+
return Objects.equals(this.moduleName, that.moduleName)
117+
&& Objects.equals(this.moduleBasePackage, that.moduleBasePackage);
118+
}
119+
120+
/*
121+
* (non-Javadoc)
122+
* @see java.lang.Object#hashCode()
123+
*/
124+
@Override
125+
public int hashCode() {
126+
return Objects.hash(moduleName, moduleBasePackage);
127+
}
69128
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
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.ArrayList;
19+
import java.util.Collection;
20+
import java.util.List;
21+
import java.util.function.Function;
22+
import java.util.stream.Stream;
23+
24+
import org.springframework.core.io.support.SpringFactoriesLoader;
25+
26+
import com.tngtech.archunit.core.domain.JavaClasses;
27+
28+
/**
29+
* Lookup of external {@link ApplicationModuleSource} contributions via {@link ApplicationModuleSourceFactory}
30+
* implementations.
31+
*
32+
* @author Oliver Drotbohm
33+
* @since 1.3
34+
*/
35+
class ApplicationModuleSourceContributions {
36+
37+
static String LOCATION = SpringFactoriesLoader.FACTORIES_RESOURCE_LOCATION;
38+
39+
private final List<String> rootPackages;
40+
private final List<ApplicationModuleSource> sources;
41+
42+
/**
43+
* Creates a new {@link ApplicationModuleSourceContributions} for the given importer function, default
44+
* {@link ApplicationModuleDetectionStrategy} and whether to use fully-qualified module names.
45+
*
46+
* @param importer must not be {@literal null}.
47+
* @param defaultStrategy must not be {@literal null}.
48+
* @param useFullyQualifiedModuleNames whether to use fully-qualified module names.
49+
*/
50+
public static ApplicationModuleSourceContributions of(Function<Collection<String>, JavaClasses> importer,
51+
ApplicationModuleDetectionStrategy defaultStrategy, boolean useFullyQualifiedModuleNames) {
52+
53+
var loader = SpringFactoriesLoader.forResourceLocation(LOCATION, ApplicationModules.class.getClassLoader());
54+
55+
return new ApplicationModuleSourceContributions(loader.load(ApplicationModuleSourceFactory.class), importer,
56+
defaultStrategy, useFullyQualifiedModuleNames);
57+
}
58+
59+
/**
60+
* Creates a new {@link ApplicationModuleSourceContributions} for the given {@link ApplicationModuleSourceFactory}s,
61+
* importer function, default {@link ApplicationModuleDetectionStrategy} and whether to use fully-qualified module
62+
* names.
63+
*
64+
* @param factories must not be {@literal null}.
65+
* @param importer must not be {@literal null}.
66+
* @param defaultStrategy must not be {@literal null}.
67+
* @param useFullyQualifiedModuleNames whether to use fully-qualified module names.
68+
*/
69+
ApplicationModuleSourceContributions(List<? extends ApplicationModuleSourceFactory> factories,
70+
Function<Collection<String>, JavaClasses> importer,
71+
ApplicationModuleDetectionStrategy defaultStrategy, boolean useFullyQualifiedModuleNames) {
72+
73+
this.rootPackages = new ArrayList<>();
74+
this.sources = new ArrayList<>();
75+
76+
factories.forEach(factory -> {
77+
78+
var contributedPackages = factory.getRootPackages();
79+
var factoryStrategy = factory.getApplicationModuleDetectionStrategy();
80+
var classes = importer.apply(contributedPackages);
81+
var strategy = factoryStrategy == null ? defaultStrategy : factoryStrategy;
82+
83+
// Add discovered ApplicationModuleSources
84+
85+
rootPackages.addAll(contributedPackages);
86+
87+
contributedPackages.stream()
88+
.map(it -> JavaPackage.of(Classes.of(classes), it))
89+
.flatMap(it -> factory.getApplicationModuleSources(it, strategy, useFullyQualifiedModuleNames))
90+
.forEach(this.sources::add);
91+
92+
// Add enumerated ApplicationModuleSources
93+
94+
Function<String, JavaPackage> packageRegistrar = it -> {
95+
return JavaPackage.of(Classes.of(importer.apply(List.of(it))), it);
96+
};
97+
98+
factory.getApplicationModuleSources(packageRegistrar, useFullyQualifiedModuleNames).forEach(this.sources::add);
99+
});
100+
}
101+
102+
/**
103+
* @return will never be {@literal null}.
104+
*/
105+
public Stream<String> getRootPackages() {
106+
return rootPackages.stream();
107+
}
108+
109+
/**
110+
* @return will never be {@literal null}.
111+
*/
112+
public Stream<ApplicationModuleSource> getSources() {
113+
return sources.stream();
114+
}
115+
}
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
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.Collections;
19+
import java.util.List;
20+
import java.util.function.Function;
21+
import java.util.stream.Stream;
22+
23+
import org.springframework.lang.Nullable;
24+
25+
/**
26+
* SPI to allow build units contribute additional {@link ApplicationModuleSource}s in the form of either declaring them
27+
* directly via {@link #getModuleBasePackages()} and {@link #getApplicationModuleSources(Function, boolean)} or via
28+
* provided {@link #getRootPackages()} and subsequent resolution via
29+
* {@link #getApplicationModuleSources(JavaPackage, ApplicationModuleDetectionStrategy, boolean)} for each of the
30+
* packages provided. <br>
31+
* The following snippet would register {@link ApplicationModuleSource}s for {@code com.acme.foo} and
32+
* {@code com.acme.bar} directly:
33+
*
34+
* <pre>
35+
* {@code
36+
* class MyCustomFactory implements ApplicationModuleSourceFactory {
37+
*
38+
* &#64;Override
39+
* public List<String> getModuleBasePackages() {
40+
* return List.of("com.acme.foo", "com.acme.bar");
41+
* }
42+
* }
43+
* }
44+
* </pre>
45+
*
46+
* The following snippet would register all modules located underneath {@code com.acme} found via the
47+
* {@link ApplicationModuleDetectionStrategy#explicitlyAnnotated()} strategy:
48+
*
49+
* <pre>
50+
* {@code
51+
* class MyCustomFactory implements ApplicationModuleSourceFactory {
52+
*
53+
* &#64;Override
54+
* public List<String> getRootPackages() {
55+
* return List.of("com.acme");
56+
* }
57+
*
58+
* &#64;Override
59+
* ApplicationModuleDetectionStrategy getApplicationModuleDetectionStrategy() {
60+
* return ApplicationModuleDetectionStrategy.explicitlyAnnotated();
61+
* }
62+
* }
63+
* }
64+
* </pre>
65+
*
66+
* @author Oliver Drotbohm
67+
* @since 1.3
68+
*/
69+
public interface ApplicationModuleSourceFactory {
70+
71+
/**
72+
* Returns the additional root packages to be considered. The ones returned from this method will be scanned for
73+
* {@link ApplicationModuleSource}s via
74+
* {@link #getApplicationModuleSources(JavaPackage, ApplicationModuleDetectionStrategy, boolean)} using the
75+
* {@link ApplicationModuleDetectionStrategy} returned from {@link #getApplicationModuleDetectionStrategy()}. If the
76+
* latter is {@literal null}, the default {@link ApplicationModuleDetectionStrategy} is used.
77+
*
78+
* @return must not be {@literal null}.
79+
*/
80+
default List<String> getRootPackages() {
81+
return Collections.emptyList();
82+
}
83+
84+
/**
85+
* Returns additional module base packages to create {@link ApplicationModuleSource}s from. Subsequently handled by
86+
* {@link #getApplicationModuleSources(Function, boolean)}.
87+
*
88+
* @return must not be {@literal null}.
89+
*/
90+
default List<String> getModuleBasePackages() {
91+
return Collections.emptyList();
92+
}
93+
94+
/**
95+
* Returns the {@link ApplicationModuleDetectionStrategy} to be used to detect {@link ApplicationModuleSource}s from
96+
* the packages returned by {@link #getRootPackages()}. If {@literal null} is returned, the default
97+
* {@link ApplicationModuleDetectionStrategy} will be used.
98+
*
99+
* @return can be {@literal null}.
100+
*/
101+
@Nullable
102+
default ApplicationModuleDetectionStrategy getApplicationModuleDetectionStrategy() {
103+
return null;
104+
}
105+
106+
/**
107+
* Creates all {@link ApplicationModuleSource}s using the given base package and
108+
* {@link ApplicationModuleDetectionStrategy}.
109+
*
110+
* @param rootPackage will never be {@literal null}.
111+
* @param strategy will never be {@literal null}.
112+
* @param useFullyQualifiedModuleNames whether to use fully-qualified names for application modules.
113+
* @return must not be {@literal null}.
114+
* @see ApplicationModuleSource#from(JavaPackage, ApplicationModuleDetectionStrategy, boolean)
115+
*/
116+
default Stream<ApplicationModuleSource> getApplicationModuleSources(JavaPackage rootPackage,
117+
ApplicationModuleDetectionStrategy strategy, boolean useFullyQualifiedModuleNames) {
118+
119+
return ApplicationModuleSource.from(rootPackage, strategy, useFullyQualifiedModuleNames);
120+
}
121+
122+
/**
123+
* Creates {@link ApplicationModuleSource} for individually, manually described application modules.
124+
*
125+
* @param packages will never be {@literal null}.
126+
* @param useFullyQualifiedModuleNames whether to use fully-qualified names for application modules.
127+
* @return must not be {@literal null}.
128+
* @see ApplicationModuleSource#from(JavaPackage, String)
129+
*/
130+
default Stream<ApplicationModuleSource> getApplicationModuleSources(Function<String, JavaPackage> packages,
131+
boolean useFullyQualifiedModuleNames) {
132+
133+
return getModuleBasePackages().stream()
134+
.map(packages)
135+
.map(it -> ApplicationModuleSource.from(it, useFullyQualifiedModuleNames ? it.getName() : it.getLocalName()));
136+
}
137+
}

0 commit comments

Comments
 (0)