Skip to content

Commit 7e08447

Browse files
committed
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.
1 parent 8192f1b commit 7e08447

File tree

5 files changed

+96
-7
lines changed

5 files changed

+96
-7
lines changed

spring-modulith-core/pom.xml

Lines changed: 6 additions & 0 deletions
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

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -302,7 +302,6 @@ public Optional<JavaClass> getType(String candidate) {
302302
* module's named interfaces.
303303
*
304304
* @param type must not be {@literal null}.
305-
* @return
306305
*/
307306
public boolean isExposed(JavaClass type) {
308307

@@ -311,6 +310,20 @@ public boolean isExposed(JavaClass type) {
311310
return namedInterfaces.stream().anyMatch(it -> it.contains(type));
312311
}
313312

313+
/**
314+
* Returns whether the given {@link JavaClass} is exposed by the current module, i.e. whether it's part of any of the
315+
* module's named interfaces.
316+
*
317+
* @param type must not be {@literal null}.
318+
* @since 1.2.8, 1.3.2
319+
*/
320+
public boolean isExposed(Class<?> type) {
321+
322+
Assert.notNull(type, "Type must not be null!");
323+
324+
return namedInterfaces.stream().anyMatch(it -> it.contains(type));
325+
}
326+
314327
public void verifyDependencies(ApplicationModules modules) {
315328
detectDependencies(modules).throwIfPresent();
316329
}

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

Lines changed: 36 additions & 1 deletion
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

Lines changed: 24 additions & 0 deletions
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

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import static org.springframework.util.ClassUtils.*;
1919

20+
import java.util.Collection;
2021
import java.util.List;
2122
import java.util.Optional;
2223
import java.util.regex.Matcher;
@@ -295,15 +296,17 @@ private String toInlineCode(ArchitecturallyEvidentType type) {
295296
return header + type.getReferenceMethods().map(it -> {
296297

297298
var method = it.getMethod();
298-
Assert.isTrue(method.getRawParameterTypes().size() > 0,
299-
() -> String.format("Method %s must have at least one parameter!", method));
299+
var exposedReferenceTypes = it.getReferenceTypes().stream()
300+
.filter(refType -> modules.getModuleByType(refType)
301+
.map(module -> module.isExposed(refType))
302+
.orElse(true))
303+
.toList();
300304

301-
var parameterType = method.getRawParameterTypes().get(0);
302305
var isAsync = it.isAsync() ? "(async) " : "";
303306

304307
return docSource.flatMap(source -> source.getDocumentation(method))
305-
.map(doc -> String.format("** %s %s-- %s", toInlineCode(parameterType), isAsync, doc))
306-
.orElseGet(() -> String.format("** %s %s", toInlineCode(parameterType), isAsync));
308+
.map(doc -> String.format("** %s %s-- %s", toInlineCode(exposedReferenceTypes), isAsync, doc))
309+
.orElseGet(() -> String.format("** %s %s", toInlineCode(exposedReferenceTypes), isAsync));
307310

308311
}).collect(Collectors.joining("\n"));
309312
}
@@ -317,6 +320,14 @@ private String toInlineCode(Stream<JavaClass> types) {
317320
.collect(Collectors.joining(", "));
318321
}
319322

323+
private String toInlineCode(Collection<Class<?>> types) {
324+
325+
return types.stream()
326+
.map(Class::getName)
327+
.map(this::toInlineCode)
328+
.collect(Collectors.joining(", "));
329+
}
330+
320331
private static String toLink(String source, String href) {
321332
return String.format("link:%s[%s]", href, source);
322333
}

0 commit comments

Comments
 (0)