From bcb6f13fc4e0b38e9e9739f9709f621963cca5bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Deleuze?= Date: Mon, 11 Jul 2022 19:40:10 +0200 Subject: [PATCH] Add native image support for STOMP messaging This commit adds reflection hints for messaging annotations as well as for classes and methods annotated with @MessageMapping. Closes gh-28754 --- .../handler/annotation/MessageMapping.java | 2 + .../MessageMappingReflectiveProcessor.java | 104 ++++++++++ ...agingAnnotationsRuntimeHintsRegistrar.java | 42 ++++ .../SimpAnnotationsRuntimeHintsRegistrar.java | 39 ++++ .../AbstractMessageBrokerConfiguration.java | 4 + ...essageMappingReflectiveProcessorTests.java | 185 ++++++++++++++++++ 6 files changed, 376 insertions(+) create mode 100644 spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/MessageMappingReflectiveProcessor.java create mode 100644 spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/MessagingAnnotationsRuntimeHintsRegistrar.java create mode 100644 spring-messaging/src/main/java/org/springframework/messaging/simp/annotation/SimpAnnotationsRuntimeHintsRegistrar.java create mode 100644 spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/MessageMappingReflectiveProcessorTests.java 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: + * + * + * @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 {} +}