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.
This commit is contained in:
@@ -38,6 +38,7 @@ import java.util.stream.Stream;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.modulith.core.Types.JMoleculesTypes;
|
||||
import org.springframework.modulith.core.Types.JavaTypes;
|
||||
import org.springframework.modulith.core.Types.SpringTypes;
|
||||
import org.springframework.modulith.core.Violations.Violation;
|
||||
import org.springframework.util.Assert;
|
||||
@@ -163,19 +164,34 @@ public class ApplicationModule implements Comparable<ApplicationModule> {
|
||||
* @param modules must not be {@literal null}.
|
||||
* @param type must not be {@literal null}.
|
||||
* @return will never be {@literal null}.
|
||||
* @deprecated since 1.3. Use {@link #getDirectDependencies(ApplicationModules, DependencyType...)} instead.
|
||||
*/
|
||||
@Deprecated
|
||||
public ApplicationModuleDependencies getDependencies(ApplicationModules modules, DependencyType... type) {
|
||||
return getDirectDependencies(modules, type);
|
||||
}
|
||||
|
||||
Assert.notNull(modules, "ApplicationModules must not be null!");
|
||||
Assert.notNull(type, "DependencyTypes must not be null!");
|
||||
/**
|
||||
* Returns the direct {@link ApplicationModuleDependencies} of the current {@link ApplicationModule}.
|
||||
*
|
||||
* @param modules must not be {@literal null}.
|
||||
* @param type must not be {@literal null}.
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
public ApplicationModuleDependencies getDirectDependencies(ApplicationModules modules, DependencyType... type) {
|
||||
return getDependencies(modules, DependencyDepth.IMMEDIATE, type);
|
||||
}
|
||||
|
||||
var dependencies = getAllModuleDependencies(modules) //
|
||||
.filter(it -> type.length == 0 ? true : Arrays.stream(type).anyMatch(it::hasType)) //
|
||||
.distinct() //
|
||||
.<ApplicationModuleDependency> flatMap(it -> DefaultApplicationModuleDependency.of(it, modules)) //
|
||||
.toList();
|
||||
|
||||
return ApplicationModuleDependencies.of(dependencies, modules);
|
||||
/**
|
||||
* Returns the all {@link ApplicationModuleDependencies} (including transitive ones) of the current
|
||||
* {@link ApplicationModule}.
|
||||
*
|
||||
* @param modules must not be {@literal null}.
|
||||
* @param type must not be {@literal null}.
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
public ApplicationModuleDependencies getAllDependencies(ApplicationModules modules, DependencyType... type) {
|
||||
return getDependencies(modules, DependencyDepth.ALL, type);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -299,20 +315,20 @@ public class ApplicationModule implements Comparable<ApplicationModule> {
|
||||
* @param type must not be {@literal null}.
|
||||
*/
|
||||
public boolean contains(JavaClass type) {
|
||||
return classes.contains(type);
|
||||
return contains(type.getName());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the current module contains the given type.
|
||||
*
|
||||
* @param type can be {@literal null}.
|
||||
* @param type must not be {@literal null}.
|
||||
*/
|
||||
public boolean contains(@Nullable Class<?> type) {
|
||||
return type != null && getType(type.getName()).isPresent();
|
||||
public boolean contains(Class<?> type) {
|
||||
return contains(type.getName());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link JavaClass} for the given candidate simple of fully-qualified type name.
|
||||
* Returns the {@link JavaClass} for the given candidate simple or fully-qualified type name.
|
||||
*
|
||||
* @param candidate must not be {@literal null} or empty.
|
||||
* @return will never be {@literal null}.
|
||||
@@ -363,23 +379,17 @@ public class ApplicationModule implements Comparable<ApplicationModule> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the module has a base package with the given name.
|
||||
* Returns whether the given module contains a type with the given simple or fully qualified name.
|
||||
*
|
||||
* @param candidate must not be {@literal null} or empty.
|
||||
* @return whether the module has a base package with the given name.
|
||||
* @since 1.1
|
||||
* @param candidate must not be {@literal null}.
|
||||
* @since 1.3
|
||||
*/
|
||||
boolean hasBasePackage(String candidate) {
|
||||
return basePackage.getName().equals(candidate);
|
||||
}
|
||||
public boolean contains(String candidate) {
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.lang.Object#toString()
|
||||
*/
|
||||
@Override
|
||||
public String toString() {
|
||||
return toString(null);
|
||||
var candidatePackageName = PackageName.ofType(candidate);
|
||||
|
||||
return (candidatePackageName.isEmpty() || basePackage.getPackageName().contains(candidatePackageName))
|
||||
&& getType(candidate).isPresent();
|
||||
}
|
||||
|
||||
public String toString(@Nullable ApplicationModules modules) {
|
||||
@@ -450,6 +460,17 @@ public class ApplicationModule implements Comparable<ApplicationModule> {
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the module has a base package with the given name.
|
||||
*
|
||||
* @param candidate must not be {@literal null} or empty.
|
||||
* @return whether the module has a base package with the given name.
|
||||
* @since 1.1
|
||||
*/
|
||||
boolean hasBasePackage(String candidate) {
|
||||
return basePackage.getName().equals(candidate);
|
||||
}
|
||||
|
||||
Classes getSpringBeansInternal() {
|
||||
return springBeans.get();
|
||||
}
|
||||
@@ -482,19 +503,6 @@ public class ApplicationModule implements Comparable<ApplicationModule> {
|
||||
.collect(Collectors.collectingAndThen(Collectors.toList(), DeclaredDependencies::closed));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the given module contains a type with the given simple or fully qualified name.
|
||||
*
|
||||
* @param candidate must not be {@literal null} or empty.
|
||||
* @return
|
||||
*/
|
||||
boolean contains(String candidate) {
|
||||
|
||||
Assert.hasText(candidate, "Candidate must not be null or empty!");
|
||||
|
||||
return getType(candidate).isPresent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the {@link ApplicationModule} contains the package with the given name, which means the given
|
||||
* package is either the module's base package or a sub package of it.
|
||||
@@ -540,6 +548,26 @@ public class ApplicationModule implements Comparable<ApplicationModule> {
|
||||
.isPresent();
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.modulith.core.ApplicationModuleDependenciesAware#getDependencies(org.springframework.modulith.core.ApplicationModules, org.springframework.modulith.core.DependencyDepth, org.springframework.modulith.core.DependencyType[])
|
||||
*/
|
||||
public ApplicationModuleDependencies getDependencies(ApplicationModules modules, DependencyDepth depth,
|
||||
DependencyType... types) {
|
||||
|
||||
Assert.notNull(modules, "ApplicationModules must not be null!");
|
||||
Assert.notNull(depth, "DependencyDepth must not be null!");
|
||||
Assert.notNull(types, "DependencyTypes must not be null!");
|
||||
|
||||
return getAllModuleDependencies(modules) //
|
||||
.filter(it -> types.length == 0 ? true : Arrays.stream(types).anyMatch(it::hasType)) //
|
||||
.distinct() //
|
||||
.flatMap(it -> DefaultApplicationModuleDependency.of(it, modules)) //
|
||||
.distinct() //
|
||||
.flatMap(it -> resolveRecurseively(modules, it, depth, types)) //
|
||||
.collect(Collectors.collectingAndThen(Collectors.toList(), ApplicationModuleDependencies::of));
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.lang.Object#equals(java.lang.Object)
|
||||
@@ -575,6 +603,15 @@ public class ApplicationModule implements Comparable<ApplicationModule> {
|
||||
valueTypes);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.lang.Object#toString()
|
||||
*/
|
||||
@Override
|
||||
public String toString() {
|
||||
return toString(null);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.lang.Comparable#compareTo(java.lang.Object)
|
||||
@@ -646,33 +683,6 @@ public class ApplicationModule implements Comparable<ApplicationModule> {
|
||||
.flatMap(it -> it.map(Stream::of).orElseGet(Stream::empty));
|
||||
}
|
||||
|
||||
private Stream<QualifiedDependency> getModuleDependenciesOf(JavaClass type, ApplicationModules modules) {
|
||||
|
||||
var evidentType = ArchitecturallyEvidentType.of(type, getSpringBeansInternal());
|
||||
|
||||
var injections = QualifiedDependency.fromType(evidentType) //
|
||||
.filter(it -> isDependencyToOtherModule(it.getTarget(), modules)); //
|
||||
|
||||
var directDependencies = type.getDirectDependenciesFromSelf().stream() //
|
||||
.filter(it -> isDependencyToOtherModule(it.getTargetClass(), modules)) //
|
||||
.map(QualifiedDependency::new);
|
||||
|
||||
return Stream.concat(injections, directDependencies).distinct();
|
||||
}
|
||||
|
||||
private boolean isDependencyToOtherModule(JavaClass dependency, ApplicationModules modules) {
|
||||
return modules.contains(dependency) && !contains(dependency);
|
||||
}
|
||||
|
||||
private Classes findAggregateRoots(Classes source) {
|
||||
|
||||
return source.stream() //
|
||||
.map(it -> ArchitecturallyEvidentType.of(it, getSpringBeansInternal()))
|
||||
.filter(ArchitecturallyEvidentType::isAggregateRoot) //
|
||||
.map(ArchitecturallyEvidentType::getType) //
|
||||
.collect(Classes.toClasses());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current module's immediate parent module, if present.
|
||||
*
|
||||
@@ -744,6 +754,58 @@ public class ApplicationModule implements Comparable<ApplicationModule> {
|
||||
return result.toList();
|
||||
}
|
||||
|
||||
private List<JavaClass> findArchitecturallyEvidentType(Predicate<ArchitecturallyEvidentType> selector) {
|
||||
|
||||
var springBeansInternal = getSpringBeansInternal();
|
||||
|
||||
return classes.stream()
|
||||
.map(it -> ArchitecturallyEvidentType.of(it, springBeansInternal))
|
||||
.filter(selector)
|
||||
.map(ArchitecturallyEvidentType::getType)
|
||||
.toList();
|
||||
}
|
||||
|
||||
private Stream<QualifiedDependency> getModuleDependenciesOf(JavaClass type, ApplicationModules modules) {
|
||||
|
||||
var evidentType = ArchitecturallyEvidentType.of(type, getSpringBeansInternal());
|
||||
|
||||
var injections = QualifiedDependency.fromType(evidentType) //
|
||||
.filter(it -> isDependencyToOtherModule(it.getTarget(), modules)); //
|
||||
|
||||
var directDependencies = type.getDirectDependenciesFromSelf().stream() //
|
||||
.filter(it -> JavaTypes.IS_NOT_CORE_JAVA_TYPE.test(it.getTargetClass())) //
|
||||
.filter(it -> isDependencyToOtherModule(it.getTargetClass(), modules)) //
|
||||
.map(QualifiedDependency::new);
|
||||
|
||||
return Stream.concat(injections, directDependencies).distinct();
|
||||
}
|
||||
|
||||
private boolean isDependencyToOtherModule(JavaClass dependency, ApplicationModules modules) {
|
||||
return modules.contains(dependency) && !contains(dependency);
|
||||
}
|
||||
|
||||
private Classes findAggregateRoots(Classes source) {
|
||||
|
||||
return source.stream() //
|
||||
.map(it -> ArchitecturallyEvidentType.of(it, getSpringBeansInternal()))
|
||||
.filter(ArchitecturallyEvidentType::isAggregateRoot) //
|
||||
.map(ArchitecturallyEvidentType::getType) //
|
||||
.collect(Classes.toClasses());
|
||||
}
|
||||
|
||||
private Stream<ApplicationModuleDependency> resolveRecurseively(ApplicationModules modules,
|
||||
DefaultApplicationModuleDependency dependency, DependencyDepth depth, DependencyType... type) {
|
||||
|
||||
var tail = depth == DependencyDepth.ALL
|
||||
? dependency.getTargetModule()
|
||||
.getDependencies(modules, depth, type)
|
||||
.stream()
|
||||
.distinct()
|
||||
: Stream.<ApplicationModuleDependency> empty();
|
||||
|
||||
return Stream.concat(Stream.of(dependency), tail);
|
||||
}
|
||||
|
||||
private static Classes filterSpringBeans(Classes source) {
|
||||
|
||||
Map<Boolean, List<JavaClass>> collect = source.that(isConfiguration()).stream() //
|
||||
@@ -769,17 +831,6 @@ public class ApplicationModule implements Comparable<ApplicationModule> {
|
||||
return it -> it.getSimpleName().equals(candidate) || it.getFullName().equals(candidate);
|
||||
}
|
||||
|
||||
private List<JavaClass> findArchitecturallyEvidentType(Predicate<ArchitecturallyEvidentType> selector) {
|
||||
|
||||
var springBeansInternal = getSpringBeansInternal();
|
||||
|
||||
return classes.stream()
|
||||
.map(it -> ArchitecturallyEvidentType.of(it, springBeansInternal))
|
||||
.filter(selector)
|
||||
.map(ArchitecturallyEvidentType::getType)
|
||||
.toList();
|
||||
}
|
||||
|
||||
static class DeclaredDependency {
|
||||
|
||||
private static final String INVALID_EXPLICIT_MODULE_DEPENDENCY = "Invalid explicit module dependency in %s! No module found with name '%s'.";
|
||||
@@ -1084,21 +1135,31 @@ public class ApplicationModule implements Comparable<ApplicationModule> {
|
||||
DependencyType.forDependency(dependency));
|
||||
}
|
||||
|
||||
static QualifiedDependency fromCodeUnitParameter(JavaCodeUnit codeUnit, JavaClass parameter) {
|
||||
static Stream<QualifiedDependency> fromCodeUnitParameter(JavaCodeUnit codeUnit, JavaClass parameter) {
|
||||
|
||||
if (JavaTypes.IS_CORE_JAVA_TYPE.test(parameter)) {
|
||||
return Stream.empty();
|
||||
}
|
||||
|
||||
var description = createDescription(codeUnit, parameter, "parameter");
|
||||
var type = DependencyType.forCodeUnit(codeUnit) //
|
||||
.defaultOr(() -> DependencyType.forParameter(parameter));
|
||||
|
||||
return new QualifiedDependency(codeUnit.getOwner(), parameter, description, type);
|
||||
return Stream.of(new QualifiedDependency(codeUnit.getOwner(), parameter, description, type));
|
||||
}
|
||||
|
||||
static QualifiedDependency fromCodeUnitReturnType(JavaCodeUnit codeUnit) {
|
||||
static Stream<QualifiedDependency> fromCodeUnitReturnType(JavaCodeUnit codeUnit) {
|
||||
|
||||
var returnType = codeUnit.getRawReturnType();
|
||||
|
||||
if (JavaTypes.IS_CORE_JAVA_TYPE.test(returnType)) {
|
||||
return Stream.empty();
|
||||
}
|
||||
|
||||
var description = createDescription(codeUnit, codeUnit.getRawReturnType(), "return type");
|
||||
|
||||
return new QualifiedDependency(codeUnit.getOwner(), codeUnit.getRawReturnType(), description,
|
||||
DependencyType.DEFAULT);
|
||||
return Stream.of(new QualifiedDependency(codeUnit.getOwner(), codeUnit.getRawReturnType(), description,
|
||||
DependencyType.DEFAULT));
|
||||
}
|
||||
|
||||
static Stream<QualifiedDependency> fromType(ArchitecturallyEvidentType type) {
|
||||
@@ -1112,9 +1173,10 @@ public class ApplicationModule implements Comparable<ApplicationModule> {
|
||||
|
||||
var parameterDependencies = codeUnit.getRawParameterTypes()//
|
||||
.stream() //
|
||||
.map(it -> fromCodeUnitParameter(codeUnit, it));
|
||||
.filter(JavaTypes.IS_NOT_CORE_JAVA_TYPE) //
|
||||
.flatMap(it -> fromCodeUnitParameter(codeUnit, it));
|
||||
|
||||
var returnType = Stream.of(fromCodeUnitReturnType(codeUnit));
|
||||
var returnType = fromCodeUnitReturnType(codeUnit);
|
||||
|
||||
return Stream.concat(parameterDependencies, returnType);
|
||||
}
|
||||
@@ -1268,6 +1330,7 @@ public class ApplicationModule implements Comparable<ApplicationModule> {
|
||||
return constructors.stream() //
|
||||
.filter(it -> constructors.size() == 1 || isInjectionPoint(it)) //
|
||||
.flatMap(it -> it.getRawParameterTypes().stream() //
|
||||
.filter(Predicate.not(JavaTypes.IS_CORE_JAVA_TYPE))
|
||||
.map(parameter -> {
|
||||
return source.isInjectable() && !source.isConfigurationProperties()
|
||||
? new InjectionDependency(it, parameter)
|
||||
@@ -1279,12 +1342,17 @@ public class ApplicationModule implements Comparable<ApplicationModule> {
|
||||
private static Stream<QualifiedDependency> fromFieldsOf(JavaClass source) {
|
||||
|
||||
return source.getAllFields().stream() //
|
||||
.filter(it -> JavaTypes.IS_NOT_CORE_JAVA_TYPE.test(it.getRawType())) //
|
||||
.filter(QualifiedDependency::isInjectionPoint) //
|
||||
.map(field -> new InjectionDependency(field, field.getRawType()));
|
||||
}
|
||||
|
||||
private static Stream<QualifiedDependency> fromMethodsOf(JavaClass source) {
|
||||
|
||||
if (JavaTypes.IS_CORE_JAVA_TYPE.test(source)) {
|
||||
return Stream.empty();
|
||||
}
|
||||
|
||||
var methods = source.getAllMethods().stream() //
|
||||
.filter(it -> !it.getOwner().isEquivalentTo(Object.class)) //
|
||||
.collect(Collectors.toSet());
|
||||
@@ -1295,8 +1363,8 @@ public class ApplicationModule implements Comparable<ApplicationModule> {
|
||||
|
||||
var returnTypes = methods.stream() //
|
||||
.filter(it -> !it.getRawReturnType().isPrimitive()) //
|
||||
.filter(it -> !it.getRawReturnType().getPackageName().startsWith("java")) //
|
||||
.map(it -> fromCodeUnitReturnType(it));
|
||||
.filter(it -> JavaTypes.IS_NOT_CORE_JAVA_TYPE.test(it.getRawReturnType())) //
|
||||
.flatMap(it -> fromCodeUnitReturnType(it));
|
||||
|
||||
var injectionMethods = methods.stream() //
|
||||
.filter(QualifiedDependency::isInjectionPoint) //
|
||||
@@ -1304,12 +1372,14 @@ public class ApplicationModule implements Comparable<ApplicationModule> {
|
||||
|
||||
var methodInjections = injectionMethods.stream() //
|
||||
.flatMap(it -> it.getRawParameterTypes().stream() //
|
||||
.filter(JavaTypes.IS_NOT_CORE_JAVA_TYPE) //
|
||||
.map(parameter -> new InjectionDependency(it, parameter)));
|
||||
|
||||
var otherMethods = methods.stream() //
|
||||
.filter(it -> !injectionMethods.contains(it)) //
|
||||
.flatMap(it -> it.getRawParameterTypes().stream() //
|
||||
.map(parameter -> fromCodeUnitParameter(it, parameter)));
|
||||
.filter(JavaTypes.IS_NOT_CORE_JAVA_TYPE) //
|
||||
.flatMap(parameter -> fromCodeUnitParameter(it, parameter)));
|
||||
|
||||
return Stream.concat(Stream.concat(methodInjections, otherMethods), returnTypes);
|
||||
}
|
||||
@@ -1427,7 +1497,8 @@ public class ApplicationModule implements Comparable<ApplicationModule> {
|
||||
}
|
||||
}
|
||||
|
||||
private static class DefaultApplicationModuleDependency implements ApplicationModuleDependency {
|
||||
private static class DefaultApplicationModuleDependency
|
||||
implements ApplicationModuleDependency {
|
||||
|
||||
private final QualifiedDependency dependency;
|
||||
private final ApplicationModule target;
|
||||
@@ -1455,7 +1526,7 @@ public class ApplicationModule implements Comparable<ApplicationModule> {
|
||||
* @param dependency must not be {@literal null}.
|
||||
* @param modules must not be {@literal null}.
|
||||
*/
|
||||
static Stream<ApplicationModuleDependency> of(QualifiedDependency dependency, ApplicationModules modules) {
|
||||
static Stream<DefaultApplicationModuleDependency> of(QualifiedDependency dependency, ApplicationModules modules) {
|
||||
|
||||
return modules.getModuleByType(dependency.getTarget()).stream()
|
||||
.map(it -> new DefaultApplicationModuleDependency(dependency, it));
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
package org.springframework.modulith.core;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
@@ -30,22 +31,23 @@ import org.springframework.util.Assert;
|
||||
public class ApplicationModuleDependencies {
|
||||
|
||||
private final List<ApplicationModuleDependency> dependencies;
|
||||
private final ApplicationModules modules;
|
||||
private final Collection<ApplicationModule> modules;
|
||||
|
||||
/**
|
||||
* Creates a new {@link ApplicationModuleDependencies} for the given {@link List} of
|
||||
* {@link ApplicationModuleDependency} and {@link ApplicationModules}.
|
||||
*
|
||||
* @param dependencies must not be {@literal null}.
|
||||
* @param modules must not be {@literal null}.
|
||||
*/
|
||||
private ApplicationModuleDependencies(List<ApplicationModuleDependency> dependencies, ApplicationModules modules) {
|
||||
private ApplicationModuleDependencies(List<ApplicationModuleDependency> dependencies) {
|
||||
|
||||
Assert.notNull(dependencies, "ApplicationModuleDependency list must not be null!");
|
||||
Assert.notNull(modules, "ApplicationModules must not be null!");
|
||||
|
||||
this.dependencies = dependencies;
|
||||
this.modules = modules;
|
||||
this.modules = dependencies.stream()
|
||||
.map(ApplicationModuleDependency::getTargetModule)
|
||||
.distinct()
|
||||
.toList();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -56,10 +58,8 @@ public class ApplicationModuleDependencies {
|
||||
* @param modules must not be {@literal null}.
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
static ApplicationModuleDependencies of(List<ApplicationModuleDependency> dependencies,
|
||||
ApplicationModules modules) {
|
||||
|
||||
return new ApplicationModuleDependencies(dependencies, modules);
|
||||
static ApplicationModuleDependencies of(List<ApplicationModuleDependency> dependencies) {
|
||||
return new ApplicationModuleDependencies(dependencies);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -72,9 +72,7 @@ public class ApplicationModuleDependencies {
|
||||
|
||||
Assert.notNull(module, "ApplicationModule must not be null!");
|
||||
|
||||
return dependencies.stream()
|
||||
.map(ApplicationModuleDependency::getTargetModule)
|
||||
.anyMatch(module::equals);
|
||||
return modules.contains(module);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -87,8 +85,7 @@ public class ApplicationModuleDependencies {
|
||||
|
||||
Assert.hasText(name, "Module name must not be null or empty!");
|
||||
|
||||
return dependencies.stream()
|
||||
.map(ApplicationModuleDependency::getTargetModule)
|
||||
return modules.stream()
|
||||
.map(ApplicationModule::getName)
|
||||
.anyMatch(name::equals);
|
||||
}
|
||||
@@ -119,13 +116,22 @@ public class ApplicationModuleDependencies {
|
||||
.filter(it -> seenTargets.add(extractor.apply(it)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new {@link ApplicationModuleDependencies} instance containing only the dependencies of the given
|
||||
* {@link DependencyType}.
|
||||
*
|
||||
* @param type must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
public ApplicationModuleDependencies withType(DependencyType type) {
|
||||
|
||||
Assert.notNull(type, "DependencyType must not be null!");
|
||||
|
||||
var filtered = dependencies.stream()
|
||||
.filter(it -> it.getDependencyType().equals(type))
|
||||
.toList();
|
||||
|
||||
return ApplicationModuleDependencies.of(filtered, modules);
|
||||
return ApplicationModuleDependencies.of(filtered);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -134,6 +140,45 @@ public class ApplicationModuleDependencies {
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
public boolean isEmpty() {
|
||||
return dependencies.isEmpty();
|
||||
return modules.isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the dependencies contain the type with the given fully-qualified name.
|
||||
*
|
||||
* @param type must not be {@literal null} or empty.
|
||||
* @return
|
||||
* @since 1.3
|
||||
*/
|
||||
public boolean contains(String type) {
|
||||
|
||||
Assert.hasText(type, "Type must not be null or empty!");
|
||||
|
||||
return uniqueModules().anyMatch(it -> it.contains(type));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all unique {@link ApplicationModule}s involved in the dependencies.
|
||||
*
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
public Stream<ApplicationModule> uniqueModules() {
|
||||
return modules.stream();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link ApplicationModule} containing the given type.
|
||||
*
|
||||
* @param name must not be {@literal null} or empty.
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
public ApplicationModule getModuleByType(String name) {
|
||||
|
||||
Assert.hasText(name, "Name must not be null or empty!");
|
||||
|
||||
return uniqueModules()
|
||||
.filter(it -> it.contains(name))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -778,8 +778,7 @@ public class ApplicationModules implements Iterable<ApplicationModule> {
|
||||
|
||||
graph.addVertex(project);
|
||||
|
||||
project.getDependencies(modules).stream() //
|
||||
.map(ApplicationModuleDependency::getTargetModule) //
|
||||
project.getDirectDependencies(modules).uniqueModules() //
|
||||
.forEach(dependency -> {
|
||||
graph.addVertex(dependency);
|
||||
graph.addEdge(project, dependency);
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
package org.springframework.modulith.core;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
/**
|
||||
* 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> {
|
||||
/**
|
||||
* Creates a new {@link PackageName} with the given name.
|
||||
*
|
||||
* @param name must not be {@literal null} or empty.
|
||||
* @param name must not be {@literal null}.
|
||||
*/
|
||||
public PackageName(String name) {
|
||||
|
||||
Assert.hasText(name, "Name must not be null or empty!");
|
||||
Assert.notNull(name, "Name must not be null!");
|
||||
|
||||
this.name = name;
|
||||
this.segments = name.split("\\.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link PackageName} for the given fully-qualified type name.
|
||||
*
|
||||
* @param fullyQualifiedName must not be {@literal null} or empty.
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
public static PackageName ofType(String fullyQualifiedName) {
|
||||
|
||||
Assert.notNull(fullyQualifiedName, "Type name must not be null!");
|
||||
|
||||
return new PackageName(ClassUtils.getPackageName(fullyQualifiedName));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the length of the package name.
|
||||
*
|
||||
@@ -118,6 +132,19 @@ class PackageName implements Comparable<PackageName> {
|
||||
return reference.name.startsWith(name + ".");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the package name contains the given one, i.e. if the given one either is the current one or a
|
||||
* sub-package of it.
|
||||
*
|
||||
* @param reference must not be {@literal null}.
|
||||
*/
|
||||
boolean contains(PackageName reference) {
|
||||
|
||||
Assert.notNull(reference, "Reference package name must not be null!");
|
||||
|
||||
return this.equals(reference) || reference.isSubPackageOf(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the current {@link PackageName} is the name of a sub-package with the given name.
|
||||
*
|
||||
@@ -131,6 +158,10 @@ class PackageName implements Comparable<PackageName> {
|
||||
return name.startsWith(reference.name + ".");
|
||||
}
|
||||
|
||||
boolean isEmpty() {
|
||||
return length() == 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.lang.Comparable#compareTo(java.lang.Object)
|
||||
|
||||
@@ -19,6 +19,7 @@ import static com.tngtech.archunit.core.domain.JavaClass.Predicates.*;
|
||||
import static org.springframework.modulith.core.SyntacticSugar.*;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.modulith.PackageInfo;
|
||||
@@ -79,6 +80,14 @@ class Types {
|
||||
}
|
||||
}
|
||||
|
||||
static class JavaTypes {
|
||||
|
||||
static Predicate<JavaClass> IS_CORE_JAVA_TYPE = it -> it.getName().startsWith("java.")
|
||||
|| it.getName().startsWith("javax.");
|
||||
|
||||
static Predicate<JavaClass> IS_NOT_CORE_JAVA_TYPE = Predicate.not(IS_CORE_JAVA_TYPE);
|
||||
}
|
||||
|
||||
static class JavaXTypes {
|
||||
|
||||
private static final String BASE_PACKAGE = "jakarta";
|
||||
|
||||
@@ -138,7 +138,7 @@ public class ApplicationModulesExporter {
|
||||
json.put("namedInterfaces", toNamedInterfaces(module.getNamedInterfaces()));
|
||||
}
|
||||
|
||||
json.put("dependencies", module.getDependencies(modules).stream() //
|
||||
json.put("dependencies", module.getDirectDependencies(modules).stream() //
|
||||
.collect(Collectors.groupingBy(ApplicationModuleDependency::getTargetModule, MAPPER))
|
||||
.entrySet() //
|
||||
.stream() //
|
||||
|
||||
@@ -44,4 +44,15 @@ class PackageNameUnitTests {
|
||||
.map(it -> it.getLocalName("com")))
|
||||
.containsExactly("acme.b", "acme.a.second", "acme.a.first.one", "acme.a.first", "acme.a", "acme");
|
||||
}
|
||||
|
||||
@Test // GH-802
|
||||
void caculatesNestingCorrectly() {
|
||||
|
||||
var comAcme = new PackageName("com.acme");
|
||||
var comAcmeA = new PackageName("com.acme.a");
|
||||
|
||||
assertThat(comAcme.contains(comAcme)).isTrue();
|
||||
assertThat(comAcme.contains(comAcmeA)).isTrue();
|
||||
assertThat(comAcmeA.contains(comAcme)).isFalse();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -359,7 +359,7 @@ class Asciidoctor {
|
||||
*/
|
||||
public String renderBeanReferences(ApplicationModule module) {
|
||||
|
||||
var bullets = module.getDependencies(modules, DependencyType.USES_COMPONENT)
|
||||
var bullets = module.getDirectDependencies(modules, DependencyType.USES_COMPONENT)
|
||||
.uniqueStream(ApplicationModuleDependency::getTargetType)
|
||||
.map(it -> "%s (in %s)".formatted(toInlineCode(it.getTargetType()), it.getTargetModule().getDisplayName()))
|
||||
.map(this::toBulletPoint)
|
||||
|
||||
@@ -36,7 +36,6 @@ import java.util.stream.Stream;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.modulith.core.ApplicationModule;
|
||||
import org.springframework.modulith.core.ApplicationModuleDependency;
|
||||
import org.springframework.modulith.core.ApplicationModules;
|
||||
import org.springframework.modulith.core.DependencyDepth;
|
||||
import org.springframework.modulith.core.DependencyType;
|
||||
@@ -442,8 +441,7 @@ public class Documenter {
|
||||
|
||||
DEPENDENCY_DESCRIPTIONS.entrySet().stream().forEach(entry -> {
|
||||
|
||||
module.getDependencies(modules, entry.getKey()).stream() //
|
||||
.map(ApplicationModuleDependency::getTargetModule) //
|
||||
module.getDirectDependencies(modules, entry.getKey()).uniqueModules() //
|
||||
.map(it -> getComponents(options).get(it)) //
|
||||
.map(it -> component.uses(it, entry.getValue())) //
|
||||
.filter(it -> it != null) //
|
||||
@@ -475,8 +473,7 @@ public class Documenter {
|
||||
Supplier<Stream<ApplicationModule>> bootstrapDependencies = () -> module.getBootstrapDependencies(modules,
|
||||
options.dependencyDepth);
|
||||
Supplier<Stream<ApplicationModule>> otherDependencies = () -> options.getDependencyTypes()
|
||||
.flatMap(it -> module.getDependencies(modules, it).stream()
|
||||
.map(ApplicationModuleDependency::getTargetModule));
|
||||
.flatMap(it -> module.getDirectDependencies(modules, it).uniqueModules());
|
||||
|
||||
Supplier<Stream<ApplicationModule>> dependencies = () -> Stream.concat(bootstrapDependencies.get(),
|
||||
otherDependencies.get());
|
||||
|
||||
@@ -110,10 +110,10 @@ class ModulithTest {
|
||||
|
||||
assertThat(modules.getModuleByName("moduleD")).hasValueSatisfying(it -> {
|
||||
|
||||
assertThat(it.getDependencies(modules, DependencyType.DEFAULT))
|
||||
assertThat(it.getDirectDependencies(modules, DependencyType.DEFAULT))
|
||||
.matches(inner -> inner.containsModuleNamed("moduleC"));
|
||||
|
||||
assertThat(it.getDependencies(modules, DependencyType.USES_COMPONENT))
|
||||
assertThat(it.getDirectDependencies(modules, DependencyType.USES_COMPONENT))
|
||||
.matches(ApplicationModuleDependencies::isEmpty);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -127,7 +127,7 @@ class ApplicationModulesIntegrationTest {
|
||||
ApplicationModule moduleA = modules.getModuleByName("moduleA").orElseThrow(IllegalStateException::new);
|
||||
|
||||
assertThat(module).hasValueSatisfying(it -> {
|
||||
assertThat(it.getDependencies(modules, DependencyType.EVENT_LISTENER).contains(moduleA)).isTrue();
|
||||
assertThat(it.getDirectDependencies(modules, DependencyType.EVENT_LISTENER).contains(moduleA)).isTrue();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -261,6 +261,17 @@ class ApplicationModulesIntegrationTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test // GH-802
|
||||
void detectsFullDependencyChain() {
|
||||
|
||||
assertThat(modules.getModuleByName("moduleC")).hasValueSatisfying(it -> {
|
||||
|
||||
assertThat(it.getAllDependencies(modules).uniqueModules())
|
||||
.extracting(ApplicationModule::getName)
|
||||
.containsExactlyInAnyOrder("moduleB", "moduleA");
|
||||
});
|
||||
}
|
||||
|
||||
private static void verifyNamedInterfaces(NamedInterfaces interfaces, String name, Class<?>... types) {
|
||||
|
||||
Stream.of(types).forEach(type -> {
|
||||
|
||||
Reference in New Issue
Block a user