Skip to content

Commit fe84013

Browse files
committed
GH-1013 - 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.
1 parent cd6aa42 commit fe84013

File tree

5 files changed

+95
-15
lines changed

5 files changed

+95
-15
lines changed

spring-modulith-core/pom.xml

+6
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,12 @@
7979
<scope>test</scope>
8080
</dependency>
8181

82+
<dependency>
83+
<groupId>org.springframework</groupId>
84+
<artifactId>spring-tx</artifactId>
85+
<scope>test</scope>
86+
</dependency>
87+
8288
<dependency>
8389
<groupId>org.springframework.boot</groupId>
8490
<artifactId>spring-boot-starter-test</artifactId>

spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModule.java

+14-1
Original file line numberDiff line numberDiff line change
@@ -358,7 +358,6 @@ public Optional<JavaClass> getType(String candidate) {
358358
* module's named interfaces.
359359
*
360360
* @param type must not be {@literal null}.
361-
* @return
362361
*/
363362
public boolean isExposed(JavaClass type) {
364363

@@ -367,6 +366,20 @@ public boolean isExposed(JavaClass type) {
367366
return namedInterfaces.stream().anyMatch(it -> it.contains(type));
368367
}
369368

369+
/**
370+
* Returns whether the given {@link JavaClass} is exposed by the current module, i.e. whether it's part of any of the
371+
* module's named interfaces.
372+
*
373+
* @param type must not be {@literal null}.
374+
* @since 1.2.8, 1.3.2
375+
*/
376+
public boolean isExposed(Class<?> type) {
377+
378+
Assert.notNull(type, "Type must not be null!");
379+
380+
return namedInterfaces.stream().anyMatch(it -> it.contains(type));
381+
}
382+
370383
public void verifyDependencies(ApplicationModules modules) {
371384
detectDependencies(modules).throwIfPresent();
372385
}

spring-modulith-core/src/main/java/org/springframework/modulith/core/ArchitecturallyEvidentType.java

+36-1
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import java.util.function.Supplier;
3232
import java.util.stream.Stream;
3333

34+
import org.springframework.core.annotation.AnnotatedElementUtils;
3435
import org.springframework.data.repository.core.RepositoryMetadata;
3536
import org.springframework.data.repository.core.support.AbstractRepositoryMetadata;
3637
import org.springframework.modulith.core.Types.JMoleculesTypes;
@@ -153,8 +154,10 @@ public boolean isValueObject() {
153154
* Returns other types that are interesting in the context of the current {@link ArchitecturallyEvidentType}. For
154155
* example, for an event listener this might be the event types the particular listener is interested in.
155156
*
156-
* @return
157+
* @return will never be {@literal null}.
158+
* @deprecated since 1.3.2, no replacement.
157159
*/
160+
@Deprecated(forRemoval = true)
158161
public Stream<JavaClass> getReferenceTypes() {
159162
return Stream.empty();
160163
}
@@ -652,5 +655,37 @@ public Optional<String> getTransactionPhase() {
652655
.map(it -> it.get("phase"))
653656
.map(Object::toString);
654657
}
658+
659+
/**
660+
* Returns all types referred to. Usually parameter types or types the method is interested in declared in
661+
* annotations.
662+
*
663+
* @return will never be {@literal null}.
664+
* @since 1.2.8, 1.3.2
665+
*/
666+
public Collection<Class<?>> getReferenceTypes() {
667+
668+
var parameterTypes = method.getRawParameterTypes();
669+
670+
if (!parameterTypes.isEmpty()) {
671+
return parameterTypes.stream()
672+
.<Class<?>> map(JavaClass::reflect)
673+
.toList();
674+
}
675+
676+
var attributes = AnnotatedElementUtils.getMergedAnnotationAttributes(method.reflect(),
677+
SpringTypes.AT_EVENT_LISTENER, false, false);
678+
679+
return List.of(attributes.getClassArray("classes"));
680+
}
681+
682+
/*
683+
* (non-Javadoc)
684+
* @see java.lang.Object#toString()
685+
*/
686+
@Override
687+
public String toString() {
688+
return method.toString();
689+
}
655690
}
656691
}

spring-modulith-core/src/test/java/org/springframework/modulith/core/ArchitecturallyEvidentTypeUnitTest.java

+24
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import org.springframework.modulith.core.ArchitecturallyEvidentType.SpringAwareArchitecturallyEvidentType;
3838
import org.springframework.modulith.core.ArchitecturallyEvidentType.SpringDataAwareArchitecturallyEvidentType;
3939
import org.springframework.stereotype.Repository;
40+
import org.springframework.transaction.event.TransactionalEventListener;
4041

4142
import com.tngtech.archunit.core.domain.JavaClass;
4243

@@ -196,6 +197,23 @@ void discoversJMoleculesRepository() {
196197
assertThat(ArchitecturallyEvidentType.of(type, classes).isRepository()).isTrue();
197198
}
198199

200+
@Test
201+
void detectsAnnotatedReferenceType() {
202+
203+
var type = classes.getRequiredClass(SomeEventListener.class);
204+
205+
var methods = ArchitecturallyEvidentType.of(type, classes).getReferenceMethods();
206+
207+
var annotatedMethod = methods.filter(it -> it.getMethod().getName().startsWith("annotated"));
208+
209+
assertThat(annotatedMethod).allSatisfy(it -> {
210+
211+
assertThat(it.getReferenceTypes())
212+
.extracting(Class::getName)
213+
.contains(String.class.getName());
214+
});
215+
}
216+
199217
private Iterator<ArchitecturallyEvidentType> getTypesFor(Class<?>... types) {
200218

201219
return Stream.of(types) //
@@ -266,6 +284,12 @@ void on(String event) {}
266284

267285
@EventListener
268286
void onOther(Object event) {}
287+
288+
@EventListener(classes = String.class)
289+
void annotatedOn() {}
290+
291+
@TransactionalEventListener(classes = String.class)
292+
void annotatedTxOn() {}
269293
}
270294

271295
class ImplementingEventListener implements ApplicationListener<ApplicationReadyEvent> {

spring-modulith-docs/src/main/java/org/springframework/modulith/docs/Asciidoctor.java

+15-13
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import static java.util.stream.Collectors.*;
1919
import static org.springframework.util.ClassUtils.*;
2020

21+
import java.util.Collection;
2122
import java.util.List;
2223
import java.util.Optional;
2324
import java.util.regex.Matcher;
@@ -321,29 +322,30 @@ private String toInlineCode(ArchitecturallyEvidentType type) {
321322
private String renderReferenceMethod(ReferenceMethod it, int level) {
322323

323324
var method = it.getMethod();
324-
Assert.isTrue(method.getRawParameterTypes().size() > 0,
325-
() -> "Method %s must have at least one parameter!".formatted(method));
325+
var exposedReferenceTypes = it.getReferenceTypes().stream()
326+
.filter(type -> modules.getModuleByType(type)
327+
.map(module -> module.isExposed(type))
328+
.orElse(true))
329+
.toList();
326330

327-
var parameterType = method.getRawParameterTypes().get(0);
328-
329-
var typeExposed = modules.getModuleByType(parameterType)
330-
.map(module -> module.isExposed(parameterType))
331-
.orElse(true);
332-
333-
if (!typeExposed) {
331+
if (exposedReferenceTypes.isEmpty()) {
334332
return "";
335333
}
336334

337335
var isAsync = it.isAsync() ? "(async) " : "";
338336
var indent = "*".repeat(level + 1);
339337

340338
return docSource.flatMap(source -> source.getDocumentation(method))
341-
.map(doc -> "%s %s %s-- %s".formatted(indent, toInlineCode(parameterType), isAsync, doc))
342-
.orElseGet(() -> "%s %s %s".formatted(indent, toInlineCode(parameterType), isAsync));
339+
.map(doc -> "%s %s %s-- %s".formatted(indent, toInlineCode(exposedReferenceTypes), isAsync, doc))
340+
.orElseGet(() -> "%s %s %s".formatted(indent, toInlineCode(exposedReferenceTypes), isAsync));
343341
}
344342

345-
private String toInlineCode(Stream<JavaClass> types) {
346-
return types.map(this::toInlineCode).collect(joining(", "));
343+
private String toInlineCode(Collection<Class<?>> types) {
344+
345+
return types.stream()
346+
.map(Class::getName)
347+
.map(this::toInlineCode)
348+
.collect(joining(", "));
347349
}
348350

349351
private static String toLink(String source, String href) {

0 commit comments

Comments
 (0)