diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/MessageMapping.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/MessageMapping.java
index d6530a69873..d006ca92693 100644
--- a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/MessageMapping.java
+++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/MessageMapping.java
@@ -22,6 +22,7 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
+import org.springframework.aot.hint.annotation.Reflective;
import org.springframework.messaging.Message;
/**
@@ -106,6 +107,7 @@ import org.springframework.messaging.Message;
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
+@Reflective(MessageMappingReflectiveProcessor.class)
public @interface MessageMapping {
/**
diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/MessageMappingReflectiveProcessor.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/MessageMappingReflectiveProcessor.java
new file mode 100644
index 00000000000..f1004dc6ba3
--- /dev/null
+++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/MessageMappingReflectiveProcessor.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2002-2022 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 org.springframework.messaging.handler.annotation;
+
+import java.lang.reflect.AnnotatedElement;
+import java.lang.reflect.Method;
+import java.lang.reflect.Parameter;
+import java.lang.reflect.Type;
+import java.security.Principal;
+
+import org.springframework.aot.hint.ExecutableMode;
+import org.springframework.aot.hint.ReflectionHints;
+import org.springframework.aot.hint.annotation.ReflectiveProcessor;
+import org.springframework.context.aot.BindingReflectionHintsRegistrar;
+import org.springframework.core.MethodParameter;
+import org.springframework.lang.Nullable;
+import org.springframework.messaging.Message;
+import org.springframework.messaging.MessageHeaders;
+import org.springframework.messaging.support.MessageHeaderAccessor;
+
+/**
+ * {@link ReflectiveProcessor} implementation for {@link MessageMapping}
+ * annotated types. On top of registering reflection hints for invoking
+ * the annotated method, this implementation handles:
+ *
+ * - Return types.
+ * - Parameters identified as potential payload.
+ * - {@link Message} parameters.
+ *
+ *
+ * @author Sebastien Deleuze
+ * @since 6.0
+ */
+class MessageMappingReflectiveProcessor implements ReflectiveProcessor {
+
+ private final BindingReflectionHintsRegistrar bindingRegistrar = new BindingReflectionHintsRegistrar();
+
+ @Override
+ public void registerReflectionHints(ReflectionHints hints, AnnotatedElement element) {
+ if (element instanceof Class> type) {
+ registerTypeHints(hints, type);
+ }
+ else if (element instanceof Method method) {
+ registerMethodHints(hints, method);
+ }
+ }
+
+ protected void registerTypeHints(ReflectionHints hints, Class> type) {
+ hints.registerType(type, hint -> {});
+ }
+
+ protected void registerMethodHints(ReflectionHints hints, Method method) {
+ hints.registerMethod(method, hint -> hint.setModes(ExecutableMode.INVOKE));
+ registerParameterHints(hints, method);
+ registerReturnValueHints(hints, method);
+ }
+
+ protected void registerParameterHints(ReflectionHints hints, Method method) {
+ hints.registerMethod(method, hint -> hint.setModes(ExecutableMode.INVOKE));
+ for (Parameter parameter : method.getParameters()) {
+ MethodParameter methodParameter = MethodParameter.forParameter(parameter);
+ if (Message.class.isAssignableFrom(methodParameter.getParameterType())) {
+ this.bindingRegistrar.registerReflectionHints(hints, getMessageType(methodParameter));
+ }
+ else if (couldBePayload(methodParameter)) {
+ this.bindingRegistrar.registerReflectionHints(hints, methodParameter.getGenericParameterType());
+ }
+ }
+ }
+
+ protected boolean couldBePayload(MethodParameter methodParameter) {
+ return !methodParameter.hasParameterAnnotation(DestinationVariable.class) &&
+ !methodParameter.hasParameterAnnotation(Header.class) &&
+ !methodParameter.hasParameterAnnotation(Headers.class) &&
+ !MessageHeaders.class.isAssignableFrom(methodParameter.getParameterType()) &&
+ !MessageHeaderAccessor.class.isAssignableFrom(methodParameter.getParameterType()) &&
+ !Principal.class.isAssignableFrom(methodParameter.nestedIfOptional().getNestedParameterType());
+ }
+
+ protected void registerReturnValueHints(ReflectionHints hints, Method method) {
+ MethodParameter returnType = MethodParameter.forExecutable(method, -1);
+ this.bindingRegistrar.registerReflectionHints(hints, returnType.getGenericParameterType());
+ }
+
+ @Nullable
+ protected Type getMessageType(MethodParameter parameter) {
+ MethodParameter nestedParameter = parameter.nested();
+ return (nestedParameter.getNestedParameterType() == nestedParameter.getParameterType() ? null : nestedParameter.getNestedParameterType());
+ }
+}
diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/MessagingAnnotationsRuntimeHintsRegistrar.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/MessagingAnnotationsRuntimeHintsRegistrar.java
new file mode 100644
index 00000000000..80727fff682
--- /dev/null
+++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/MessagingAnnotationsRuntimeHintsRegistrar.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2002-2022 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 org.springframework.messaging.handler.annotation;
+
+import java.util.stream.Stream;
+
+import org.springframework.aot.hint.RuntimeHints;
+import org.springframework.aot.hint.RuntimeHintsRegistrar;
+import org.springframework.aot.hint.support.RuntimeHintsUtils;
+import org.springframework.lang.Nullable;
+import org.springframework.stereotype.Controller;
+
+/**
+ * {@link RuntimeHintsRegistrar} implementation that makes messaging
+ * annotations available at runtime.
+ *
+ * @author Sebastien Deleuze
+ * @since 6.0
+ */
+public class MessagingAnnotationsRuntimeHintsRegistrar implements RuntimeHintsRegistrar {
+
+ @Override
+ public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) {
+ Stream.of(Controller.class, DestinationVariable.class, Header.class, Headers.class,
+ MessageExceptionHandler.class, MessageMapping.class, Payload.class, SendTo.class).forEach(
+ annotationType -> RuntimeHintsUtils.registerAnnotation(hints, annotationType));
+ }
+}
diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/annotation/SimpAnnotationsRuntimeHintsRegistrar.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/annotation/SimpAnnotationsRuntimeHintsRegistrar.java
new file mode 100644
index 00000000000..b971af9b810
--- /dev/null
+++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/annotation/SimpAnnotationsRuntimeHintsRegistrar.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2002-2022 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 org.springframework.messaging.simp.annotation;
+
+import java.util.stream.Stream;
+
+import org.springframework.aot.hint.RuntimeHints;
+import org.springframework.aot.hint.RuntimeHintsRegistrar;
+import org.springframework.aot.hint.support.RuntimeHintsUtils;
+
+/**
+ * {@link RuntimeHintsRegistrar} implementation that makes Simp annotations
+ * available at runtime.
+ *
+ * @author Sebastien Deleuze
+ * @since 6.0
+ */
+public class SimpAnnotationsRuntimeHintsRegistrar implements RuntimeHintsRegistrar {
+
+ @Override
+ public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
+ Stream.of(SendToUser.class, SubscribeMapping.class).forEach(
+ annotationType -> RuntimeHintsUtils.registerAnnotation(hints, annotationType));
+ }
+}
diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/config/AbstractMessageBrokerConfiguration.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/config/AbstractMessageBrokerConfiguration.java
index f490c0cf664..1a59485adec 100644
--- a/spring-messaging/src/main/java/org/springframework/messaging/simp/config/AbstractMessageBrokerConfiguration.java
+++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/config/AbstractMessageBrokerConfiguration.java
@@ -28,6 +28,7 @@ import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.ImportRuntimeHints;
import org.springframework.context.event.SmartApplicationListener;
import org.springframework.core.task.TaskExecutor;
import org.springframework.lang.Nullable;
@@ -41,10 +42,12 @@ import org.springframework.messaging.converter.KotlinSerializationJsonMessageCon
import org.springframework.messaging.converter.MappingJackson2MessageConverter;
import org.springframework.messaging.converter.MessageConverter;
import org.springframework.messaging.converter.StringMessageConverter;
+import org.springframework.messaging.handler.annotation.MessagingAnnotationsRuntimeHintsRegistrar;
import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver;
import org.springframework.messaging.handler.invocation.HandlerMethodReturnValueHandler;
import org.springframework.messaging.simp.SimpLogging;
import org.springframework.messaging.simp.SimpMessagingTemplate;
+import org.springframework.messaging.simp.annotation.SimpAnnotationsRuntimeHintsRegistrar;
import org.springframework.messaging.simp.annotation.support.SimpAnnotationMethodMessageHandler;
import org.springframework.messaging.simp.broker.AbstractBrokerMessageHandler;
import org.springframework.messaging.simp.broker.SimpleBrokerMessageHandler;
@@ -95,6 +98,7 @@ import org.springframework.validation.Validator;
* @author Sebastien Deleuze
* @since 4.0
*/
+@ImportRuntimeHints({ MessagingAnnotationsRuntimeHintsRegistrar.class, SimpAnnotationsRuntimeHintsRegistrar.class })
public abstract class AbstractMessageBrokerConfiguration implements ApplicationContextAware {
private static final String MVC_VALIDATOR_NAME = "mvcValidator";
diff --git a/spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/MessageMappingReflectiveProcessorTests.java b/spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/MessageMappingReflectiveProcessorTests.java
new file mode 100644
index 00000000000..fbb6d72d665
--- /dev/null
+++ b/spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/MessageMappingReflectiveProcessorTests.java
@@ -0,0 +1,185 @@
+/*
+ * Copyright 2002-2022 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 org.springframework.messaging.handler.annotation;
+
+import java.lang.reflect.Method;
+import java.security.Principal;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.aot.hint.MemberCategory;
+import org.springframework.aot.hint.ReflectionHints;
+import org.springframework.aot.hint.TypeReference;
+import org.springframework.messaging.Message;
+import org.springframework.messaging.MessageHeaders;
+import org.springframework.messaging.support.MessageHeaderAccessor;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Tests for {@link MessageMappingReflectiveProcessor}.
+ *
+ * @author Sebastien Deleuze
+ */
+public class MessageMappingReflectiveProcessorTests {
+
+ private final MessageMappingReflectiveProcessor processor = new MessageMappingReflectiveProcessor();
+
+ private final ReflectionHints hints = new ReflectionHints();
+
+ @Test
+ void registerReflectiveHintsForMethodWithReturnValue() throws NoSuchMethodException {
+ Method method = SampleController.class.getDeclaredMethod("returnValue");
+ processor.registerReflectionHints(hints, method);
+ assertThat(hints.typeHints()).satisfiesExactlyInAnyOrder(
+ typeHint -> assertThat(typeHint.getType()).isEqualTo(TypeReference.of(SampleController.class)),
+ typeHint -> {
+ assertThat(typeHint.getType()).isEqualTo(TypeReference.of(OutgoingMessage.class));
+ assertThat(typeHint.getMemberCategories()).containsExactlyInAnyOrder(
+ MemberCategory.INVOKE_DECLARED_CONSTRUCTORS,
+ MemberCategory.DECLARED_FIELDS);
+ assertThat(typeHint.methods()).satisfiesExactlyInAnyOrder(
+ hint -> assertThat(hint.getName()).isEqualTo("getMessage"),
+ hint -> assertThat(hint.getName()).isEqualTo("setMessage"));
+ },
+ typeHint -> assertThat(typeHint.getType()).isEqualTo(TypeReference.of(String.class)));
+ }
+
+ @Test
+ void registerReflectiveHintsForMethodWithExplicitPayload() throws NoSuchMethodException {
+ Method method = SampleController.class.getDeclaredMethod("explicitPayload", IncomingMessage.class);
+ processor.registerReflectionHints(hints, method);
+ assertThat(hints.typeHints()).satisfiesExactlyInAnyOrder(
+ typeHint -> assertThat(typeHint.getType()).isEqualTo(TypeReference.of(SampleController.class)),
+ typeHint -> {
+ assertThat(typeHint.getType()).isEqualTo(TypeReference.of(IncomingMessage.class));
+ assertThat(typeHint.getMemberCategories()).containsExactlyInAnyOrder(
+ MemberCategory.INVOKE_DECLARED_CONSTRUCTORS,
+ MemberCategory.DECLARED_FIELDS);
+ assertThat(typeHint.methods()).satisfiesExactlyInAnyOrder(
+ hint -> assertThat(hint.getName()).isEqualTo("getMessage"),
+ hint -> assertThat(hint.getName()).isEqualTo("setMessage"));
+ },
+ typeHint -> assertThat(typeHint.getType()).isEqualTo(TypeReference.of(String.class)));
+ }
+
+ @Test
+ void registerReflectiveHintsForMethodWithImplicitPayload() throws NoSuchMethodException {
+ Method method = SampleController.class.getDeclaredMethod("implicitPayload", IncomingMessage.class);
+ processor.registerReflectionHints(hints, method);
+ assertThat(hints.typeHints()).satisfiesExactlyInAnyOrder(
+ typeHint -> assertThat(typeHint.getType()).isEqualTo(TypeReference.of(SampleController.class)),
+ typeHint -> assertThat(typeHint.getType()).isEqualTo(TypeReference.of(IncomingMessage.class)),
+ typeHint -> assertThat(typeHint.getType()).isEqualTo(TypeReference.of(String.class)));
+ }
+
+ @Test
+ void registerReflectiveHintsForMethodWithMessage() throws NoSuchMethodException {
+ Method method = SampleController.class.getDeclaredMethod("message", Message.class);
+ processor.registerReflectionHints(hints, method);
+ assertThat(hints.typeHints()).satisfiesExactlyInAnyOrder(
+ typeHint -> assertThat(typeHint.getType()).isEqualTo(TypeReference.of(SampleController.class)),
+ typeHint -> assertThat(typeHint.getType()).isEqualTo(TypeReference.of(IncomingMessage.class)),
+ typeHint -> assertThat(typeHint.getType()).isEqualTo(TypeReference.of(String.class)));
+ }
+
+ @Test
+ void registerReflectiveHintsForMethodWithImplicitPayloadAndIgnoredAnnotations() throws NoSuchMethodException {
+ Method method = SampleController.class.getDeclaredMethod("implicitPayloadWithIgnoredAnnotations",
+ IncomingMessage.class, Ignored.class, Ignored.class, Ignored.class, MessageHeaders.class,
+ MessageHeaderAccessor.class, Principal.class);
+ processor.registerReflectionHints(hints, method);
+ assertThat(hints.typeHints()).satisfiesExactlyInAnyOrder(
+ typeHint -> assertThat(typeHint.getType()).isEqualTo(TypeReference.of(SampleController.class)),
+ typeHint -> assertThat(typeHint.getType()).isEqualTo(TypeReference.of(IncomingMessage.class)),
+ typeHint -> assertThat(typeHint.getType()).isEqualTo(TypeReference.of(String.class)));
+ }
+
+ @Test
+ void registerReflectiveHintsForClass() {
+ processor.registerReflectionHints(hints, SampleAnnotatedController.class);
+ assertThat(hints.typeHints()).singleElement().satisfies(
+ typeHint -> assertThat(typeHint.getType()).isEqualTo(TypeReference.of(SampleAnnotatedController.class)));
+ }
+
+
+ static class SampleController {
+
+ @MessageMapping
+ OutgoingMessage returnValue() {
+ return new OutgoingMessage("message");
+ }
+
+ @MessageMapping
+ void explicitPayload(@Payload IncomingMessage incomingMessage) {
+ }
+
+ @MessageMapping
+ void implicitPayload(IncomingMessage incomingMessage) {
+ }
+
+ @MessageMapping
+ void message(Message message) {
+ }
+
+ @MessageMapping
+ void implicitPayloadWithIgnoredAnnotations(IncomingMessage incomingMessage,
+ @DestinationVariable Ignored destinationVariable,
+ @Header Ignored header,
+ @Headers Ignored headers,
+ MessageHeaders messageHeaders,
+ MessageHeaderAccessor messageHeaderAccessor,
+ Principal principal) {
+ }
+ }
+
+ @MessageMapping
+ static class SampleAnnotatedController {
+ }
+
+ static class IncomingMessage {
+
+ private String message;
+
+ public String getMessage() {
+ return message;
+ }
+
+ public void setMessage(String message) {
+ this.message = message;
+ }
+ }
+
+ static class OutgoingMessage {
+
+ private String message;
+
+ public OutgoingMessage(String message) {
+ this.message = message;
+ }
+
+ public String getMessage() {
+ return message;
+ }
+
+ public void setMessage(String message) {
+ this.message = message;
+ }
+ }
+
+ static class Ignored {}
+}