diff --git a/spring-modulith-core/pom.xml b/spring-modulith-core/pom.xml
index 588c0816..ba9ce291 100644
--- a/spring-modulith-core/pom.xml
+++ b/spring-modulith-core/pom.xml
@@ -79,6 +79,12 @@
test
+
+ org.springframework
+ spring-tx
+ test
+
+
org.springframework.boot
spring-boot-starter-test
diff --git a/spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModule.java b/spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModule.java
index 8294ec6b..c0840dff 100644
--- a/spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModule.java
+++ b/spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModule.java
@@ -302,7 +302,6 @@ public class ApplicationModule {
* module's named interfaces.
*
* @param type must not be {@literal null}.
- * @return
*/
public boolean isExposed(JavaClass type) {
@@ -311,6 +310,20 @@ public class ApplicationModule {
return namedInterfaces.stream().anyMatch(it -> it.contains(type));
}
+ /**
+ * Returns whether the given {@link JavaClass} is exposed by the current module, i.e. whether it's part of any of the
+ * module's named interfaces.
+ *
+ * @param type must not be {@literal null}.
+ * @since 1.2.8, 1.3.2
+ */
+ public boolean isExposed(Class> type) {
+
+ Assert.notNull(type, "Type must not be null!");
+
+ return namedInterfaces.stream().anyMatch(it -> it.contains(type));
+ }
+
public void verifyDependencies(ApplicationModules modules) {
detectDependencies(modules).throwIfPresent();
}
diff --git a/spring-modulith-core/src/main/java/org/springframework/modulith/core/ArchitecturallyEvidentType.java b/spring-modulith-core/src/main/java/org/springframework/modulith/core/ArchitecturallyEvidentType.java
index 3325e049..0fde9f84 100644
--- a/spring-modulith-core/src/main/java/org/springframework/modulith/core/ArchitecturallyEvidentType.java
+++ b/spring-modulith-core/src/main/java/org/springframework/modulith/core/ArchitecturallyEvidentType.java
@@ -31,6 +31,7 @@ import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Stream;
+import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.core.support.AbstractRepositoryMetadata;
import org.springframework.modulith.core.Types.JMoleculesTypes;
@@ -153,8 +154,10 @@ public abstract class ArchitecturallyEvidentType {
* Returns other types that are interesting in the context of the current {@link ArchitecturallyEvidentType}. For
* example, for an event listener this might be the event types the particular listener is interested in.
*
- * @return
+ * @return will never be {@literal null}.
+ * @deprecated since 1.3.2, no replacement.
*/
+ @Deprecated(forRemoval = true)
public Stream getReferenceTypes() {
return Stream.empty();
}
@@ -652,5 +655,37 @@ public abstract class ArchitecturallyEvidentType {
.map(it -> it.get("phase"))
.map(Object::toString);
}
+
+ /**
+ * Returns all types referred to. Usually parameter types or types the method is interested in declared in
+ * annotations.
+ *
+ * @return will never be {@literal null}.
+ * @since 1.2.8, 1.3.2
+ */
+ public Collection> getReferenceTypes() {
+
+ var parameterTypes = method.getRawParameterTypes();
+
+ if (!parameterTypes.isEmpty()) {
+ return parameterTypes.stream()
+ .> map(JavaClass::reflect)
+ .toList();
+ }
+
+ var attributes = AnnotatedElementUtils.getMergedAnnotationAttributes(method.reflect(),
+ SpringTypes.AT_EVENT_LISTENER, false, false);
+
+ return List.of(attributes.getClassArray("classes"));
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see java.lang.Object#toString()
+ */
+ @Override
+ public String toString() {
+ return method.toString();
+ }
}
}
diff --git a/spring-modulith-core/src/test/java/org/springframework/modulith/core/ArchitecturallyEvidentTypeUnitTest.java b/spring-modulith-core/src/test/java/org/springframework/modulith/core/ArchitecturallyEvidentTypeUnitTest.java
index f7b5b26d..fec01a19 100644
--- a/spring-modulith-core/src/test/java/org/springframework/modulith/core/ArchitecturallyEvidentTypeUnitTest.java
+++ b/spring-modulith-core/src/test/java/org/springframework/modulith/core/ArchitecturallyEvidentTypeUnitTest.java
@@ -37,6 +37,7 @@ import org.springframework.data.repository.reactive.ReactiveCrudRepository;
import org.springframework.modulith.core.ArchitecturallyEvidentType.SpringAwareArchitecturallyEvidentType;
import org.springframework.modulith.core.ArchitecturallyEvidentType.SpringDataAwareArchitecturallyEvidentType;
import org.springframework.stereotype.Repository;
+import org.springframework.transaction.event.TransactionalEventListener;
import com.tngtech.archunit.core.domain.JavaClass;
@@ -196,6 +197,23 @@ class ArchitecturallyEvidentTypeUnitTest {
assertThat(ArchitecturallyEvidentType.of(type, classes).isRepository()).isTrue();
}
+ @Test
+ void detectsAnnotatedReferenceType() {
+
+ var type = classes.getRequiredClass(SomeEventListener.class);
+
+ var methods = ArchitecturallyEvidentType.of(type, classes).getReferenceMethods();
+
+ var annotatedMethod = methods.filter(it -> it.getMethod().getName().startsWith("annotated"));
+
+ assertThat(annotatedMethod).allSatisfy(it -> {
+
+ assertThat(it.getReferenceTypes())
+ .extracting(Class::getName)
+ .contains(String.class.getName());
+ });
+ }
+
private Iterator getTypesFor(Class>... types) {
return Stream.of(types) //
@@ -266,6 +284,12 @@ class ArchitecturallyEvidentTypeUnitTest {
@EventListener
void onOther(Object event) {}
+
+ @EventListener(classes = String.class)
+ void annotatedOn() {}
+
+ @TransactionalEventListener(classes = String.class)
+ void annotatedTxOn() {}
}
class ImplementingEventListener implements ApplicationListener {
diff --git a/spring-modulith-docs/src/main/java/org/springframework/modulith/docs/Asciidoctor.java b/spring-modulith-docs/src/main/java/org/springframework/modulith/docs/Asciidoctor.java
index 1d8514ee..47d2c403 100644
--- a/spring-modulith-docs/src/main/java/org/springframework/modulith/docs/Asciidoctor.java
+++ b/spring-modulith-docs/src/main/java/org/springframework/modulith/docs/Asciidoctor.java
@@ -17,6 +17,7 @@ package org.springframework.modulith.docs;
import static org.springframework.util.ClassUtils.*;
+import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.regex.Matcher;
@@ -295,15 +296,17 @@ class Asciidoctor {
return header + type.getReferenceMethods().map(it -> {
var method = it.getMethod();
- Assert.isTrue(method.getRawParameterTypes().size() > 0,
- () -> String.format("Method %s must have at least one parameter!", method));
+ var exposedReferenceTypes = it.getReferenceTypes().stream()
+ .filter(refType -> modules.getModuleByType(refType)
+ .map(module -> module.isExposed(refType))
+ .orElse(true))
+ .toList();
- var parameterType = method.getRawParameterTypes().get(0);
var isAsync = it.isAsync() ? "(async) " : "";
return docSource.flatMap(source -> source.getDocumentation(method))
- .map(doc -> String.format("** %s %s-- %s", toInlineCode(parameterType), isAsync, doc))
- .orElseGet(() -> String.format("** %s %s", toInlineCode(parameterType), isAsync));
+ .map(doc -> String.format("** %s %s-- %s", toInlineCode(exposedReferenceTypes), isAsync, doc))
+ .orElseGet(() -> String.format("** %s %s", toInlineCode(exposedReferenceTypes), isAsync));
}).collect(Collectors.joining("\n"));
}
@@ -317,6 +320,14 @@ class Asciidoctor {
.collect(Collectors.joining(", "));
}
+ private String toInlineCode(Collection> types) {
+
+ return types.stream()
+ .map(Class::getName)
+ .map(this::toInlineCode)
+ .collect(Collectors.joining(", "));
+ }
+
private static String toLink(String source, String href) {
return String.format("link:%s[%s]", href, source);
}