From 7e084471395104f07e37589c74299cf0da805899 Mon Sep 17 00:00:00 2001 From: Oliver Drotbohm Date: Mon, 6 Jan 2025 19:12:36 +0100 Subject: [PATCH] GH-1014 - Pick up reference types for event listeners declared in annotations. We now detect event types a listener is interested in declared in annotations for inclusion the reference documentation. This allows for the rare case that the event listener method not actually declaring the event type as parameter as it might not be needed in the payload but only the fact that an event was published at all. --- spring-modulith-core/pom.xml | 6 +++ .../modulith/core/ApplicationModule.java | 15 +++++++- .../core/ArchitecturallyEvidentType.java | 37 ++++++++++++++++++- .../ArchitecturallyEvidentTypeUnitTest.java | 24 ++++++++++++ .../modulith/docs/Asciidoctor.java | 21 ++++++++--- 5 files changed, 96 insertions(+), 7 deletions(-) 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); }