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.
This commit is contained in:
@@ -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 <T> 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 <T> Class<T> loadIfPresent(String name) {
|
||||
public static <T> Class<T> loadIfPresent(String name) {
|
||||
|
||||
Assert.hasText(name, "Name must not be null or empty!");
|
||||
|
||||
ClassLoader loader = Types.class.getClassLoader();
|
||||
|
||||
|
||||
@@ -93,6 +93,12 @@
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-messaging</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
|
||||
@@ -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<Class<?>> IGNORED_TYPES = List.of(Advised.class, TargetClassAware.class);
|
||||
private static Predicate<Method> IS_USER_METHOD = it -> !Modifier.isPrivate(it.getModifiers())
|
||||
private static final Collection<Class<?>> IGNORED_TYPES = List.of(Advised.class, TargetClassAware.class);
|
||||
private static final Predicate<Method> 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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() {}
|
||||
}
|
||||
@@ -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 {}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user