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.
This commit is contained in:
Oliver Drotbohm
2025-01-06 19:12:36 +01:00
parent 8192f1bb2d
commit 7e08447139
5 changed files with 96 additions and 7 deletions

View File

@@ -79,6 +79,12 @@
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>

View File

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

View File

@@ -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<JavaClass> 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<Class<?>> getReferenceTypes() {
var parameterTypes = method.getRawParameterTypes();
if (!parameterTypes.isEmpty()) {
return parameterTypes.stream()
.<Class<?>> 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();
}
}
}

View File

@@ -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<ArchitecturallyEvidentType> 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<ApplicationReadyEvent> {

View File

@@ -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<Class<?>> 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);
}