From 626a7fc52aa6a60c9f47db5abbfb44e59782a961 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Deleuze?= Date: Tue, 21 Feb 2023 18:47:30 +0100 Subject: [PATCH] Add native support for @SubscribeMapping and @MessageExceptionHandler Closes gh-30002 --- .../annotation/MessageExceptionHandler.java | 3 + .../MessageMappingReflectiveProcessor.java | 20 ++++++- .../simp/annotation/SubscribeMapping.java | 4 ++ ...essageMappingReflectiveProcessorTests.java | 56 ++++++++++++++----- 4 files changed, 66 insertions(+), 17 deletions(-) diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/MessageExceptionHandler.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/MessageExceptionHandler.java index 81a1c403e8..42588c0975 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/MessageExceptionHandler.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/MessageExceptionHandler.java @@ -22,6 +22,8 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import org.springframework.aot.hint.annotation.Reflective; + /** * Annotation for handling exceptions thrown from message-handling methods within a * specific handler class. @@ -32,6 +34,7 @@ import java.lang.annotation.Target; @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented +@Reflective(MessageMappingReflectiveProcessor.class) public @interface MessageExceptionHandler { /** 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 index cdfed1ff7c..825921fd9a 100644 --- 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 @@ -30,23 +30,28 @@ import org.springframework.core.MethodParameter; import org.springframework.lang.Nullable; import org.springframework.messaging.Message; import org.springframework.messaging.MessageHeaders; +import org.springframework.messaging.simp.annotation.SubscribeMapping; import org.springframework.messaging.support.MessageHeaderAccessor; /** - * {@link ReflectiveProcessor} implementation for {@link MessageMapping} - * annotated types. In addition to registering reflection hints for invoking + * {@link ReflectiveProcessor} implementation for types annotated + * with {@link MessageMapping @MessageMapping}, + * {@link SubscribeMapping @SubscribeMapping} + * and {@link MessageExceptionHandler @MessageExceptionHandler}. + * In addition to registering reflection hints for invoking * the annotated method, this implementation handles: * * * * @author Sebastien Deleuze * @since 6.0 */ -class MessageMappingReflectiveProcessor implements ReflectiveProcessor { +public class MessageMappingReflectiveProcessor implements ReflectiveProcessor { private final BindingReflectionHintsRegistrar bindingRegistrar = new BindingReflectionHintsRegistrar(); @@ -58,6 +63,9 @@ class MessageMappingReflectiveProcessor implements ReflectiveProcessor { } else if (element instanceof Method method) { registerMethodHints(hints, method); + if (element.isAnnotationPresent(MessageExceptionHandler.class)) { + registerMessageExceptionHandlerHints(hints, element.getAnnotation(MessageExceptionHandler.class)); + } } } @@ -84,6 +92,12 @@ class MessageMappingReflectiveProcessor implements ReflectiveProcessor { } } + protected void registerMessageExceptionHandlerHints(ReflectionHints hints, MessageExceptionHandler annotation) { + for (Class exceptionClass : annotation.value()) { + hints.registerType(exceptionClass); + } + } + protected boolean couldBePayload(MethodParameter methodParameter) { return !methodParameter.hasParameterAnnotation(DestinationVariable.class) && !methodParameter.hasParameterAnnotation(Header.class) && diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/annotation/SubscribeMapping.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/annotation/SubscribeMapping.java index 1714115f24..bfa891a1f2 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/annotation/SubscribeMapping.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/annotation/SubscribeMapping.java @@ -22,6 +22,9 @@ 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.handler.annotation.MessageMappingReflectiveProcessor; + /** * Annotation for mapping subscription messages onto specific handler methods based * on the destination of a subscription. Supported with STOMP over WebSocket only @@ -54,6 +57,7 @@ import java.lang.annotation.Target; @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented +@Reflective(MessageMappingReflectiveProcessor.class) public @interface SubscribeMapping { /** 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 index fbb6d72d66..bc3d435c70 100644 --- 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 @@ -16,16 +16,19 @@ package org.springframework.messaging.handler.annotation; +import java.io.IOException; 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.RuntimeHints; import org.springframework.aot.hint.TypeReference; +import org.springframework.aot.hint.predicate.RuntimeHintsPredicates; import org.springframework.messaging.Message; import org.springframework.messaging.MessageHeaders; +import org.springframework.messaging.simp.annotation.SubscribeMapping; import org.springframework.messaging.support.MessageHeaderAccessor; import static org.assertj.core.api.Assertions.assertThat; @@ -39,13 +42,13 @@ public class MessageMappingReflectiveProcessorTests { private final MessageMappingReflectiveProcessor processor = new MessageMappingReflectiveProcessor(); - private final ReflectionHints hints = new ReflectionHints(); + private final RuntimeHints hints = new RuntimeHints(); @Test void registerReflectiveHintsForMethodWithReturnValue() throws NoSuchMethodException { Method method = SampleController.class.getDeclaredMethod("returnValue"); - processor.registerReflectionHints(hints, method); - assertThat(hints.typeHints()).satisfiesExactlyInAnyOrder( + processor.registerReflectionHints(hints.reflection(), method); + assertThat(hints.reflection().typeHints()).satisfiesExactlyInAnyOrder( typeHint -> assertThat(typeHint.getType()).isEqualTo(TypeReference.of(SampleController.class)), typeHint -> { assertThat(typeHint.getType()).isEqualTo(TypeReference.of(OutgoingMessage.class)); @@ -62,8 +65,8 @@ public class MessageMappingReflectiveProcessorTests { @Test void registerReflectiveHintsForMethodWithExplicitPayload() throws NoSuchMethodException { Method method = SampleController.class.getDeclaredMethod("explicitPayload", IncomingMessage.class); - processor.registerReflectionHints(hints, method); - assertThat(hints.typeHints()).satisfiesExactlyInAnyOrder( + processor.registerReflectionHints(hints.reflection(), method); + assertThat(hints.reflection().typeHints()).satisfiesExactlyInAnyOrder( typeHint -> assertThat(typeHint.getType()).isEqualTo(TypeReference.of(SampleController.class)), typeHint -> { assertThat(typeHint.getType()).isEqualTo(TypeReference.of(IncomingMessage.class)); @@ -80,8 +83,8 @@ public class MessageMappingReflectiveProcessorTests { @Test void registerReflectiveHintsForMethodWithImplicitPayload() throws NoSuchMethodException { Method method = SampleController.class.getDeclaredMethod("implicitPayload", IncomingMessage.class); - processor.registerReflectionHints(hints, method); - assertThat(hints.typeHints()).satisfiesExactlyInAnyOrder( + processor.registerReflectionHints(hints.reflection(), method); + assertThat(hints.reflection().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))); @@ -90,8 +93,8 @@ public class MessageMappingReflectiveProcessorTests { @Test void registerReflectiveHintsForMethodWithMessage() throws NoSuchMethodException { Method method = SampleController.class.getDeclaredMethod("message", Message.class); - processor.registerReflectionHints(hints, method); - assertThat(hints.typeHints()).satisfiesExactlyInAnyOrder( + processor.registerReflectionHints(hints.reflection(), method); + assertThat(hints.reflection().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))); @@ -102,8 +105,8 @@ public class MessageMappingReflectiveProcessorTests { 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( + processor.registerReflectionHints(hints.reflection(), method); + assertThat(hints.reflection().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))); @@ -111,11 +114,26 @@ public class MessageMappingReflectiveProcessorTests { @Test void registerReflectiveHintsForClass() { - processor.registerReflectionHints(hints, SampleAnnotatedController.class); - assertThat(hints.typeHints()).singleElement().satisfies( + processor.registerReflectionHints(hints.reflection(), SampleAnnotatedController.class); + assertThat(hints.reflection().typeHints()).singleElement().satisfies( typeHint -> assertThat(typeHint.getType()).isEqualTo(TypeReference.of(SampleAnnotatedController.class))); } + @Test + void registerReflectiveHintsForMethodWithSubscribeMapping() throws NoSuchMethodException { + Method method = SampleController.class.getDeclaredMethod("handleSubscribe"); + processor.registerReflectionHints(hints.reflection(), method); + assertThat(RuntimeHintsPredicates.reflection().onMethod(SampleController.class, "handleSubscribe")).accepts(hints); + } + + @Test + void registerReflectiveHintsForMethodWithMessageExceptionHandler() throws NoSuchMethodException { + Method method = SampleController.class.getDeclaredMethod("handleIOException"); + processor.registerReflectionHints(hints.reflection(), method); + assertThat(RuntimeHintsPredicates.reflection().onMethod(SampleController.class, "handleIOException")).accepts(hints); + assertThat(RuntimeHintsPredicates.reflection().onType(IOException.class)).accepts(hints); + } + static class SampleController { @@ -145,6 +163,16 @@ public class MessageMappingReflectiveProcessorTests { MessageHeaderAccessor messageHeaderAccessor, Principal principal) { } + + @SubscribeMapping("/foo") + public String handleSubscribe() { + return "bar"; + } + + @MessageExceptionHandler(IOException.class) + public void handleIOException() { + } + } @MessageMapping