Skip to content

Commit 671df7b

Browse files
committed
GH-947 - Detect methods (meta-)annotated with @MessageMapping as module entry points.
We now consider all methods that are (meta-)annotated with Spring Messaging's @MessageMapping which is consistently used in a lot of broker annotations such as @(Rabbit|Kafka)Listener etc.
1 parent ed6c4a0 commit 671df7b

File tree

6 files changed

+118
-9
lines changed

6 files changed

+118
-9
lines changed

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

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import java.lang.annotation.Annotation;
2121

2222
import org.springframework.lang.Nullable;
23+
import org.springframework.util.Assert;
2324
import org.springframework.util.ClassUtils;
2425

2526
import com.tngtech.archunit.base.DescribedPredicate;
@@ -29,13 +30,24 @@
2930
import com.tngtech.archunit.core.domain.properties.CanBeAnnotated.Predicates;
3031

3132
/**
33+
* Utility to deal with a variety of types.
34+
*
3235
* @author Oliver Drotbohm
3336
*/
34-
class Types {
35-
37+
public class Types {
38+
39+
/**
40+
* Loads the class with the given name if present on the classpath.
41+
*
42+
* @param <T> the type to be loaded
43+
* @param name the fully-qualified name of the type to be loaded, must not be {@literal null} or empty.
44+
* @return can be {@literal null}.
45+
*/
3646
@Nullable
3747
@SuppressWarnings("unchecked")
38-
static <T> Class<T> loadIfPresent(String name) {
48+
public static <T> Class<T> loadIfPresent(String name) {
49+
50+
Assert.hasText(name, "Name must not be null or empty!");
3951

4052
ClassLoader loader = Types.class.getClassLoader();
4153

spring-modulith-observability/pom.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,12 @@
9393
<scope>test</scope>
9494
</dependency>
9595

96+
<dependency>
97+
<groupId>org.springframework</groupId>
98+
<artifactId>spring-messaging</artifactId>
99+
<scope>test</scope>
100+
</dependency>
101+
96102
</dependencies>
97103

98104
</project>

spring-modulith-observability/src/main/java/org/springframework/modulith/observability/ObservedModuleType.java

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,16 +31,19 @@
3131
import org.springframework.util.Assert;
3232
import org.springframework.util.ReflectionUtils;
3333

34+
import com.tngtech.archunit.core.domain.JavaClass;
35+
3436
/**
3537
* Represents a type in an {@link ObservedModule}.
3638
*
3739
* @author Oliver Drotbohm
3840
*/
3941
class ObservedModuleType {
4042

41-
private static Collection<Class<?>> IGNORED_TYPES = List.of(Advised.class, TargetClassAware.class);
42-
private static Predicate<Method> IS_USER_METHOD = it -> !Modifier.isPrivate(it.getModifiers())
43+
private static final Collection<Class<?>> IGNORED_TYPES = List.of(Advised.class, TargetClassAware.class);
44+
private static final Predicate<Method> IS_USER_METHOD = it -> !Modifier.isPrivate(it.getModifiers())
4345
&& !(ReflectionUtils.isObjectMethod(it) || IGNORED_TYPES.contains(it.getDeclaringClass()));
46+
private static final String MESSAGE_MAPPING_ANNOTATION = "org.springframework.messaging.handler.annotation.MessageMapping";
4447

4548
private final ApplicationModules modules;
4649
private final ObservedModule module;
@@ -75,18 +78,19 @@ class ObservedModuleType {
7578
/**
7679
* Returns whether the type should be observed at all. Can be skipped for types not exposed by the module unless they
7780
* listen to events of other modules.
78-
*
79-
* @return
8081
*/
8182
public boolean shouldBeObserved() {
8283

83-
if (type.getType().isMetaAnnotatedWith(Configuration.class)) {
84+
var javaType = type.getType();
85+
86+
if (javaType.isMetaAnnotatedWith(Configuration.class)) {
8487
return false;
8588
}
8689

8790
return type.isController()
8891
|| listensToOtherModulesEvents()
89-
|| module.exposes(type.getType());
92+
|| module.exposes(javaType)
93+
|| hasMethodWithMessageMappingAnnotation(javaType);
9094
}
9195

9296
/**
@@ -115,4 +119,19 @@ private boolean listensToOtherModulesEvents() {
115119
.map(it -> !module.isObservedModule(it))
116120
.orElse(true);
117121
}
122+
123+
/**
124+
* Returns whether the given type contains a method (meta-)annotated with {@code MessageMapping}.
125+
*
126+
* @param type must not be {@literal null}.
127+
*/
128+
private static boolean hasMethodWithMessageMappingAnnotation(JavaClass type) {
129+
130+
Assert.notNull(type, "Type must not be null!");
131+
132+
return MESSAGE_MAPPING_ANNOTATION != null
133+
&& type.getMethods()
134+
.stream()
135+
.anyMatch(it -> it.isMetaAnnotatedWith(MESSAGE_MAPPING_ANNOTATION));
136+
}
118137
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*
2+
* Copyright 2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package example.sample;
17+
18+
import org.springframework.stereotype.Component;
19+
20+
/**
21+
* @author Oliver Drotbohm
22+
*/
23+
@Component
24+
class SampleMessageListener {
25+
26+
@TestListener
27+
void listener() {}
28+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
* Copyright 2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package example.sample;
17+
18+
import static java.lang.annotation.ElementType.*;
19+
import static java.lang.annotation.RetentionPolicy.*;
20+
21+
import java.lang.annotation.Retention;
22+
import java.lang.annotation.Target;
23+
24+
import org.springframework.messaging.handler.annotation.MessageMapping;
25+
26+
/**
27+
* @author Oliver Drotbohm
28+
*/
29+
@Retention(RUNTIME)
30+
@Target(METHOD)
31+
@MessageMapping
32+
@interface TestListener {}

spring-modulith-observability/src/test/java/org/springframework/modulith/observability/ObservedModuleTypeUnitTests.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import org.springframework.modulith.core.ApplicationModule;
2626
import org.springframework.modulith.core.ApplicationModules;
2727
import org.springframework.modulith.core.ArchitecturallyEvidentType;
28+
import org.springframework.modulith.core.Types;
2829
import org.springframework.modulith.test.TestApplicationModules;
2930
import org.springframework.util.ReflectionUtils;
3031

@@ -69,4 +70,15 @@ void doesNotObserveConfigurationClasses() {
6970

7071
assertThat(observedType.shouldBeObserved()).isFalse();
7172
}
73+
74+
@Test // GH-936
75+
void exposesMessageListenerMethodsForObservation() {
76+
77+
var type = Types.loadIfPresent("example.sample.SampleMessageListener");
78+
79+
var architecturallyEvidentType = module.getArchitecturallyEvidentType(type);
80+
var moduleType = new ObservedModuleType(modules, new DefaultObservedModule(module), architecturallyEvidentType);
81+
82+
assertThat(moduleType.shouldBeObserved()).isTrue();
83+
}
7284
}

0 commit comments

Comments
 (0)