From 671df7b180a33a05697021ea71e09da42897b117 Mon Sep 17 00:00:00 2001 From: Oliver Drotbohm Date: Mon, 18 Nov 2024 20:01:04 +0100 Subject: [PATCH] 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. --- .../springframework/modulith/core/Types.java | 16 ++++++++-- spring-modulith-observability/pom.xml | 6 ++++ .../observability/ObservedModuleType.java | 31 ++++++++++++++---- .../example/sample/SampleMessageListener.java | 28 ++++++++++++++++ .../java/example/sample/TestListener.java | 32 +++++++++++++++++++ .../ObservedModuleTypeUnitTests.java | 12 +++++++ 6 files changed, 117 insertions(+), 8 deletions(-) create mode 100644 spring-modulith-observability/src/test/java/example/sample/SampleMessageListener.java create mode 100644 spring-modulith-observability/src/test/java/example/sample/TestListener.java diff --git a/spring-modulith-core/src/main/java/org/springframework/modulith/core/Types.java b/spring-modulith-core/src/main/java/org/springframework/modulith/core/Types.java index 1f4064ed..35e0592f 100644 --- a/spring-modulith-core/src/main/java/org/springframework/modulith/core/Types.java +++ b/spring-modulith-core/src/main/java/org/springframework/modulith/core/Types.java @@ -20,6 +20,7 @@ import static com.tngtech.archunit.core.domain.JavaClass.Predicates.*; import java.lang.annotation.Annotation; import org.springframework.lang.Nullable; +import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import com.tngtech.archunit.base.DescribedPredicate; @@ -29,13 +30,24 @@ import com.tngtech.archunit.core.domain.properties.CanBeAnnotated; import com.tngtech.archunit.core.domain.properties.CanBeAnnotated.Predicates; /** + * Utility to deal with a variety of types. + * * @author Oliver Drotbohm */ -class Types { +public class Types { + /** + * Loads the class with the given name if present on the classpath. + * + * @param the type to be loaded + * @param name the fully-qualified name of the type to be loaded, must not be {@literal null} or empty. + * @return can be {@literal null}. + */ @Nullable @SuppressWarnings("unchecked") - static Class loadIfPresent(String name) { + public static Class loadIfPresent(String name) { + + Assert.hasText(name, "Name must not be null or empty!"); ClassLoader loader = Types.class.getClassLoader(); diff --git a/spring-modulith-observability/pom.xml b/spring-modulith-observability/pom.xml index d4c937ff..212f8753 100644 --- a/spring-modulith-observability/pom.xml +++ b/spring-modulith-observability/pom.xml @@ -93,6 +93,12 @@ test + + org.springframework + spring-messaging + test + + diff --git a/spring-modulith-observability/src/main/java/org/springframework/modulith/observability/ObservedModuleType.java b/spring-modulith-observability/src/main/java/org/springframework/modulith/observability/ObservedModuleType.java index 357a1cc2..c0c616c5 100644 --- a/spring-modulith-observability/src/main/java/org/springframework/modulith/observability/ObservedModuleType.java +++ b/spring-modulith-observability/src/main/java/org/springframework/modulith/observability/ObservedModuleType.java @@ -31,6 +31,8 @@ import org.springframework.modulith.core.ArchitecturallyEvidentType.ReferenceMet import org.springframework.util.Assert; import org.springframework.util.ReflectionUtils; +import com.tngtech.archunit.core.domain.JavaClass; + /** * Represents a type in an {@link ObservedModule}. * @@ -38,9 +40,10 @@ import org.springframework.util.ReflectionUtils; */ class ObservedModuleType { - private static Collection> IGNORED_TYPES = List.of(Advised.class, TargetClassAware.class); - private static Predicate IS_USER_METHOD = it -> !Modifier.isPrivate(it.getModifiers()) + private static final Collection> IGNORED_TYPES = List.of(Advised.class, TargetClassAware.class); + private static final Predicate IS_USER_METHOD = it -> !Modifier.isPrivate(it.getModifiers()) && !(ReflectionUtils.isObjectMethod(it) || IGNORED_TYPES.contains(it.getDeclaringClass())); + private static final String MESSAGE_MAPPING_ANNOTATION = "org.springframework.messaging.handler.annotation.MessageMapping"; private final ApplicationModules modules; private final ObservedModule module; @@ -75,18 +78,19 @@ class ObservedModuleType { /** * Returns whether the type should be observed at all. Can be skipped for types not exposed by the module unless they * listen to events of other modules. - * - * @return */ public boolean shouldBeObserved() { - if (type.getType().isMetaAnnotatedWith(Configuration.class)) { + var javaType = type.getType(); + + if (javaType.isMetaAnnotatedWith(Configuration.class)) { return false; } return type.isController() || listensToOtherModulesEvents() - || module.exposes(type.getType()); + || module.exposes(javaType) + || hasMethodWithMessageMappingAnnotation(javaType); } /** @@ -115,4 +119,19 @@ class ObservedModuleType { .map(it -> !module.isObservedModule(it)) .orElse(true); } + + /** + * Returns whether the given type contains a method (meta-)annotated with {@code MessageMapping}. + * + * @param type must not be {@literal null}. + */ + private static boolean hasMethodWithMessageMappingAnnotation(JavaClass type) { + + Assert.notNull(type, "Type must not be null!"); + + return MESSAGE_MAPPING_ANNOTATION != null + && type.getMethods() + .stream() + .anyMatch(it -> it.isMetaAnnotatedWith(MESSAGE_MAPPING_ANNOTATION)); + } } diff --git a/spring-modulith-observability/src/test/java/example/sample/SampleMessageListener.java b/spring-modulith-observability/src/test/java/example/sample/SampleMessageListener.java new file mode 100644 index 00000000..27828508 --- /dev/null +++ b/spring-modulith-observability/src/test/java/example/sample/SampleMessageListener.java @@ -0,0 +1,28 @@ +/* + * Copyright 2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package example.sample; + +import org.springframework.stereotype.Component; + +/** + * @author Oliver Drotbohm + */ +@Component +class SampleMessageListener { + + @TestListener + void listener() {} +} diff --git a/spring-modulith-observability/src/test/java/example/sample/TestListener.java b/spring-modulith-observability/src/test/java/example/sample/TestListener.java new file mode 100644 index 00000000..7da569b3 --- /dev/null +++ b/spring-modulith-observability/src/test/java/example/sample/TestListener.java @@ -0,0 +1,32 @@ +/* + * Copyright 2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package example.sample; + +import static java.lang.annotation.ElementType.*; +import static java.lang.annotation.RetentionPolicy.*; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import org.springframework.messaging.handler.annotation.MessageMapping; + +/** + * @author Oliver Drotbohm + */ +@Retention(RUNTIME) +@Target(METHOD) +@MessageMapping +@interface TestListener {} diff --git a/spring-modulith-observability/src/test/java/org/springframework/modulith/observability/ObservedModuleTypeUnitTests.java b/spring-modulith-observability/src/test/java/org/springframework/modulith/observability/ObservedModuleTypeUnitTests.java index b20e5893..42b85bf0 100644 --- a/spring-modulith-observability/src/test/java/org/springframework/modulith/observability/ObservedModuleTypeUnitTests.java +++ b/spring-modulith-observability/src/test/java/org/springframework/modulith/observability/ObservedModuleTypeUnitTests.java @@ -25,6 +25,7 @@ import org.springframework.aop.framework.Advised; import org.springframework.modulith.core.ApplicationModule; import org.springframework.modulith.core.ApplicationModules; import org.springframework.modulith.core.ArchitecturallyEvidentType; +import org.springframework.modulith.core.Types; import org.springframework.modulith.test.TestApplicationModules; import org.springframework.util.ReflectionUtils; @@ -69,4 +70,15 @@ class ObservedModuleTypeUnitTests { assertThat(observedType.shouldBeObserved()).isFalse(); } + + @Test // GH-936 + void exposesMessageListenerMethodsForObservation() { + + var type = Types.loadIfPresent("example.sample.SampleMessageListener"); + + var architecturallyEvidentType = module.getArchitecturallyEvidentType(type); + var moduleType = new ObservedModuleType(modules, new DefaultObservedModule(module), architecturallyEvidentType); + + assertThat(moduleType.shouldBeObserved()).isTrue(); + } }