From 79a80d2325bf2bc33b283d2dbc30560c544b49f4 Mon Sep 17 00:00:00 2001 From: Oliver Drotbohm Date: Mon, 6 Jan 2025 19:12:36 +0100 Subject: [PATCH] GH-1006 - 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 | 28 +++++++------- 5 files changed, 95 insertions(+), 15 deletions(-) diff --git a/spring-modulith-core/pom.xml b/spring-modulith-core/pom.xml index 7cc9c12c..040b45e6 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 b1cf3a89..4f742128 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 @@ -358,7 +358,6 @@ public class ApplicationModule implements Comparable { * module's named interfaces. * * @param type must not be {@literal null}. - * @return */ public boolean isExposed(JavaClass type) { @@ -367,6 +366,20 @@ public class ApplicationModule implements Comparable { 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 5d9b8626..5e6d1606 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 6b634e85..941b83d1 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 @@ -18,6 +18,7 @@ package org.springframework.modulith.docs; import static java.util.stream.Collectors.*; import static org.springframework.util.ClassUtils.*; +import java.util.Collection; import java.util.List; import java.util.Optional; import java.util.regex.Matcher; @@ -321,16 +322,13 @@ class Asciidoctor { private String renderReferenceMethod(ReferenceMethod it, int level) { var method = it.getMethod(); - Assert.isTrue(method.getRawParameterTypes().size() > 0, - () -> "Method %s must have at least one parameter!".formatted(method)); + var exposedReferenceTypes = it.getReferenceTypes().stream() + .filter(type -> modules.getModuleByType(type) + .map(module -> module.isExposed(type)) + .orElse(true)) + .toList(); - var parameterType = method.getRawParameterTypes().get(0); - - var typeExposed = modules.getModuleByType(parameterType) - .map(module -> module.isExposed(parameterType)) - .orElse(true); - - if (!typeExposed) { + if (exposedReferenceTypes.isEmpty()) { return ""; } @@ -338,12 +336,16 @@ class Asciidoctor { var indent = "*".repeat(level + 1); return docSource.flatMap(source -> source.getDocumentation(method)) - .map(doc -> "%s %s %s-- %s".formatted(indent, toInlineCode(parameterType), isAsync, doc)) - .orElseGet(() -> "%s %s %s".formatted(indent, toInlineCode(parameterType), isAsync)); + .map(doc -> "%s %s %s-- %s".formatted(indent, toInlineCode(exposedReferenceTypes), isAsync, doc)) + .orElseGet(() -> "%s %s %s".formatted(indent, toInlineCode(exposedReferenceTypes), isAsync)); } - private String toInlineCode(Stream types) { - return types.map(this::toInlineCode).collect(joining(", ")); + private String toInlineCode(Collection> types) { + + return types.stream() + .map(Class::getName) + .map(this::toInlineCode) + .collect(joining(", ")); } private static String toLink(String source, String href) {