Refactor HandlerMethod support in spring-messaging

Introduce base class AbstractMethodMessageHandler for
HandlerMethod-based message handling.

Add MessageCondition interface for mapping conditions to messages
with support for combining type- and method-level annotation
conditions, the ability to match conditions to messages, and also
comparing matches to select the best match.

Issue: SPR-11024
This commit is contained in:
Rossen Stoyanchev
2013-10-24 21:50:49 -04:00
parent 4892a27016
commit b8809daf5f
28 changed files with 1945 additions and 786 deletions

View File

@@ -30,29 +30,29 @@ import static org.junit.Assert.*;
/**
* Test fixture for {@link ExceptionHandlerMethodResolver} tests.
* Test fixture for {@link AnnotationExceptionHandlerMethodResolver} tests.
*
* @author Rossen Stoyanchev
*/
public class ExceptionHandlerMethodResolverTests {
public class AnnotationExceptionHandlerMethodResolverTests {
@Test
public void resolveMethodFromAnnotation() {
ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(ExceptionController.class);
AnnotationExceptionHandlerMethodResolver resolver = new AnnotationExceptionHandlerMethodResolver(ExceptionController.class);
IOException exception = new IOException();
assertEquals("handleIOException", resolver.resolveMethod(exception).getName());
}
@Test
public void resolveMethodFromArgument() {
ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(ExceptionController.class);
AnnotationExceptionHandlerMethodResolver resolver = new AnnotationExceptionHandlerMethodResolver(ExceptionController.class);
IllegalArgumentException exception = new IllegalArgumentException();
assertEquals("handleIllegalArgumentException", resolver.resolveMethod(exception).getName());
}
@Test
public void resolveMethodExceptionSubType() {
ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(ExceptionController.class);
AnnotationExceptionHandlerMethodResolver resolver = new AnnotationExceptionHandlerMethodResolver(ExceptionController.class);
IOException ioException = new FileNotFoundException();
assertEquals("handleIOException", resolver.resolveMethod(ioException).getName());
SocketException bindException = new BindException();
@@ -61,14 +61,14 @@ public class ExceptionHandlerMethodResolverTests {
@Test
public void resolveMethodBestMatch() {
ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(ExceptionController.class);
AnnotationExceptionHandlerMethodResolver resolver = new AnnotationExceptionHandlerMethodResolver(ExceptionController.class);
SocketException exception = new SocketException();
assertEquals("handleSocketException", resolver.resolveMethod(exception).getName());
}
@Test
public void resolveMethodNoMatch() {
ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(ExceptionController.class);
AnnotationExceptionHandlerMethodResolver resolver = new AnnotationExceptionHandlerMethodResolver(ExceptionController.class);
Exception exception = new Exception();
assertNull("1st lookup", resolver.resolveMethod(exception));
assertNull("2nd lookup from cache", resolver.resolveMethod(exception));
@@ -76,19 +76,19 @@ public class ExceptionHandlerMethodResolverTests {
@Test
public void resolveMethodInherited() {
ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(InheritedController.class);
AnnotationExceptionHandlerMethodResolver resolver = new AnnotationExceptionHandlerMethodResolver(InheritedController.class);
IOException exception = new IOException();
assertEquals("handleIOException", resolver.resolveMethod(exception).getName());
}
@Test(expected = IllegalStateException.class)
public void ambiguousExceptionMapping() {
new ExceptionHandlerMethodResolver(AmbiguousController.class);
new AnnotationExceptionHandlerMethodResolver(AmbiguousController.class);
}
@Test(expected = IllegalArgumentException.class)
public void noExceptionMapping() {
new ExceptionHandlerMethodResolver(NoExceptionController.class);
new AnnotationExceptionHandlerMethodResolver(NoExceptionController.class);
}
@Controller

View File

@@ -28,7 +28,7 @@ import org.springframework.core.MethodParameter;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.messaging.Message;
import org.springframework.messaging.handler.annotation.PathVariable;
import org.springframework.messaging.simp.handler.AnnotationMethodMessageHandler;
import org.springframework.messaging.simp.handler.SimpAnnotationMethodMessageHandler;
import org.springframework.messaging.support.MessageBuilder;
import static org.junit.Assert.*;
@@ -74,7 +74,7 @@ public class PathVariableMethodArgumentResolverTests {
pathParams.put("foo","bar");
pathParams.put("name","value");
Message<byte[]> message = MessageBuilder.withPayload(new byte[0])
.setHeader(AnnotationMethodMessageHandler.PATH_TEMPLATE_VARIABLES_HEADER, pathParams).build();
.setHeader(PathVariableMethodArgumentResolver.PATH_TEMPLATE_VARIABLES_HEADER, pathParams).build();
Object result = this.resolver.resolveArgument(this.paramAnnotated, message);
assertEquals("bar",result);
result = this.resolver.resolveArgument(this.paramAnnotatedValue, message);

View File

@@ -0,0 +1,142 @@
/*
* Copyright 2002-2012 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.condition;
import org.junit.Test;
import org.springframework.messaging.Message;
import org.springframework.messaging.handler.method.AbstractMethodMessageHandler;
import org.springframework.messaging.support.MessageBuilder;
import static org.junit.Assert.*;
/**
* Unit tests for DestinationPatternsMessageCondition.
*
* @author Rossen Stoyanchev
*/
public class DestinationPatternsMessageConditionTests {
@Test
public void prependSlash() {
DestinationPatternsMessageCondition c = condition("foo");
assertEquals("/foo", c.getPatterns().iterator().next());
}
// SPR-8255
@Test
public void prependNonEmptyPatternsOnly() {
DestinationPatternsMessageCondition c = condition("");
assertEquals("", c.getPatterns().iterator().next());
}
@Test
public void combineEmptySets() {
DestinationPatternsMessageCondition c1 = condition();
DestinationPatternsMessageCondition c2 = condition();
assertEquals(condition(""), c1.combine(c2));
}
@Test
public void combineOnePatternWithEmptySet() {
DestinationPatternsMessageCondition c1 = condition("/type1", "/type2");
DestinationPatternsMessageCondition c2 = condition();
assertEquals(condition("/type1", "/type2"), c1.combine(c2));
c1 = condition();
c2 = condition("/method1", "/method2");
assertEquals(condition("/method1", "/method2"), c1.combine(c2));
}
@Test
public void combineMultiplePatterns() {
DestinationPatternsMessageCondition c1 = condition("/t1", "/t2");
DestinationPatternsMessageCondition c2 = condition("/m1", "/m2");
assertEquals(new DestinationPatternsMessageCondition(
"/t1/m1", "/t1/m2", "/t2/m1", "/t2/m2"), c1.combine(c2));
}
@Test
public void matchDirectPath() {
DestinationPatternsMessageCondition condition = condition("/foo");
DestinationPatternsMessageCondition match = condition.getMatchingCondition(messageTo("/foo"));
assertNotNull(match);
}
@Test
public void matchPattern() {
DestinationPatternsMessageCondition condition = condition("/foo/*");
DestinationPatternsMessageCondition match = condition.getMatchingCondition(messageTo("/foo/bar"));
assertNotNull(match);
}
@Test
public void matchSortPatterns() {
DestinationPatternsMessageCondition condition = condition("/**", "/foo/bar", "/foo/*");
DestinationPatternsMessageCondition match = condition.getMatchingCondition(messageTo("/foo/bar"));
DestinationPatternsMessageCondition expected = condition("/foo/bar", "/foo/*", "/**");
assertEquals(expected, match);
}
@Test
public void compareEqualPatterns() {
DestinationPatternsMessageCondition c1 = condition("/foo*");
DestinationPatternsMessageCondition c2 = condition("/foo*");
assertEquals(0, c1.compareTo(c2, messageTo("/foo")));
}
@Test
public void comparePatternSpecificity() {
DestinationPatternsMessageCondition c1 = condition("/fo*");
DestinationPatternsMessageCondition c2 = condition("/foo");
assertEquals(1, c1.compareTo(c2, messageTo("/foo")));
}
@Test
public void compareNumberOfMatchingPatterns() throws Exception {
Message<?> message = messageTo("/foo");
DestinationPatternsMessageCondition c1 = condition("/foo", "bar");
DestinationPatternsMessageCondition c2 = condition("/foo", "f*");
DestinationPatternsMessageCondition match1 = c1.getMatchingCondition(message);
DestinationPatternsMessageCondition match2 = c2.getMatchingCondition(message);
assertEquals(1, match1.compareTo(match2, message));
}
private DestinationPatternsMessageCondition condition(String... patterns) {
return new DestinationPatternsMessageCondition(patterns);
}
private Message<?> messageTo(String destination) {
return MessageBuilder.withPayload(new byte[0]).setHeader(
AbstractMethodMessageHandler.LOOKUP_DESTINATION_HEADER, destination).build();
}
}

View File

@@ -34,7 +34,7 @@ import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.messaging.handler.websocket.SubProtocolWebSocketHandler;
import org.springframework.messaging.simp.SimpMessageType;
import org.springframework.messaging.simp.annotation.SubscribeEvent;
import org.springframework.messaging.simp.handler.AnnotationMethodMessageHandler;
import org.springframework.messaging.simp.handler.SimpAnnotationMethodMessageHandler;
import org.springframework.messaging.simp.handler.MutableUserQueueSuffixResolver;
import org.springframework.messaging.simp.handler.SimpleBrokerMessageHandler;
import org.springframework.messaging.simp.handler.UserDestinationMessageHandler;
@@ -103,7 +103,7 @@ public class WebSocketMessageBrokerConfigurationSupportTests {
List<MessageHandler> values = captor.getAllValues();
assertEquals(3, values.size());
assertTrue(values.contains(cxtSimpleBroker.getBean(AnnotationMethodMessageHandler.class)));
assertTrue(values.contains(cxtSimpleBroker.getBean(SimpAnnotationMethodMessageHandler.class)));
assertTrue(values.contains(cxtSimpleBroker.getBean(UserDestinationMessageHandler.class)));
assertTrue(values.contains(cxtSimpleBroker.getBean(SimpleBrokerMessageHandler.class)));
}
@@ -117,7 +117,7 @@ public class WebSocketMessageBrokerConfigurationSupportTests {
List<MessageHandler> values = captor.getAllValues();
assertEquals(3, values.size());
assertTrue(values.contains(cxtStompBroker.getBean(AnnotationMethodMessageHandler.class)));
assertTrue(values.contains(cxtStompBroker.getBean(SimpAnnotationMethodMessageHandler.class)));
assertTrue(values.contains(cxtStompBroker.getBean(UserDestinationMessageHandler.class)));
assertTrue(values.contains(cxtStompBroker.getBean(StompBrokerRelayMessageHandler.class)));
}
@@ -152,7 +152,7 @@ public class WebSocketMessageBrokerConfigurationSupportTests {
public void webSocketResponseChannelUsedByAnnotatedMethod() {
SubscribableChannel channel = this.cxtSimpleBroker.getBean("webSocketResponseChannel", SubscribableChannel.class);
AnnotationMethodMessageHandler messageHandler = this.cxtSimpleBroker.getBean(AnnotationMethodMessageHandler.class);
SimpAnnotationMethodMessageHandler messageHandler = this.cxtSimpleBroker.getBean(SimpAnnotationMethodMessageHandler.class);
StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.SUBSCRIBE);
headers.setSessionId("sess1");
@@ -235,7 +235,7 @@ public class WebSocketMessageBrokerConfigurationSupportTests {
@Test
public void brokerChannelUsedByAnnotatedMethod() {
SubscribableChannel channel = this.cxtSimpleBroker.getBean("brokerChannel", SubscribableChannel.class);
AnnotationMethodMessageHandler messageHandler = this.cxtSimpleBroker.getBean(AnnotationMethodMessageHandler.class);
SimpAnnotationMethodMessageHandler messageHandler = this.cxtSimpleBroker.getBean(SimpAnnotationMethodMessageHandler.class);
StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.SEND);
headers.setDestination("/foo");

View File

@@ -62,7 +62,7 @@ import static org.springframework.messaging.simp.stomp.StompTextMessageBuilder.*
* @author Rossen Stoyanchev
*/
@RunWith(Parameterized.class)
public class AnnotationMethodIntegrationTests extends AbstractWebSocketIntegrationTests {
public class SimpAnnotationMethodIntegrationTests extends AbstractWebSocketIntegrationTests {
@Parameters
public static Iterable<Object[]> arguments() {
@@ -190,7 +190,7 @@ public class AnnotationMethodIntegrationTests extends AbstractWebSocketIntegrati
}
@Configuration
@ComponentScan(basePackageClasses=AnnotationMethodIntegrationTests.class,
@ComponentScan(basePackageClasses=SimpAnnotationMethodIntegrationTests.class,
useDefaultFilters=false,
includeFilters=@ComponentScan.Filter(IntegrationTestController.class))
static class TestMessageBrokerConfigurer implements WebSocketMessageBrokerConfigurer {

View File

@@ -41,13 +41,13 @@ import static org.junit.Assert.*;
/**
* Test fixture for {@link AnnotationMethodMessageHandler}.
* Test fixture for {@link SimpAnnotationMethodMessageHandler}.
* @author Rossen Stoyanchev
* @author Brian Clozel
*/
public class AnnotationMethodMessageHandlerTests {
public class SimpAnnotationMethodMessageHandlerTests {
private TestAnnotationMethodMessageHandler messageHandler;
private TestSimpAnnotationMethodMessageHandler messageHandler;
private TestController testController;
@@ -56,7 +56,7 @@ public class AnnotationMethodMessageHandlerTests {
public void setup() {
MessageChannel channel = Mockito.mock(MessageChannel.class);
SimpMessageSendingOperations brokerTemplate = new SimpMessagingTemplate(channel);
this.messageHandler = new TestAnnotationMethodMessageHandler(brokerTemplate, channel);
this.messageHandler = new TestSimpAnnotationMethodMessageHandler(brokerTemplate, channel);
this.messageHandler.setApplicationContext(new StaticApplicationContext());
this.messageHandler.afterPropertiesSet();
@@ -69,7 +69,7 @@ public class AnnotationMethodMessageHandlerTests {
@Test
public void headerArgumentResolution() {
SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.create();
headers.setDestination("/headers");
headers.setDestination("/pre/headers");
headers.setHeader("foo", "bar");
Message<?> message = MessageBuilder.withPayload(new byte[0]).setHeaders(headers).build();
this.messageHandler.handleMessage(message);
@@ -87,7 +87,7 @@ public class AnnotationMethodMessageHandlerTests {
@Test
public void messageMappingPathVariableResolution() {
SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.create();
headers.setDestination("/message/bar/value");
headers.setDestination("/pre/message/bar/value");
Message<?> message = MessageBuilder.withPayload(new byte[0]).setHeaders(headers).build();
this.messageHandler.handleMessage(message);
@@ -99,7 +99,7 @@ public class AnnotationMethodMessageHandlerTests {
@Test
public void subscribeEventPathVariableResolution() {
SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.create(SimpMessageType.SUBSCRIBE);
headers.setDestination("/sub/bar/value");
headers.setDestination("/pre/sub/bar/value");
Message<?> message = MessageBuilder.withPayload(new byte[0])
.copyHeaders(headers.toMap()).build();
this.messageHandler.handleMessage(message);
@@ -112,7 +112,7 @@ public class AnnotationMethodMessageHandlerTests {
@Test
public void antPatchMatchWildcard() {
SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.create();
headers.setDestination("/pathmatch/wildcard/test");
headers.setDestination("/pre/pathmatch/wildcard/test");
Message<?> message = MessageBuilder.withPayload(new byte[0]).setHeaders(headers).build();
this.messageHandler.handleMessage(message);
@@ -122,7 +122,7 @@ public class AnnotationMethodMessageHandlerTests {
@Test
public void bestMatchWildcard() {
SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.create();
headers.setDestination("/bestmatch/bar/path");
headers.setDestination("/pre/bestmatch/bar/path");
Message<?> message = MessageBuilder.withPayload(new byte[0]).setHeaders(headers).build();
this.messageHandler.handleMessage(message);
@@ -133,7 +133,7 @@ public class AnnotationMethodMessageHandlerTests {
@Test
public void simpleBinding() {
SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.create();
headers.setDestination("/binding/id/12");
headers.setDestination("/pre/binding/id/12");
Message<?> message = MessageBuilder.withPayload(new byte[0]).setHeaders(headers).build();
this.messageHandler.handleMessage(message);
@@ -142,9 +142,10 @@ public class AnnotationMethodMessageHandlerTests {
assertEquals(12L, this.testController.arguments.get("id"));
}
private static class TestAnnotationMethodMessageHandler extends AnnotationMethodMessageHandler {
public TestAnnotationMethodMessageHandler(SimpMessageSendingOperations brokerTemplate,
private static class TestSimpAnnotationMethodMessageHandler extends SimpAnnotationMethodMessageHandler {
public TestSimpAnnotationMethodMessageHandler(SimpMessageSendingOperations brokerTemplate,
MessageChannel webSocketResponseChannel) {
super(brokerTemplate, webSocketResponseChannel);
@@ -157,6 +158,8 @@ public class AnnotationMethodMessageHandlerTests {
@Controller
@MessageMapping("/pre")
@SubscribeEvent("/pre")
private static class TestController {
private String method;

View File

@@ -0,0 +1,105 @@
/*
* Copyright 2002-2013 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.simp.handler;
import org.junit.Test;
import org.springframework.messaging.Message;
import org.springframework.messaging.handler.condition.DestinationPatternsMessageCondition;
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
import org.springframework.messaging.simp.SimpMessageType;
import org.springframework.messaging.support.MessageBuilder;
import static junit.framework.Assert.assertNotNull;
import static junit.framework.Assert.assertNull;
import static org.junit.Assert.assertEquals;
/**
* Unit tests for SimpMessageTypeMessageCondition.
*
* @author Rossen Stoyanchev
*/
public class SimpMessageTypeMessageConditionTests {
@Test
public void combineEmptySets() {
SimpMessageTypeMessageCondition c1 = condition();
SimpMessageTypeMessageCondition c2 = condition();
assertNull(c1.combine(c2).getMessageType());
}
@Test
public void combine() {
SimpMessageType actual = condition().combine(condition()).getMessageType();
assertNull(actual);
actual = condition().combine(condition(SimpMessageType.SUBSCRIBE)).getMessageType();
assertEquals(SimpMessageType.SUBSCRIBE, actual);
actual = condition(SimpMessageType.SUBSCRIBE).combine(condition()).getMessageType();
assertEquals(SimpMessageType.SUBSCRIBE, actual);
actual = condition(SimpMessageType.SUBSCRIBE).combine(condition(SimpMessageType.SUBSCRIBE)).getMessageType();
assertEquals(SimpMessageType.SUBSCRIBE, actual);
}
@Test
public void getMatchingCondition() {
Message<?> message = message(SimpMessageType.MESSAGE);
SimpMessageTypeMessageCondition condition = condition(SimpMessageType.MESSAGE);
SimpMessageTypeMessageCondition actual = condition.getMatchingCondition(message);
assertNotNull(actual);
assertEquals(SimpMessageType.MESSAGE, actual.getMessageType());
}
@Test
public void getMatchingConditionNoMessageType() {
Message<?> message = message(null);
SimpMessageTypeMessageCondition condition = condition(SimpMessageType.MESSAGE);
assertNull(condition.getMatchingCondition(message));
}
@Test
public void compareTo() {
Message<byte[]> message = message(null);
assertEquals(1, condition().compareTo(condition(SimpMessageType.MESSAGE), message));
assertEquals(-1, condition(SimpMessageType.MESSAGE).compareTo(condition(), message));
assertEquals(0, condition(SimpMessageType.MESSAGE).compareTo(condition(SimpMessageType.MESSAGE), message));
}
private Message<byte[]> message(SimpMessageType messageType) {
MessageBuilder<byte[]> builder = MessageBuilder.withPayload(new byte[0]);
if (messageType != null) {
builder.setHeader(SimpMessageHeaderAccessor.MESSAGE_TYPE_HEADER, messageType);
}
return builder.build();
}
private SimpMessageTypeMessageCondition condition() {
return new SimpMessageTypeMessageCondition();
}
private SimpMessageTypeMessageCondition condition(SimpMessageType messageType) {
return new SimpMessageTypeMessageCondition(messageType);
}
}