diff --git a/spring-modulith-core/pom.xml b/spring-modulith-core/pom.xml
index 9de659ab..4abd2bb4 100644
--- a/spring-modulith-core/pom.xml
+++ b/spring-modulith-core/pom.xml
@@ -29,6 +29,13 @@
${archunit.version}
+
+ org.jgrapht
+ jgrapht-core
+ 1.4.0
+ true
+
+
org.springframework
spring-core
diff --git a/spring-modulith-core/src/main/java/org/springframework/modulith/model/ApplicationModule.java b/spring-modulith-core/src/main/java/org/springframework/modulith/model/ApplicationModule.java
index a5a193c5..ca64556e 100644
--- a/spring-modulith-core/src/main/java/org/springframework/modulith/model/ApplicationModule.java
+++ b/spring-modulith-core/src/main/java/org/springframework/modulith/model/ApplicationModule.java
@@ -684,7 +684,7 @@ public class ApplicationModule {
*/
@Override
public String toString() {
- return type.format(FormatableJavaClass.of(source), FormatableJavaClass.of(target));
+ return type.format(FormatableType.of(source), FormatableType.of(target));
}
static QualifiedDependency fromCodeUnitParameter(JavaCodeUnit codeUnit, JavaClass parameter) {
diff --git a/spring-modulith-core/src/main/java/org/springframework/modulith/model/ApplicationModules.java b/spring-modulith-core/src/main/java/org/springframework/modulith/model/ApplicationModules.java
index 3863f188..af7eceed 100644
--- a/spring-modulith-core/src/main/java/org/springframework/modulith/model/ApplicationModules.java
+++ b/spring-modulith-core/src/main/java/org/springframework/modulith/model/ApplicationModules.java
@@ -29,13 +29,23 @@ import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
+import java.util.stream.StreamSupport;
+import org.jgrapht.Graph;
+import org.jgrapht.graph.DefaultDirectedGraph;
+import org.jgrapht.graph.DefaultEdge;
+import org.jgrapht.traverse.TopologicalOrderIterator;
import org.jmolecules.archunit.JMoleculesDddRules;
+import org.springframework.core.Ordered;
+import org.springframework.core.annotation.AnnotationAwareOrderComparator;
+import org.springframework.core.annotation.Order;
import org.springframework.core.io.support.SpringFactoriesLoader;
+import org.springframework.lang.Nullable;
import org.springframework.modulith.Modulith;
import org.springframework.modulith.Modulithic;
import org.springframework.modulith.model.Types.JMoleculesTypes;
import org.springframework.util.Assert;
+import org.springframework.util.ClassUtils;
import com.tngtech.archunit.base.DescribedPredicate;
import com.tngtech.archunit.core.domain.JavaClass;
@@ -56,6 +66,8 @@ public class ApplicationModules implements Iterable {
private static final Map CACHE = new HashMap<>();
private static final ApplicationModuleDetectionStrategy DETECTION_STRATEGY;
private static final ImportOption IMPORT_OPTION = new ImportOption.DoNotIncludeTests();
+ private static final boolean JGRAPHT_PRESENT = ClassUtils.isPresent("org.jgrapht.Graph",
+ ApplicationModules.class.getClassLoader());
static {
@@ -79,6 +91,7 @@ public class ApplicationModules implements Iterable {
private final JavaClasses allClasses;
private final List rootPackages;
private final @With(AccessLevel.PRIVATE) @Getter Set sharedModules;
+ private final List orderedNames;
private boolean verified;
@@ -104,6 +117,10 @@ public class ApplicationModules implements Iterable {
.toList();
this.sharedModules = Collections.emptySet();
+
+ this.orderedNames = JGRAPHT_PRESENT //
+ ? TopologicalSorter.topologicallySortModules(this) //
+ : modules.values().stream().map(ApplicationModule::getName).toList();
}
/**
@@ -272,6 +289,16 @@ public class ApplicationModules implements Iterable {
.findFirst();
}
+ /**
+ * Returns the {@link ApplicationModule} containing the given type.
+ *
+ * @param candidate must not be {@literal null}.
+ * @return will never be {@literal null}.
+ */
+ public Optional getModuleByType(Class> candidate) {
+ return getModuleByType(candidate.getName());
+ }
+
public Optional getModuleForPackage(String name) {
return modules.values().stream() //
@@ -328,23 +355,13 @@ public class ApplicationModules implements Iterable {
.reduce(violations, Violations::and);
}
- private FailureReport assertNoCyclesFor(JavaPackage rootPackage) {
-
- EvaluationResult result = SlicesRuleDefinition.slices() //
- .matching(rootPackage.getName().concat(".(*)..")) //
- .should().beFreeOfCycles() //
- .evaluate(allClasses.that(resideInAPackage(rootPackage.getName().concat(".."))));
-
- return result.getFailureReport();
- }
-
/**
* Returns all {@link ApplicationModule}s.
*
* @return will never be {@literal null}.
*/
public Stream stream() {
- return modules.values().stream();
+ return StreamSupport.stream(this.spliterator(), false);
}
/**
@@ -356,13 +373,71 @@ public class ApplicationModules implements Iterable {
return metadata.getSystemName();
}
+ /**
+ * Returns a {@link Comparator} that will sort objects based on their types' application module. In other words,
+ * objects of types in more fundamental modules will be ordered before ones residing in downstream modules. For
+ * example, if module A depends on B, objects of types residing in B will be ordered before ones in A. For objects
+ * residing in the same module, standard Spring-based ordering (via {@link Order} or {@link Ordered}) will be applied.
+ *
+ * @return will never be {@literal null}.
+ */
+ public Comparator
+
+
+ org.jgrapht
+ jgrapht-core
+ 1.4.0
+ test
+
diff --git a/spring-modulith-integration-test/src/test/java/org/springframework/modulith/model/ModulesIntegrationTest.java b/spring-modulith-integration-test/src/test/java/org/springframework/modulith/model/ApplicationModulesIntegrationTest.java
similarity index 88%
rename from spring-modulith-integration-test/src/test/java/org/springframework/modulith/model/ModulesIntegrationTest.java
rename to spring-modulith-integration-test/src/test/java/org/springframework/modulith/model/ApplicationModulesIntegrationTest.java
index 6510946f..cc989503 100644
--- a/spring-modulith-integration-test/src/test/java/org/springframework/modulith/model/ModulesIntegrationTest.java
+++ b/spring-modulith-integration-test/src/test/java/org/springframework/modulith/model/ApplicationModulesIntegrationTest.java
@@ -17,9 +17,9 @@ package org.springframework.modulith.model;
import static org.assertj.core.api.Assertions.*;
+import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
-import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.junit.jupiter.api.Test;
@@ -27,13 +27,15 @@ import org.junit.jupiter.api.Test;
import com.acme.myproject.Application;
import com.acme.myproject.complex.internal.FirstTypeBasedPort;
import com.acme.myproject.complex.internal.SecondTypeBasePort;
+import com.acme.myproject.moduleA.ServiceComponentA;
import com.acme.myproject.moduleA.SomeConfigurationA.SomeAtBeanComponentA;
+import com.acme.myproject.moduleB.ServiceComponentB;
/**
* @author Oliver Drotbohm
* @author Peter Gafert
*/
-class ModulesIntegrationTest {
+class ApplicationModulesIntegrationTest {
ApplicationModules modules = ApplicationModules.of(Application.class);
@@ -150,6 +152,18 @@ class ModulesIntegrationTest {
modules.stream().map(ApplicationModule::getName).toList());
}
+ @Test // #102
+ void ordersTypesByModuleDependencies() {
+
+ // Non-module type first, B depending on A
+ var source = new ArrayList<>(List.of(String.class, ServiceComponentB.class, ServiceComponentA.class));
+
+ source.sort(ApplicationModules.of(Application.class).getComparator());
+
+ // Expect A before B before non-module type.
+ assertThat(source).containsExactly(ServiceComponentA.class, ServiceComponentB.class, String.class);
+ }
+
private static void verifyNamedInterfaces(NamedInterfaces interfaces, String name, Class>... types) {
Stream.of(types).forEach(type -> {
diff --git a/spring-modulith-observability/src/main/java/org/springframework/modulith/observability/DefaultObservedModule.java b/spring-modulith-observability/src/main/java/org/springframework/modulith/observability/DefaultObservedModule.java
index 096f4c73..c8cf4d9d 100644
--- a/spring-modulith-observability/src/main/java/org/springframework/modulith/observability/DefaultObservedModule.java
+++ b/spring-modulith-observability/src/main/java/org/springframework/modulith/observability/DefaultObservedModule.java
@@ -23,7 +23,7 @@ import java.util.Arrays;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.aop.ProxyMethodInvocation;
import org.springframework.aop.framework.Advised;
-import org.springframework.modulith.model.FormatableJavaClass;
+import org.springframework.modulith.model.FormatableType;
import org.springframework.modulith.model.ApplicationModule;
import org.springframework.modulith.model.ApplicationModules;
import org.springframework.modulith.model.SpringBean;
@@ -132,8 +132,8 @@ class DefaultObservedModule implements ObservedModule {
private static String toString(Class> type, Method method, ApplicationModule module) {
String typeName = module.getType(type.getName())
- .map(FormatableJavaClass::of)
- .map(FormatableJavaClass::getAbbreviatedFullName)
+ .map(FormatableType::of)
+ .map(FormatableType::getAbbreviatedFullName)
.orElseGet(() -> type.getName());
return String.format("%s.%s(…)", typeName, method.getName());
diff --git a/spring-modulith-test/src/main/java/org/springframework/modulith/test/ModuleTestExecution.java b/spring-modulith-test/src/main/java/org/springframework/modulith/test/ModuleTestExecution.java
index 290c30c5..b83840e8 100644
--- a/spring-modulith-test/src/main/java/org/springframework/modulith/test/ModuleTestExecution.java
+++ b/spring-modulith-test/src/main/java/org/springframework/modulith/test/ModuleTestExecution.java
@@ -126,7 +126,7 @@ public class ModuleTestExecution implements Iterable {
|| basePackages.get().stream().anyMatch(it -> it.contains(className));
if (result) {
- LOG.debug("Including class {}.", className);
+ LOG.trace("Including class {}.", className);
}
return !result;