Skip to content

Commit 0d43b1f

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. The bootstrap of ApplicationModules now uses a dedicated ApplicationModuleSource to allow calculating a default module name relative to the application base package. 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 0d43b1f

31 files changed

+1312
-130
lines changed

pom.xml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -299,11 +299,11 @@ limitations under the License.
299299
<extensions>true</extensions>
300300
<configuration>
301301
<packages>
302-
<package>@antora/[email protected].1</package>
303-
<package>@antora/[email protected]alpha.3</package>
304-
<package>@asciidoctor/[email protected].3</package>
305-
<package>@springio/antora-extensions@1.5.0</package>
306-
<package>@springio/[email protected].9</package>
302+
<package>@antora/[email protected].2</package>
303+
<package>@antora/[email protected]beta.2</package>
304+
<package>@asciidoctor/[email protected].6</package>
305+
<package>@springio/antora-extensions@1.13.1</package>
306+
<package>@springio/[email protected].12</package>
307307
<package>asciidoctor-kroki</package>
308308
</packages>
309309
<playbook>../src/docs/antora/antora-playbook.yml</playbook>

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

Lines changed: 184 additions & 23 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,45 +59,62 @@
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
/**
6974
* All {@link NamedInterfaces} of the {@link ApplicationModule} either declared explicitly via {@link NamedInterface}
7075
* or implicitly.
7176
*/
7277
private final NamedInterfaces namedInterfaces;
73-
private final boolean useFullyQualifiedModuleNames;
78+
private final ApplicationModuleSource source;
7479

7580
private final Supplier<Classes> springBeans;
7681
private final Supplier<Classes> aggregateRoots;
7782
private final Supplier<List<JavaClass>> valueTypes;
7883
private final Supplier<List<EventType>> publishedEvents;
7984

85+
/**
86+
* Creates a new {@link ApplicationModule} from the given {@link ApplicationModuleSource}.
87+
*
88+
* @param source must not be {@literal null}.
89+
*/
90+
ApplicationModule(ApplicationModuleSource source) {
91+
this(source, JavaPackages.NONE);
92+
}
93+
8094
/**
8195
* Creates a new {@link ApplicationModule} for the given base package and whether to use fully-qualified module names.
8296
*
83-
* @param basePackage must not be {@literal null}.
84-
* @param useFullyQualifiedModuleNames
97+
* @param source must not be {@literal null}.
98+
* @param exclusions must not be {@literal null}.
8599
*/
86-
ApplicationModule(JavaPackage basePackage, boolean useFullyQualifiedModuleNames) {
100+
ApplicationModule(ApplicationModuleSource source, JavaPackages exclusions) {
87101

88-
Assert.notNull(basePackage, "Base package must not be null!");
102+
Assert.notNull(source, "Base package must not be null!");
103+
Assert.notNull(exclusions, "Exclusions must not be null!");
89104

105+
JavaPackage basePackage = source.moduleBasePackage();
106+
107+
this.source = source;
90108
this.basePackage = basePackage;
109+
this.exclusions = exclusions;
110+
this.classes = basePackage.getClasses(exclusions);
91111
this.information = ApplicationModuleInformation.of(basePackage);
92112
this.namedInterfaces = isOpen()
93113
? NamedInterfaces.forOpen(basePackage)
94114
: NamedInterfaces.discoverNamedInterfaces(basePackage);
95-
this.useFullyQualifiedModuleNames = useFullyQualifiedModuleNames;
96115

97-
this.springBeans = SingletonSupplier.of(() -> filterSpringBeans(basePackage));
98-
this.aggregateRoots = SingletonSupplier.of(() -> findAggregateRoots(basePackage));
116+
this.springBeans = SingletonSupplier.of(() -> filterSpringBeans(classes));
117+
this.aggregateRoots = SingletonSupplier.of(() -> findAggregateRoots(classes));
99118
this.valueTypes = SingletonSupplier
100119
.of(() -> findArchitecturallyEvidentType(ArchitecturallyEvidentType::isValueObject));
101120
this.publishedEvents = SingletonSupplier.of(() -> findPublishedEvents());
@@ -125,7 +144,7 @@ public NamedInterfaces getNamedInterfaces() {
125144
* @return will never be {@literal null} or empty.
126145
*/
127146
public String getName() {
128-
return useFullyQualifiedModuleNames ? basePackage.getName() : basePackage.getLocalName();
147+
return source.moduleName();
129148
}
130149

131150
/**
@@ -274,10 +293,20 @@ public ArchitecturallyEvidentType getArchitecturallyEvidentType(Class<?> type) {
274293
FormatableType.of(type).getAbbreviatedFullName(this), getName())));
275294
}
276295

296+
/**
297+
* Returns whether the current module contains the given type.
298+
*
299+
* @param type must not be {@literal null}.
300+
*/
277301
public boolean contains(JavaClass type) {
278-
return basePackage.contains(type);
302+
return classes.contains(type);
279303
}
280304

305+
/**
306+
* Returns whether the current module contains the given type.
307+
*
308+
* @param type can be {@literal null}.
309+
*/
281310
public boolean contains(@Nullable Class<?> type) {
282311
return type != null && getType(type.getName()).isPresent();
283312
}
@@ -292,7 +321,7 @@ public Optional<JavaClass> getType(String candidate) {
292321

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

295-
return basePackage.stream()
324+
return classes.stream()
296325
.filter(hasSimpleOrFullyQualifiedName(candidate))
297326
.findFirst();
298327
}
@@ -363,9 +392,28 @@ public String toString(@Nullable ApplicationModules modules) {
363392

364393
builder.append("\n");
365394

395+
if (modules != null) {
396+
modules.getParentOf(this).ifPresent(it -> {
397+
builder.append("> Parent module: ").append(it.getName()).append("\n");
398+
});
399+
}
400+
366401
builder.append("> Logical name: ").append(getName()).append('\n');
367402
builder.append("> Base package: ").append(basePackage.getName()).append('\n');
368403

404+
builder.append("> Excluded packages: ");
405+
406+
if (!exclusions.iterator().hasNext()) {
407+
builder.append("none").append('\n');
408+
} else {
409+
410+
builder.append('\n');
411+
412+
exclusions.stream().forEach(it -> {
413+
builder.append(" - ").append(it.getName()).append('\n');
414+
});
415+
}
416+
369417
if (namedInterfaces.hasExplicitInterfaces()) {
370418

371419
builder.append("> Named interfaces:\n");
@@ -475,6 +523,23 @@ boolean isOpen() {
475523
return information.isOpen();
476524
}
477525

526+
/**
527+
* Returns whether the given type is contained in any of the parent modules of the current one.
528+
*
529+
* @param type must not be {@literal null}.
530+
* @param modules must not be {@literal null}.
531+
* @since 1.3
532+
*/
533+
boolean containsTypeInAnyParent(JavaClass type, ApplicationModules modules) {
534+
535+
Assert.notNull(type, "Type must not be null!");
536+
Assert.notNull(modules, "ApplicationModules must not be null!");
537+
538+
return modules.getParentOf(this)
539+
.filter(it -> it.contains(type) || it.containsTypeInAnyParent(type, modules))
540+
.isPresent();
541+
}
542+
478543
/*
479544
* (non-Javadoc)
480545
* @see java.lang.Object#equals(java.lang.Object)
@@ -490,13 +555,13 @@ public boolean equals(Object obj) {
490555
return false;
491556
}
492557

493-
return Objects.equals(this.basePackage, that.basePackage) //
558+
return Objects.equals(this.source, that.source)
559+
&& Objects.equals(this.basePackage, that.basePackage) //
494560
&& Objects.equals(this.aggregateRoots, that.aggregateRoots) //
495561
&& Objects.equals(this.information, that.information) //
496562
&& Objects.equals(this.namedInterfaces, that.namedInterfaces) //
497563
&& Objects.equals(this.publishedEvents, that.publishedEvents) //
498564
&& Objects.equals(this.springBeans, that.springBeans) //
499-
&& Objects.equals(this.useFullyQualifiedModuleNames, that.useFullyQualifiedModuleNames) //
500565
&& Objects.equals(this.valueTypes, that.valueTypes);
501566
}
502567

@@ -506,16 +571,25 @@ public boolean equals(Object obj) {
506571
*/
507572
@Override
508573
public int hashCode() {
509-
return Objects.hash(basePackage, aggregateRoots, information, namedInterfaces, publishedEvents, springBeans,
510-
useFullyQualifiedModuleNames, valueTypes);
574+
return Objects.hash(source, basePackage, aggregateRoots, information, namedInterfaces, publishedEvents, springBeans,
575+
valueTypes);
576+
}
577+
578+
/*
579+
* (non-Javadoc)
580+
* @see java.lang.Comparable#compareTo(java.lang.Object)
581+
*/
582+
@Override
583+
public int compareTo(ApplicationModule o) {
584+
return getBasePackage().compareTo(o.getBasePackage());
511585
}
512586

513587
private List<EventType> findPublishedEvents() {
514588

515589
DescribedPredicate<JavaClass> isEvent = implement(JMoleculesTypes.DOMAIN_EVENT) //
516590
.or(isAnnotatedWith(JMoleculesTypes.AT_DOMAIN_EVENT));
517591

518-
return basePackage.that(isEvent).stream() //
592+
return classes.that(isEvent).stream() //
519593
.map(EventType::new).toList();
520594
}
521595

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

538612
private Stream<QualifiedDependency> getAllModuleDependencies(ApplicationModules modules) {
539613

540-
return basePackage.stream() //
614+
return classes.stream() //
541615
.flatMap(it -> getModuleDependenciesOf(it, modules));
542616
}
543617

@@ -590,7 +664,7 @@ private boolean isDependencyToOtherModule(JavaClass dependency, ApplicationModul
590664
return modules.contains(dependency) && !contains(dependency);
591665
}
592666

593-
private Classes findAggregateRoots(JavaPackage source) {
667+
private Classes findAggregateRoots(Classes source) {
594668

595669
return source.stream() //
596670
.map(it -> ArchitecturallyEvidentType.of(it, getSpringBeansInternal()))
@@ -599,11 +673,78 @@ private Classes findAggregateRoots(JavaPackage source) {
599673
.collect(Classes.toClasses());
600674
}
601675

676+
/**
677+
* Returns the current module's immediate parent module, if present.
678+
*
679+
* @param modules must not be {@literal null}.
680+
* @return will never be {@literal null}.
681+
* @since 1.3
682+
*/
683+
Optional<ApplicationModule> getParentModule(ApplicationModules modules) {
684+
685+
Assert.notNull(modules, "ApplicationModules must not be null!");
686+
687+
var byPackageDepth = comparing(ApplicationModule::getBasePackage, JavaPackage.reverse());
688+
689+
return modules.stream()
690+
.filter(it -> basePackage.isSubPackageOf(it.getBasePackage()))
691+
.sorted(byPackageDepth)
692+
.findFirst();
693+
}
694+
695+
/**
696+
* Returns the {@link ApplicationModule}s directly nested inside the current one.
697+
*
698+
* @param modules must not be {@literal null}.
699+
* @return will never be {@literal null}.
700+
* @since 1.3
701+
*/
702+
Collection<ApplicationModule> getDirectlyNestedModules(ApplicationModules modules) {
703+
704+
Assert.notNull(modules, "ApplicationModules must not be null!");
705+
706+
return doGetNestedModules(modules, false);
707+
}
708+
709+
/**
710+
* Returns all of the current {@link ApplicationModule}'s nested {@link ApplicationModule}s including ones contained
711+
* in nested modules in turn.
712+
*
713+
* @param modules must not be {@literal null}.
714+
* @return will never be {@literal null}.
715+
* @since 1.3
716+
*/
717+
Collection<ApplicationModule> getNestedModules(ApplicationModules modules) {
718+
719+
Assert.notNull(modules, "ApplicationModules must not be null!");
720+
721+
return doGetNestedModules(modules, true);
722+
}
723+
724+
/**
725+
* @return the classes
726+
*/
727+
Classes getClasses() {
728+
return classes;
729+
}
730+
602731
private String getQualifiedName(NamedInterface namedInterface) {
603732
return namedInterface.getQualifiedName(getName());
604733
}
605734

606-
private static Classes filterSpringBeans(JavaPackage source) {
735+
private Collection<ApplicationModule> doGetNestedModules(ApplicationModules modules, boolean recursive) {
736+
737+
var result = modules.stream()
738+
.filter(it -> it.getParentModule(modules).filter(this::equals).isPresent());
739+
740+
if (recursive) {
741+
result = result.flatMap(it -> Stream.concat(Stream.of(it), it.getNestedModules(modules).stream()));
742+
}
743+
744+
return result.toList();
745+
}
746+
747+
private static Classes filterSpringBeans(Classes source) {
607748

608749
Map<Boolean, List<JavaClass>> collect = source.that(isConfiguration()).stream() //
609750
.flatMap(it -> it.getMethods().stream()) //
@@ -632,7 +773,7 @@ private List<JavaClass> findArchitecturallyEvidentType(Predicate<Architecturally
632773

633774
var springBeansInternal = getSpringBeansInternal();
634775

635-
return basePackage.stream()
776+
return classes.stream()
636777
.map(it -> ArchitecturallyEvidentType.of(it, springBeansInternal))
637778
.filter(selector)
638779
.map(ArchitecturallyEvidentType::getType)
@@ -907,6 +1048,9 @@ static class QualifiedDependency {
9071048

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

1051+
private static final String INVALID_SUB_MODULE_REFERENCE = "Invalid sub-module reference from module '%s' to module '%s' (via %s -> %s)!";
1052+
private static final String INTERNAL_REFERENCE = "Module '%s' depends on non-exposed type %s within module '%s'!";
1053+
9101054
private final JavaClass source, target;
9111055
private final String description;
9121056
private final DependencyType type;
@@ -1043,15 +1187,32 @@ Violations isValidDependencyWithin(ApplicationModules modules) {
10431187
return violations;
10441188
}
10451189

1190+
if (originModule.containsTypeInAnyParent(target, modules)) {
1191+
return violations;
1192+
}
1193+
10461194
if (!targetModule.isExposed(target)) {
10471195

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

10511199
return violations.and(new Violation(violationText + lineSeparator() + description));
10521200
}
10531201

1054-
return violations;
1202+
// Parent child relationships
1203+
1204+
return targetModule.getParentModule(modules)
1205+
.filter(it -> !it.equals(originModule))
1206+
.map(__ -> {
1207+
1208+
var violationText = INVALID_SUB_MODULE_REFERENCE
1209+
.formatted(originModule.getName(), targetModule.getName(),
1210+
FormatableType.of(source).getAbbreviatedFullName(originModule),
1211+
FormatableType.of(target).getAbbreviatedFullName(targetModule));
1212+
1213+
return violations.and(new Violation(violationText));
1214+
})
1215+
.orElse(violations);
10551216
}
10561217

10571218
ApplicationModule getExistingModuleOf(JavaClass javaClass, ApplicationModules modules) {

0 commit comments

Comments
 (0)