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:
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user