Skip to content

Commit a1f6b48

Browse files
committed
GH-578 - Support for nested application modules.
The ApplicationModules bootstrap now triggers the module base package detection, followed by a new, additional pass of detecting nested application module packages. Those packages are now added to the ones we create ApplicationModule instances for and also handed into the module instance creation step as exclusions to make sure that parent modules do not include code residing in sub-modules. Each module now operates on the Classes instance obtained from the JavaPackage instance that constitutes the module's base package but filtered by the given exclusions.
1 parent 8ccd03c commit a1f6b48

27 files changed

+1075
-90
lines changed

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

Lines changed: 137 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,14 @@
1818
import static com.tngtech.archunit.base.DescribedPredicate.*;
1919
import static com.tngtech.archunit.core.domain.JavaClass.Predicates.*;
2020
import static java.lang.System.*;
21+
import static java.util.Comparator.*;
2122
import static org.springframework.modulith.core.SyntacticSugar.*;
2223
import static org.springframework.modulith.core.Types.JavaXTypes.*;
2324
import static org.springframework.modulith.core.Types.SpringDataTypes.*;
2425
import static org.springframework.modulith.core.Types.SpringTypes.*;
2526

2627
import java.util.Arrays;
28+
import java.util.Collection;
2729
import java.util.Collections;
2830
import java.util.List;
2931
import java.util.Map;
@@ -57,12 +59,15 @@
5759
*
5860
* @author Oliver Drotbohm
5961
*/
60-
public class ApplicationModule {
62+
public class ApplicationModule implements Comparable<ApplicationModule> {
6163

6264
/**
6365
* The base package of the {@link ApplicationModule}.
6466
*/
6567
private final JavaPackage basePackage;
68+
private final Classes classes;
69+
private final JavaPackages exclusions;
70+
6671
private final ApplicationModuleInformation information;
6772

6873
/**
@@ -77,25 +82,31 @@ public class ApplicationModule {
7782
private final Supplier<List<JavaClass>> valueTypes;
7883
private final Supplier<List<EventType>> publishedEvents;
7984

85+
ApplicationModule(JavaPackage basePackage, boolean useFullyQualifiedModuleNames) {
86+
this(basePackage, JavaPackages.NONE, useFullyQualifiedModuleNames);
87+
}
88+
8089
/**
8190
* Creates a new {@link ApplicationModule} for the given base package and whether to use fully-qualified module names.
8291
*
8392
* @param basePackage must not be {@literal null}.
8493
* @param useFullyQualifiedModuleNames
8594
*/
86-
ApplicationModule(JavaPackage basePackage, boolean useFullyQualifiedModuleNames) {
95+
ApplicationModule(JavaPackage basePackage, JavaPackages exclusions, boolean useFullyQualifiedModuleNames) {
8796

8897
Assert.notNull(basePackage, "Base package must not be null!");
8998

9099
this.basePackage = basePackage;
100+
this.exclusions = exclusions;
101+
this.classes = basePackage.getClasses(exclusions);
91102
this.information = ApplicationModuleInformation.of(basePackage);
92103
this.namedInterfaces = isOpen()
93104
? NamedInterfaces.forOpen(basePackage)
94105
: NamedInterfaces.discoverNamedInterfaces(basePackage);
95106
this.useFullyQualifiedModuleNames = useFullyQualifiedModuleNames;
96107

97-
this.springBeans = SingletonSupplier.of(() -> filterSpringBeans(basePackage));
98-
this.aggregateRoots = SingletonSupplier.of(() -> findAggregateRoots(basePackage));
108+
this.springBeans = SingletonSupplier.of(() -> filterSpringBeans(classes));
109+
this.aggregateRoots = SingletonSupplier.of(() -> findAggregateRoots(classes));
99110
this.valueTypes = SingletonSupplier
100111
.of(() -> findArchitecturallyEvidentType(ArchitecturallyEvidentType::isValueObject));
101112
this.publishedEvents = SingletonSupplier.of(() -> findPublishedEvents());
@@ -275,7 +286,7 @@ public ArchitecturallyEvidentType getArchitecturallyEvidentType(Class<?> type) {
275286
}
276287

277288
public boolean contains(JavaClass type) {
278-
return basePackage.contains(type);
289+
return classes.contains(type);
279290
}
280291

281292
public boolean contains(@Nullable Class<?> type) {
@@ -292,7 +303,7 @@ public Optional<JavaClass> getType(String candidate) {
292303

293304
Assert.hasText(candidate, "Candidate must not be null or emtpy!");
294305

295-
return basePackage.stream()
306+
return classes.stream()
296307
.filter(hasSimpleOrFullyQualifiedName(candidate))
297308
.findFirst();
298309
}
@@ -366,6 +377,19 @@ public String toString(@Nullable ApplicationModules modules) {
366377
builder.append("> Logical name: ").append(getName()).append('\n');
367378
builder.append("> Base package: ").append(basePackage.getName()).append('\n');
368379

380+
builder.append("> Excluded packages: ");
381+
382+
if (!exclusions.iterator().hasNext()) {
383+
builder.append("none").append('\n');
384+
} else {
385+
386+
builder.append('\n');
387+
388+
exclusions.stream().forEach(it -> {
389+
builder.append(" - ").append(it.getName()).append('\n');
390+
});
391+
}
392+
369393
if (namedInterfaces.hasExplicitInterfaces()) {
370394

371395
builder.append("> Named interfaces:\n");
@@ -510,12 +534,21 @@ public int hashCode() {
510534
useFullyQualifiedModuleNames, valueTypes);
511535
}
512536

537+
/*
538+
* (non-Javadoc)
539+
* @see java.lang.Comparable#compareTo(java.lang.Object)
540+
*/
541+
@Override
542+
public int compareTo(ApplicationModule o) {
543+
return getBasePackage().compareTo(o.getBasePackage());
544+
}
545+
513546
private List<EventType> findPublishedEvents() {
514547

515548
DescribedPredicate<JavaClass> isEvent = implement(JMoleculesTypes.DOMAIN_EVENT) //
516549
.or(isAnnotatedWith(JMoleculesTypes.AT_DOMAIN_EVENT));
517550

518-
return basePackage.that(isEvent).stream() //
551+
return classes.that(isEvent).stream() //
519552
.map(EventType::new).toList();
520553
}
521554

@@ -537,7 +570,7 @@ private Stream<JavaClass> resolveModuleSuperTypes(JavaClass type) {
537570

538571
private Stream<QualifiedDependency> getAllModuleDependencies(ApplicationModules modules) {
539572

540-
return basePackage.stream() //
573+
return classes.stream() //
541574
.flatMap(it -> getModuleDependenciesOf(it, modules));
542575
}
543576

@@ -590,7 +623,7 @@ private boolean isDependencyToOtherModule(JavaClass dependency, ApplicationModul
590623
return modules.contains(dependency) && !contains(dependency);
591624
}
592625

593-
private Classes findAggregateRoots(JavaPackage source) {
626+
private Classes findAggregateRoots(Classes source) {
594627

595628
return source.stream() //
596629
.map(it -> ArchitecturallyEvidentType.of(it, getSpringBeansInternal()))
@@ -599,11 +632,71 @@ private Classes findAggregateRoots(JavaPackage source) {
599632
.collect(Classes.toClasses());
600633
}
601634

635+
/**
636+
* Returns the current module's immediate parent module, if present.
637+
*
638+
* @param modules must not be {@literal null}.
639+
* @return will never be {@literal null}.
640+
* @since 1.3
641+
*/
642+
Optional<ApplicationModule> getParentModule(ApplicationModules modules) {
643+
644+
Assert.notNull(modules, "ApplicationModules must not be null!");
645+
646+
var byPackageDepth = comparing(ApplicationModule::getBasePackage, JavaPackage.reverse());
647+
648+
return modules.stream()
649+
.filter(it -> basePackage.isSubPackageOf(it.getBasePackage()))
650+
.sorted(byPackageDepth)
651+
.findFirst();
652+
}
653+
654+
/**
655+
* Returns the {@link ApplicationModule}s directly nested inside the current one.
656+
*
657+
* @param modules must not be {@literal null}.
658+
* @return will never be {@literal null}.
659+
* @since 1.3
660+
*/
661+
Collection<ApplicationModule> getDirectlyNestedModules(ApplicationModules modules) {
662+
663+
Assert.notNull(modules, "ApplicationModules must not be null!");
664+
665+
return doGetNestedModules(modules, false);
666+
}
667+
668+
/**
669+
* Returns all of the current {@link ApplicationModule}'s nested {@link ApplicationModule}s including ones contained
670+
* in nested modules in turn.
671+
*
672+
* @param modules must not be {@literal null}.
673+
* @return will never be {@literal null}.
674+
* @since 1.3
675+
*/
676+
Collection<ApplicationModule> getNestedModules(ApplicationModules modules) {
677+
678+
Assert.notNull(modules, "ApplicationModules must not be null!");
679+
680+
return doGetNestedModules(modules, true);
681+
}
682+
602683
private String getQualifiedName(NamedInterface namedInterface) {
603684
return namedInterface.getQualifiedName(getName());
604685
}
605686

606-
private static Classes filterSpringBeans(JavaPackage source) {
687+
private Collection<ApplicationModule> doGetNestedModules(ApplicationModules modules, boolean recursive) {
688+
689+
var result = modules.stream()
690+
.filter(it -> it.getParentModule(modules).filter(this::equals).isPresent());
691+
692+
if (recursive) {
693+
result = result.flatMap(it -> Stream.concat(Stream.of(it), it.getNestedModules(modules).stream()));
694+
}
695+
696+
return result.toList();
697+
}
698+
699+
private static Classes filterSpringBeans(Classes source) {
607700

608701
Map<Boolean, List<JavaClass>> collect = source.that(isConfiguration()).stream() //
609702
.flatMap(it -> it.getMethods().stream()) //
@@ -632,7 +725,7 @@ private List<JavaClass> findArchitecturallyEvidentType(Predicate<Architecturally
632725

633726
var springBeansInternal = getSpringBeansInternal();
634727

635-
return basePackage.stream()
728+
return classes.stream()
636729
.map(it -> ArchitecturallyEvidentType.of(it, springBeansInternal))
637730
.filter(selector)
638731
.map(ArchitecturallyEvidentType::getType)
@@ -907,6 +1000,9 @@ static class QualifiedDependency {
9071000

9081001
private static final List<String> INJECTION_TYPES = Arrays.asList(AT_AUTOWIRED, AT_RESOURCE, AT_INJECT);
9091002

1003+
private static final String INVALID_SUB_MODULE_REFERENCE = "Invalid sub-module reference from module '%s' to module '%s' (via %s -> %s)!";
1004+
private static final String INTERNAL_REFERENCE = "Module '%s' depends on non-exposed type %s within module '%s'!";
1005+
9101006
private final JavaClass source, target;
9111007
private final String description;
9121008
private final DependencyType type;
@@ -1045,13 +1141,41 @@ Violations isValidDependencyWithin(ApplicationModules modules) {
10451141

10461142
if (!targetModule.isExposed(target)) {
10471143

1048-
var violationText = "Module '%s' depends on non-exposed type %s within module '%s'!"
1144+
var violationText = INTERNAL_REFERENCE
10491145
.formatted(originModule.getName(), target.getName(), targetModule.getName());
10501146

10511147
return violations.and(new Violation(violationText + lineSeparator() + description));
10521148
}
10531149

1054-
return violations;
1150+
// Parent child relationships
1151+
1152+
var targetParent = targetModule.getParentModule(modules);
1153+
1154+
if (targetParent.isEmpty()) {
1155+
return violations;
1156+
}
1157+
1158+
var originParent = originModule.getParentModule(modules);
1159+
1160+
if (targetParent.isPresent()) {
1161+
1162+
var resolved = targetParent.get();
1163+
1164+
if (resolved.equals(originModule)) {
1165+
return violations;
1166+
}
1167+
1168+
if (originParent.isPresent() && originParent.get().equals(targetModule)) {
1169+
return violations;
1170+
}
1171+
}
1172+
1173+
var violationText = INVALID_SUB_MODULE_REFERENCE
1174+
.formatted(originModule.getName(), targetModule.getName(),
1175+
FormatableType.of(source).getAbbreviatedFullName(originModule),
1176+
FormatableType.of(target).getAbbreviatedFullName(targetModule));
1177+
1178+
return violations.and(new Violation(violationText));
10551179
}
10561180

10571181
ApplicationModule getExistingModuleOf(JavaClass javaClass, ApplicationModules modules) {

0 commit comments

Comments
 (0)