Skip to content

Commit f09f3aa

Browse files
committed
GH-802 - Allow transitive application module dependency resolution.
ApplicationModule now exposes both getDirectDependencies(…) and getAllDependencies(…), the former as alias for the now deprecated getDependencies(…) for symmetry reasons. The latter recursively resolves transitive dependencies. We now optimize the dependency analysis by skipping types residing java and javax packages as they're not relevant to our dependency arrangement model. A few additional optimizations in ApplicationModuleDependencies to avoid iterating over each establishing dependency if all we need to look at is the general module dependency arrangement. Improve performance of ApplicationModule.contains(…) checks by checking whether the given type can even live inside the package space of the module.
1 parent d5ab8b6 commit f09f3aa

File tree

11 files changed

+295
-121
lines changed

11 files changed

+295
-121
lines changed

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

Lines changed: 162 additions & 91 deletions
Large diffs are not rendered by default.

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

Lines changed: 61 additions & 16 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.Collection;
1819
import java.util.HashSet;
1920
import java.util.List;
2021
import java.util.function.Function;
@@ -30,22 +31,23 @@
3031
public class ApplicationModuleDependencies {
3132

3233
private final List<ApplicationModuleDependency> dependencies;
33-
private final ApplicationModules modules;
34+
private final Collection<ApplicationModule> modules;
3435

3536
/**
3637
* Creates a new {@link ApplicationModuleDependencies} for the given {@link List} of
3738
* {@link ApplicationModuleDependency} and {@link ApplicationModules}.
3839
*
3940
* @param dependencies must not be {@literal null}.
40-
* @param modules must not be {@literal null}.
4141
*/
42-
private ApplicationModuleDependencies(List<ApplicationModuleDependency> dependencies, ApplicationModules modules) {
42+
private ApplicationModuleDependencies(List<ApplicationModuleDependency> dependencies) {
4343

4444
Assert.notNull(dependencies, "ApplicationModuleDependency list must not be null!");
45-
Assert.notNull(modules, "ApplicationModules must not be null!");
4645

4746
this.dependencies = dependencies;
48-
this.modules = modules;
47+
this.modules = dependencies.stream()
48+
.map(ApplicationModuleDependency::getTargetModule)
49+
.distinct()
50+
.toList();
4951
}
5052

5153
/**
@@ -56,10 +58,8 @@ private ApplicationModuleDependencies(List<ApplicationModuleDependency> dependen
5658
* @param modules must not be {@literal null}.
5759
* @return will never be {@literal null}.
5860
*/
59-
static ApplicationModuleDependencies of(List<ApplicationModuleDependency> dependencies,
60-
ApplicationModules modules) {
61-
62-
return new ApplicationModuleDependencies(dependencies, modules);
61+
static ApplicationModuleDependencies of(List<ApplicationModuleDependency> dependencies) {
62+
return new ApplicationModuleDependencies(dependencies);
6363
}
6464

6565
/**
@@ -72,9 +72,7 @@ public boolean contains(ApplicationModule module) {
7272

7373
Assert.notNull(module, "ApplicationModule must not be null!");
7474

75-
return dependencies.stream()
76-
.map(ApplicationModuleDependency::getTargetModule)
77-
.anyMatch(module::equals);
75+
return modules.contains(module);
7876
}
7977

8078
/**
@@ -87,8 +85,7 @@ public boolean containsModuleNamed(String name) {
8785

8886
Assert.hasText(name, "Module name must not be null or empty!");
8987

90-
return dependencies.stream()
91-
.map(ApplicationModuleDependency::getTargetModule)
88+
return modules.stream()
9289
.map(ApplicationModule::getName)
9390
.anyMatch(name::equals);
9491
}
@@ -119,13 +116,22 @@ public Stream<ApplicationModuleDependency> uniqueStream(Function<ApplicationModu
119116
.filter(it -> seenTargets.add(extractor.apply(it)));
120117
}
121118

119+
/**
120+
* Returns a new {@link ApplicationModuleDependencies} instance containing only the dependencies of the given
121+
* {@link DependencyType}.
122+
*
123+
* @param type must not be {@literal null}.
124+
* @return
125+
*/
122126
public ApplicationModuleDependencies withType(DependencyType type) {
123127

128+
Assert.notNull(type, "DependencyType must not be null!");
129+
124130
var filtered = dependencies.stream()
125131
.filter(it -> it.getDependencyType().equals(type))
126132
.toList();
127133

128-
return ApplicationModuleDependencies.of(filtered, modules);
134+
return ApplicationModuleDependencies.of(filtered);
129135
}
130136

131137
/**
@@ -134,6 +140,45 @@ public ApplicationModuleDependencies withType(DependencyType type) {
134140
* @return will never be {@literal null}.
135141
*/
136142
public boolean isEmpty() {
137-
return dependencies.isEmpty();
143+
return modules.isEmpty();
144+
}
145+
146+
/**
147+
* Returns whether the dependencies contain the type with the given fully-qualified name.
148+
*
149+
* @param type must not be {@literal null} or empty.
150+
* @return
151+
* @since 1.3
152+
*/
153+
public boolean contains(String type) {
154+
155+
Assert.hasText(type, "Type must not be null or empty!");
156+
157+
return uniqueModules().anyMatch(it -> it.contains(type));
158+
}
159+
160+
/**
161+
* Returns all unique {@link ApplicationModule}s involved in the dependencies.
162+
*
163+
* @return will never be {@literal null}.
164+
*/
165+
public Stream<ApplicationModule> uniqueModules() {
166+
return modules.stream();
167+
}
168+
169+
/**
170+
* Returns the {@link ApplicationModule} containing the given type.
171+
*
172+
* @param name must not be {@literal null} or empty.
173+
* @return will never be {@literal null}.
174+
*/
175+
public ApplicationModule getModuleByType(String name) {
176+
177+
Assert.hasText(name, "Name must not be null or empty!");
178+
179+
return uniqueModules()
180+
.filter(it -> it.contains(name))
181+
.findFirst()
182+
.orElse(null);
138183
}
139184
}

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -778,8 +778,7 @@ private static List<String> topologicallySortModules(ApplicationModules modules)
778778

779779
graph.addVertex(project);
780780

781-
project.getDependencies(modules).stream() //
782-
.map(ApplicationModuleDependency::getTargetModule) //
781+
project.getDirectDependencies(modules).uniqueModules() //
783782
.forEach(dependency -> {
784783
graph.addVertex(dependency);
785784
graph.addEdge(project, dependency);

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

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package org.springframework.modulith.core;
1717

1818
import org.springframework.util.Assert;
19+
import org.springframework.util.ClassUtils;
1920

2021
/**
2122
* The name of a Java package. Packages are sortable comparing their individual segments and deeper packages sorted
@@ -32,16 +33,29 @@ class PackageName implements Comparable<PackageName> {
3233
/**
3334
* Creates a new {@link PackageName} with the given name.
3435
*
35-
* @param name must not be {@literal null} or empty.
36+
* @param name must not be {@literal null}.
3637
*/
3738
public PackageName(String name) {
3839

39-
Assert.hasText(name, "Name must not be null or empty!");
40+
Assert.notNull(name, "Name must not be null!");
4041

4142
this.name = name;
4243
this.segments = name.split("\\.");
4344
}
4445

46+
/**
47+
* Creates a new {@link PackageName} for the given fully-qualified type name.
48+
*
49+
* @param fullyQualifiedName must not be {@literal null} or empty.
50+
* @return will never be {@literal null}.
51+
*/
52+
public static PackageName ofType(String fullyQualifiedName) {
53+
54+
Assert.notNull(fullyQualifiedName, "Type name must not be null!");
55+
56+
return new PackageName(ClassUtils.getPackageName(fullyQualifiedName));
57+
}
58+
4559
/**
4660
* Returns the length of the package name.
4761
*
@@ -118,6 +132,19 @@ boolean isParentPackageOf(PackageName reference) {
118132
return reference.name.startsWith(name + ".");
119133
}
120134

135+
/**
136+
* Returns whether the package name contains the given one, i.e. if the given one either is the current one or a
137+
* sub-package of it.
138+
*
139+
* @param reference must not be {@literal null}.
140+
*/
141+
boolean contains(PackageName reference) {
142+
143+
Assert.notNull(reference, "Reference package name must not be null!");
144+
145+
return this.equals(reference) || reference.isSubPackageOf(this);
146+
}
147+
121148
/**
122149
* Returns whether the current {@link PackageName} is the name of a sub-package with the given name.
123150
*
@@ -131,6 +158,10 @@ boolean isSubPackageOf(PackageName reference) {
131158
return name.startsWith(reference.name + ".");
132159
}
133160

161+
boolean isEmpty() {
162+
return length() == 0;
163+
}
164+
134165
/*
135166
* (non-Javadoc)
136167
* @see java.lang.Comparable#compareTo(java.lang.Object)

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import static org.springframework.modulith.core.SyntacticSugar.*;
2020

2121
import java.lang.annotation.Annotation;
22+
import java.util.function.Predicate;
2223

2324
import org.springframework.lang.Nullable;
2425
import org.springframework.modulith.PackageInfo;
@@ -79,6 +80,14 @@ public static boolean areRulesPresent() {
7980
}
8081
}
8182

83+
static class JavaTypes {
84+
85+
static Predicate<JavaClass> IS_CORE_JAVA_TYPE = it -> it.getName().startsWith("java.")
86+
|| it.getName().startsWith("javax.");
87+
88+
static Predicate<JavaClass> IS_NOT_CORE_JAVA_TYPE = Predicate.not(IS_CORE_JAVA_TYPE);
89+
}
90+
8291
static class JavaXTypes {
8392

8493
private static final String BASE_PACKAGE = "jakarta";

spring-modulith-core/src/main/java/org/springframework/modulith/core/util/ApplicationModulesExporter.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ private static Map<String, Object> toInfo(ApplicationModule module, ApplicationM
138138
json.put("namedInterfaces", toNamedInterfaces(module.getNamedInterfaces()));
139139
}
140140

141-
json.put("dependencies", module.getDependencies(modules).stream() //
141+
json.put("dependencies", module.getDirectDependencies(modules).stream() //
142142
.collect(Collectors.groupingBy(ApplicationModuleDependency::getTargetModule, MAPPER))
143143
.entrySet() //
144144
.stream() //

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,4 +44,15 @@ void sortsPackagesByNameAndDepth() {
4444
.map(it -> it.getLocalName("com")))
4545
.containsExactly("acme.b", "acme.a.second", "acme.a.first.one", "acme.a.first", "acme.a", "acme");
4646
}
47+
48+
@Test // GH-802
49+
void caculatesNestingCorrectly() {
50+
51+
var comAcme = new PackageName("com.acme");
52+
var comAcmeA = new PackageName("com.acme.a");
53+
54+
assertThat(comAcme.contains(comAcme)).isTrue();
55+
assertThat(comAcme.contains(comAcmeA)).isTrue();
56+
assertThat(comAcmeA.contains(comAcme)).isFalse();
57+
}
4758
}

spring-modulith-docs/src/main/java/org/springframework/modulith/docs/Asciidoctor.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -359,7 +359,7 @@ public String toAsciidoctor(String source) {
359359
*/
360360
public String renderBeanReferences(ApplicationModule module) {
361361

362-
var bullets = module.getDependencies(modules, DependencyType.USES_COMPONENT)
362+
var bullets = module.getDirectDependencies(modules, DependencyType.USES_COMPONENT)
363363
.uniqueStream(ApplicationModuleDependency::getTargetType)
364364
.map(it -> "%s (in %s)".formatted(toInlineCode(it.getTargetType()), it.getTargetModule().getDisplayName()))
365365
.map(this::toBulletPoint)

spring-modulith-docs/src/main/java/org/springframework/modulith/docs/Documenter.java

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@
3636

3737
import org.springframework.lang.Nullable;
3838
import org.springframework.modulith.core.ApplicationModule;
39-
import org.springframework.modulith.core.ApplicationModuleDependency;
4039
import org.springframework.modulith.core.ApplicationModules;
4140
import org.springframework.modulith.core.DependencyDepth;
4241
import org.springframework.modulith.core.DependencyType;
@@ -442,8 +441,7 @@ private void addDependencies(ApplicationModule module, Component component, Diag
442441

443442
DEPENDENCY_DESCRIPTIONS.entrySet().stream().forEach(entry -> {
444443

445-
module.getDependencies(modules, entry.getKey()).stream() //
446-
.map(ApplicationModuleDependency::getTargetModule) //
444+
module.getDirectDependencies(modules, entry.getKey()).uniqueModules() //
447445
.map(it -> getComponents(options).get(it)) //
448446
.map(it -> component.uses(it, entry.getValue())) //
449447
.filter(it -> it != null) //
@@ -475,8 +473,7 @@ private void addComponentsToView(ApplicationModule module, ComponentView view, D
475473
Supplier<Stream<ApplicationModule>> bootstrapDependencies = () -> module.getBootstrapDependencies(modules,
476474
options.dependencyDepth);
477475
Supplier<Stream<ApplicationModule>> otherDependencies = () -> options.getDependencyTypes()
478-
.flatMap(it -> module.getDependencies(modules, it).stream()
479-
.map(ApplicationModuleDependency::getTargetModule));
476+
.flatMap(it -> module.getDirectDependencies(modules, it).uniqueModules());
480477

481478
Supplier<Stream<ApplicationModule>> dependencies = () -> Stream.concat(bootstrapDependencies.get(),
482479
otherDependencies.get());

spring-modulith-integration-test/src/test/java/com/acme/myproject/ModulithTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,10 +110,10 @@ void configrationPropertiesTypesEstablishSimpleDependency() {
110110

111111
assertThat(modules.getModuleByName("moduleD")).hasValueSatisfying(it -> {
112112

113-
assertThat(it.getDependencies(modules, DependencyType.DEFAULT))
113+
assertThat(it.getDirectDependencies(modules, DependencyType.DEFAULT))
114114
.matches(inner -> inner.containsModuleNamed("moduleC"));
115115

116-
assertThat(it.getDependencies(modules, DependencyType.USES_COMPONENT))
116+
assertThat(it.getDirectDependencies(modules, DependencyType.USES_COMPONENT))
117117
.matches(ApplicationModuleDependencies::isEmpty);
118118
});
119119
}

0 commit comments

Comments
 (0)