From 2aee0d82507b8a7ca63df8cdd6d6502526886dd3 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Thu, 20 Mar 2014 07:47:42 -0700 Subject: [PATCH] Improve MessageMethodArgumentResolver This commit validates that the payload type of the message is assignable to the one declared in the method signature. If that is not the case, a meaningful exception message is thrown with the types mismatch. Prior to this commit, only the Message interface could be defined in the method signature: it is now possible to define a sub-class of Message if necessary which will match as long as the Message parameter is assignable to that type. Issue: SPR-11584 --- .../MessageMethodArgumentResolver.java | 29 +++- .../MethodArgumentTypeMismatchException.java | 34 ++++ .../MessageMethodArgumentResolverTests.java | 154 ++++++++++++++++++ 3 files changed, 215 insertions(+), 2 deletions(-) create mode 100644 spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/MethodArgumentTypeMismatchException.java create mode 100644 spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/support/MessageMethodArgumentResolverTests.java diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/MessageMethodArgumentResolver.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/MessageMethodArgumentResolver.java index c057e46643..a190626ee1 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/MessageMethodArgumentResolver.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/MessageMethodArgumentResolver.java @@ -17,13 +17,16 @@ package org.springframework.messaging.handler.annotation.support; import org.springframework.core.MethodParameter; +import org.springframework.core.ResolvableType; import org.springframework.messaging.Message; import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver; /** - * A {@link HandlerMethodArgumentResolver} for {@link Message} parameters. + * A {@link HandlerMethodArgumentResolver} for {@link Message} parameters. Validates + * that the generic type of the payload matches with the message value. * * @author Rossen Stoyanchev + * @author Stephane Nicoll * @since 4.0 */ public class MessageMethodArgumentResolver implements HandlerMethodArgumentResolver { @@ -31,12 +34,34 @@ public class MessageMethodArgumentResolver implements HandlerMethodArgumentResol @Override public boolean supportsParameter(MethodParameter parameter) { - return parameter.getParameterType().equals(Message.class); + return Message.class.isAssignableFrom(parameter.getParameterType()); } @Override public Object resolveArgument(MethodParameter parameter, Message message) throws Exception { + // Validate the message type is assignable + if (!parameter.getParameterType().isAssignableFrom(message.getClass())) { + throw new MethodArgumentTypeMismatchException(message, + "Could not resolve Message parameter: invalid message type:" + + "expected [" + message.getClass().getName() + "] but got [" + + parameter.getParameterType().getName() + "]"); + } + + // validate that the payload type matches + Class effectivePayloadType = getPayloadType(parameter); + if (effectivePayloadType != null && !effectivePayloadType.isInstance(message.getPayload())) { + throw new MethodArgumentTypeMismatchException(message, + "Could not resolve Message parameter: invalid payload type: " + + "expected [" + effectivePayloadType.getName() + "] but got [" + + message.getPayload().getClass().getName() + "]"); + } return message; } + private Class getPayloadType(MethodParameter parameter) { + ResolvableType resolvableType = ResolvableType + .forType(parameter.getGenericParameterType()).as(Message.class); + return resolvableType.getGeneric(0).resolve(Object.class); + } + } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/MethodArgumentTypeMismatchException.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/MethodArgumentTypeMismatchException.java new file mode 100644 index 0000000000..3a4392003a --- /dev/null +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/MethodArgumentTypeMismatchException.java @@ -0,0 +1,34 @@ +/* + * Copyright 2002-2014 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 + * + * http://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.support; + +import org.springframework.messaging.Message; +import org.springframework.messaging.MessagingException; + +/** + * Exception that indicates that a method argument has not the + * expected type. + * + * @author Stephane Nicoll + * @since 4.0.3 + */ +public class MethodArgumentTypeMismatchException extends MessagingException { + + public MethodArgumentTypeMismatchException(Message message, String description) { + super(message, description); + } +} diff --git a/spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/support/MessageMethodArgumentResolverTests.java b/spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/support/MessageMethodArgumentResolverTests.java new file mode 100644 index 0000000000..6f4addb506 --- /dev/null +++ b/spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/support/MessageMethodArgumentResolverTests.java @@ -0,0 +1,154 @@ +/* + * Copyright 2002-2014 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 + * + * http://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.support; + +import static org.junit.Assert.*; + +import java.lang.reflect.Method; +import java.util.Locale; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import org.springframework.core.MethodParameter; +import org.springframework.messaging.Message; +import org.springframework.messaging.support.ErrorMessage; +import org.springframework.messaging.support.GenericMessage; +import org.springframework.messaging.support.MessageBuilder; + +/** + * + * @author Stephane Nicoll + */ +public class MessageMethodArgumentResolverTests { + + @Rule + public final ExpectedException thrown = ExpectedException.none(); + + private final MessageMethodArgumentResolver resolver = new MessageMethodArgumentResolver(); + + private Method method; + + @Before + public void setup() throws Exception { + method = MessageMethodArgumentResolverTests.class.getDeclaredMethod("handleMessage", + Message.class, Message.class, Message.class, Message.class, ErrorMessage.class); + } + + @Test + public void resolveAnyPayloadType() throws Exception { + Message message = MessageBuilder.withPayload("test").build(); + MethodParameter parameter = new MethodParameter(method, 0); + + assertTrue("Parameter '" + parameter + "' should be supported", resolver.supportsParameter(parameter)); + assertSame(message, resolver.resolveArgument(parameter, message)); + } + + @Test + public void resolvePayloadTypeExactType() throws Exception { + Message message = MessageBuilder.withPayload(123).build(); + MethodParameter parameter = new MethodParameter(method, 1); + + assertTrue("Parameter '" + parameter + "' should be supported", resolver.supportsParameter(parameter)); + assertSame(message, resolver.resolveArgument(parameter, message)); + } + + @Test + public void resolvePayloadTypeSubClass() throws Exception { + Message message = MessageBuilder.withPayload(123).build(); + MethodParameter parameter = new MethodParameter(method, 2); + + assertTrue("Parameter '" + parameter + "' should be supported", resolver.supportsParameter(parameter)); + assertSame(message, resolver.resolveArgument(parameter, message)); + } + + @Test + public void resolveInvalidPayloadType() throws Exception { + Message message = MessageBuilder.withPayload("test").build(); + MethodParameter parameter = new MethodParameter(method, 1); + + assertTrue("Parameter '" + parameter + "' should be supported", resolver.supportsParameter(parameter)); + thrown.expect(MethodArgumentTypeMismatchException.class); + thrown.expectMessage(Integer.class.getName()); + thrown.expectMessage(String.class.getName()); + resolver.resolveArgument(parameter, message); + } + + @Test + public void resolveUpperBoundPayloadType() throws Exception { + Message message = MessageBuilder.withPayload(123).build(); + MethodParameter parameter = new MethodParameter(method, 3); + + assertTrue("Parameter '" + parameter + "' should be supported", resolver.supportsParameter(parameter)); + assertSame(message, resolver.resolveArgument(parameter, message)); + } + + @Test + public void resolveOutOfBoundPayloadType() throws Exception { + Message message = MessageBuilder.withPayload(Locale.getDefault()).build(); + MethodParameter parameter = new MethodParameter(method, 3); + + assertTrue("Parameter '" + parameter + "' should be supported", resolver.supportsParameter(parameter)); + thrown.expect(MethodArgumentTypeMismatchException.class); + thrown.expectMessage(Number.class.getName()); + thrown.expectMessage(Locale.class.getName()); + resolver.resolveArgument(parameter, message); + } + + @Test + public void resolveMessageSubTypeExactMatch() throws Exception { + ErrorMessage message = new ErrorMessage(new UnsupportedOperationException()); + MethodParameter parameter = new MethodParameter(method, 4); + + assertTrue("Parameter '" + parameter + "' should be supported", resolver.supportsParameter(parameter)); + assertSame(message, resolver.resolveArgument(parameter, message)); + } + + @Test + public void resolveMessageSubTypeSubClass() throws Exception { + ErrorMessage message = new ErrorMessage(new UnsupportedOperationException()); + MethodParameter parameter = new MethodParameter(method, 0); + + assertTrue("Parameter '" + parameter + "' should be supported", resolver.supportsParameter(parameter)); + assertSame(message, resolver.resolveArgument(parameter, message)); + } + + @Test + public void resolveWrongMessageType() throws Exception { + Message message = new GenericMessage( + new UnsupportedOperationException()); + MethodParameter parameter = new MethodParameter(method, 4); + + assertTrue("Parameter '" + parameter + "' should be supported", resolver.supportsParameter(parameter)); + thrown.expect(MethodArgumentTypeMismatchException.class); + thrown.expectMessage(ErrorMessage.class.getName()); + thrown.expectMessage(GenericMessage.class.getName()); + assertSame(message, resolver.resolveArgument(parameter, message)); + } + + @SuppressWarnings("unused") + private void handleMessage( + Message wildcardPayload, + Message integerPayload, + Message numberPayload, + Message anyNumberPayload, + ErrorMessage subClass) { + } + +}