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); }