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.
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -358,7 +358,6 @@ public class ApplicationModule implements Comparable<ApplicationModule> {
|
||||
* 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<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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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<JavaClass> types) {
|
||||
return types.map(this::toInlineCode).collect(joining(", "));
|
||||
private String toInlineCode(Collection<Class<?>> types) {
|
||||
|
||||
return types.stream()
|
||||
.map(Class::getName)
|
||||
.map(this::toInlineCode)
|
||||
.collect(joining(", "));
|
||||
}
|
||||
|
||||
private static String toLink(String source, String href) {
|
||||
|
||||
Reference in New Issue
Block a user