From 715a11ce8c46436efb73fbfa53132d1ffc4890de Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Fri, 18 Oct 2013 15:08:13 -0400 Subject: [PATCH] Polish AnnotationMethodMessageHandler and annotations --- .../handler/annotation/MessageMapping.java | 12 +- .../handler/annotation/PathVariable.java | 23 +-- .../PathVariableMethodArgumentResolver.java | 18 ++- .../simp/annotation/SubscribeEvent.java | 6 +- .../AnnotationMethodMessageHandler.java | 140 ++++++++++-------- ...thVariableMethodArgumentResolverTests.java | 17 +-- 6 files changed, 124 insertions(+), 92 deletions(-) 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 aa2a635a7a..a14dac5e0f 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 @@ -26,11 +26,13 @@ import org.springframework.messaging.Message; /** - * Annotation for mapping a {@link Message} onto specific handler methods based on - * the destination for the message. + * Annotation for mapping a {@link Message} onto message handling methods by matching to + * the message destination. * * @author Rossen Stoyanchev * @since 4.0 + * + * @see org.springframework.messaging.simp.handler.AnnotationMethodMessageHandler */ @Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @@ -38,7 +40,11 @@ import org.springframework.messaging.Message; public @interface MessageMapping { /** - * Destination values for the message. + * Destination-based mapping expressed by this annotation. + *

+ * For STOMP over WebSocket messages: this is the destination of the STOMP message + * (e.g. "/positions"). Ant-style path patterns (e.g. "/price.stock.*") are supported + * and so are path template variables (e.g. "/price.stock.{ticker}""). */ String[] value() default {}; diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/PathVariable.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/PathVariable.java index 00b21e9a74..52eb2fbf3c 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/PathVariable.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/PathVariable.java @@ -23,19 +23,17 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** - * Annotation which indicates that a method parameter should be bound to a path template - * variable. Supported for {@link org.springframework.messaging.simp.annotation.SubscribeEvent}, - * {@link org.springframework.messaging.simp.annotation.UnsubscribeEvent}, - * {@link org.springframework.messaging.handler.annotation.MessageMapping} - * annotated handler methods. - * - *

A {@code @PathVariable} template variable is always required and does not have - * a default value to fall back on. + * Annotation that indicates a method parameter should be bound to a path template + * variable. Supported on message handling methods such as {@link MessageMapping + * @MessageMapping} for messages with path-like destination semantics. + *

+ * A {@code @PathVariable} template variable is always required and does not have a + * default value to fall back on. * * @author Brian Clozel - * @see org.springframework.messaging.simp.annotation.SubscribeEvent - * @see org.springframework.messaging.simp.annotation.UnsubscribeEvent * @see org.springframework.messaging.handler.annotation.MessageMapping + * @see org.springframework.messaging.simp.handler.AnnotationMethodMessageHandler + * * @since 4.0 */ @Target(ElementType.PARAMETER) @@ -43,6 +41,9 @@ import java.lang.annotation.Target; @Documented public @interface PathVariable { - /** The path template variable to bind to. */ + /** + * The path template variable to bind to. + */ String value() default ""; + } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/PathVariableMethodArgumentResolver.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/PathVariableMethodArgumentResolver.java index d095211913..d2eca95239 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/PathVariableMethodArgumentResolver.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/PathVariableMethodArgumentResolver.java @@ -16,7 +16,8 @@ package org.springframework.messaging.handler.annotation.support; -import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import java.util.Map; + import org.springframework.core.MethodParameter; import org.springframework.core.convert.ConversionService; import org.springframework.messaging.Message; @@ -24,8 +25,6 @@ import org.springframework.messaging.handler.annotation.PathVariable; import org.springframework.messaging.handler.annotation.ValueConstants; import org.springframework.messaging.simp.handler.AnnotationMethodMessageHandler; -import java.util.Map; - /** * Resolves method parameters annotated with {@link PathVariable @PathVariable}. * @@ -40,8 +39,9 @@ import java.util.Map; */ public class PathVariableMethodArgumentResolver extends AbstractNamedValueMethodArgumentResolver { - public PathVariableMethodArgumentResolver(ConversionService cs, ConfigurableBeanFactory beanFactory) { - super(cs, beanFactory); + + public PathVariableMethodArgumentResolver(ConversionService cs) { + super(cs, null); } @Override @@ -57,9 +57,10 @@ public class PathVariableMethodArgumentResolver extends AbstractNamedValueMethod @Override protected Object resolveArgumentInternal(MethodParameter parameter, Message message, String name) throws Exception { - Map pathTemplateVars = - (Map) message.getHeaders().get(AnnotationMethodMessageHandler.PATH_TEMPLATE_VARIABLES_HEADER); - return (pathTemplateVars != null) ? pathTemplateVars.get(name) : null; + String headerName = AnnotationMethodMessageHandler.PATH_TEMPLATE_VARIABLES_HEADER; + @SuppressWarnings("unchecked") + Map vars = (Map) message.getHeaders().get(headerName); + return (vars != null) ? vars.get(name) : null; } @Override @@ -68,6 +69,7 @@ public class PathVariableMethodArgumentResolver extends AbstractNamedValueMethod "' for method parameter type [" + parameter.getParameterType() + "]"); } + private static class PathVariableNamedValueInfo extends NamedValueInfo { private PathVariableNamedValueInfo(PathVariable annotation) { diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/annotation/SubscribeEvent.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/annotation/SubscribeEvent.java index 7fb829a636..68b8359c51 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/annotation/SubscribeEvent.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/annotation/SubscribeEvent.java @@ -36,7 +36,11 @@ import java.lang.annotation.Target; public @interface SubscribeEvent { /** - * Destination value(s) for the subscription. + * Destination-based mapping expressed by this annotation. + *

+ * For STOMP over WebSocket messages: this is the destination of the STOMP message + * (e.g. "/positions"). Ant-style path patterns (e.g. "/price.stock.*") are supported + * and so are path template variables (e.g. "/price.stock.{ticker}""). */ String[] value() default {}; diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/handler/AnnotationMethodMessageHandler.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/handler/AnnotationMethodMessageHandler.java index 51d846124e..71f5765e38 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/handler/AnnotationMethodMessageHandler.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/handler/AnnotationMethodMessageHandler.java @@ -51,8 +51,8 @@ import org.springframework.messaging.handler.annotation.support.ExceptionHandler import org.springframework.messaging.handler.annotation.support.HeaderMethodArgumentResolver; import org.springframework.messaging.handler.annotation.support.HeadersMethodArgumentResolver; import org.springframework.messaging.handler.annotation.support.MessageMethodArgumentResolver; -import org.springframework.messaging.handler.annotation.support.PayloadArgumentResolver; import org.springframework.messaging.handler.annotation.support.PathVariableMethodArgumentResolver; +import org.springframework.messaging.handler.annotation.support.PayloadArgumentResolver; import org.springframework.messaging.handler.method.HandlerMethod; import org.springframework.messaging.handler.method.HandlerMethodArgumentResolver; import org.springframework.messaging.handler.method.HandlerMethodArgumentResolverComposite; @@ -70,7 +70,6 @@ import org.springframework.messaging.simp.annotation.support.PrincipalMethodArgu import org.springframework.messaging.simp.annotation.support.SendToMethodReturnValueHandler; import org.springframework.messaging.simp.annotation.support.SubscriptionMethodReturnValueHandler; import org.springframework.messaging.support.MessageBuilder; -import org.springframework.messaging.support.MessageHeaderAccessor; import org.springframework.messaging.support.converter.ByteArrayMessageConverter; import org.springframework.messaging.support.converter.CompositeMessageConverter; import org.springframework.messaging.support.converter.MessageConverter; @@ -85,18 +84,24 @@ import org.springframework.util.ReflectionUtils.MethodFilter; /** + * A handler for messages that delegates to {@link SubscribeEvent @SubscribeEvent} and + * {@link MessageMapping @MessageMapping} annotated methods. + * * @author Rossen Stoyanchev * @author Brian Clozel * @since 4.0 */ public class AnnotationMethodMessageHandler implements MessageHandler, ApplicationContextAware, InitializingBean { - public static final String PATH_TEMPLATE_VARIABLES_HEADER = "Spring-PathTemplateVariables"; + public static final String PATH_TEMPLATE_VARIABLES_HEADER = + AnnotationMethodMessageHandler.class.getSimpleName() + ".templateVariables"; - public static final String BEST_MATCHING_PATTERN_HEADER = "Spring-BestMatchingPattern"; + public static final String BEST_MATCHING_PATTERN_HEADER = + AnnotationMethodMessageHandler.class.getSimpleName() + ".bestMatchingPattern"; private static final Log logger = LogFactory.getLog(AnnotationMethodMessageHandler.class); + private final PathMatcher pathMatcher = new AntPathMatcher(); private final SimpMessageSendingOperations brokerTemplate; @@ -255,7 +260,7 @@ public class AnnotationMethodMessageHandler implements MessageHandler, Applicati // Annotation-based argument resolution this.argumentResolvers.addResolver(new HeaderMethodArgumentResolver(this.conversionService, beanFactory)); this.argumentResolvers.addResolver(new HeadersMethodArgumentResolver()); - this.argumentResolvers.addResolver(new PathVariableMethodArgumentResolver(this.conversionService, beanFactory)); + this.argumentResolvers.addResolver(new PathVariableMethodArgumentResolver(this.conversionService)); // Type-based argument resolution this.argumentResolvers.addResolver(new PrincipalMethodArgumentResolver()); @@ -287,6 +292,9 @@ public class AnnotationMethodMessageHandler implements MessageHandler, Applicati } } + /** + * Whether the given bean type should be introspected for messaging handling methods. + */ protected boolean isHandler(Class beanType) { return (AnnotationUtils.findAnnotation(beanType, Controller.class) != null); } @@ -369,32 +377,33 @@ public class AnnotationMethodMessageHandler implements MessageHandler, Applicati } SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.wrap(message); - String lookupPath = getLookupPath(headers.getDestination()); - if (lookupPath == null) { + String destinationToMatch = getDestinationToMatch(headers.getDestination()); + if (destinationToMatch == null) { if (logger.isTraceEnabled()) { - logger.trace("Ignoring message with destination " + headers.getDestination()); + logger.trace("Ignoring message with destination=" + headers.getDestination()); } return; } - MappingInfoMatch match = matchMappingInfo(lookupPath, handlerMethods); + Match match = getMatchingHandlerMethod(destinationToMatch, handlerMethods); if (match == null) { if (logger.isTraceEnabled()) { - logger.trace("No matching method, lookup path " + lookupPath); + logger.trace("No matching handler method for destination=" + destinationToMatch); } return; } - HandlerMethod handlerMethod = match.handlerMethod.createWithResolvedBean(); + String matchedPattern = match.getMatchedPattern(); + HandlerMethod handlerMethod = match.getHandlerMethod().createWithResolvedBean(); InvocableHandlerMethod invocableHandlerMethod = new InvocableHandlerMethod(handlerMethod); invocableHandlerMethod.setMessageMethodArgumentResolvers(this.argumentResolvers); try { - headers.setDestination(lookupPath); - headers.setHeader(BEST_MATCHING_PATTERN_HEADER,match.mappingDestination); - headers.setHeader(PATH_TEMPLATE_VARIABLES_HEADER, - pathMatcher.extractUriTemplateVariables(match.mappingDestination,lookupPath)); + headers.setDestination(destinationToMatch); + headers.setHeader(BEST_MATCHING_PATTERN_HEADER, matchedPattern); + Map vars = this.pathMatcher.extractUriTemplateVariables(matchedPattern, destinationToMatch); + headers.setHeader(PATH_TEMPLATE_VARIABLES_HEADER, vars); message = MessageBuilder.withPayload(message.getPayload()).setHeaders(headers).build(); Object returnValue = invocableHandlerMethod.invoke(message); @@ -413,15 +422,20 @@ public class AnnotationMethodMessageHandler implements MessageHandler, Applicati } } - private String getLookupPath(String destination) { - if (destination != null) { - if (CollectionUtils.isEmpty(this.destinationPrefixes)) { - return destination; - } - for (String prefix : this.destinationPrefixes) { - if (destination.startsWith(prefix)) { - return destination.substring(prefix.length() - 1); - } + /** + * Match the destination against the list the configured destination prefixes, if any, + * and return a destination with the matched prefix removed. + */ + private String getDestinationToMatch(String destination) { + if (destination == null) { + return null; + } + if (CollectionUtils.isEmpty(this.destinationPrefixes)) { + return destination; + } + for (String prefix : this.destinationPrefixes) { + if (destination.startsWith(prefix)) { + return destination.substring(prefix.length() - 1); } } return null; @@ -461,29 +475,31 @@ public class AnnotationMethodMessageHandler implements MessageHandler, Applicati } } - protected MappingInfoMatch matchMappingInfo(String destination, - Map handlerMethods) { - - List matches = new ArrayList(4); + protected Match getMatchingHandlerMethod(String destination, Map handlerMethods) { + List matches = new ArrayList(4); for (MappingInfo key : handlerMethods.keySet()) { - for (String mappingDestination : key.getDestinations()) { - if (this.pathMatcher.match(mappingDestination, destination)) { - matches.add(new MappingInfoMatch(mappingDestination, - handlerMethods.get(key))); + for (String pattern : key.getDestinationPatterns()) { + if (this.pathMatcher.match(pattern, destination)) { + matches.add(new Match(pattern, handlerMethods.get(key))); } } } - if(!matches.isEmpty()) { - Comparator comparator = getMappingInfoMatchComparator(destination, this.pathMatcher); + if (matches.isEmpty()) { + return null; + } + else if (matches.size() == 1) { + return matches.get(0); + } + else { + Comparator comparator = getMatchComparator(destination, this.pathMatcher); Collections.sort(matches, comparator); - if (logger.isTraceEnabled()) { - logger.trace("Found " + matches.size() + " matching mapping(s) for [" + destination + "] : " + matches); + logger.trace("Found " + matches.size() + + " matching mapping(s) for [" + destination + "] : " + matches); } - - MappingInfoMatch bestMatch = matches.get(0); + Match bestMatch = matches.get(0); if (matches.size() > 1) { - MappingInfoMatch secondBestMatch = matches.get(1); + Match secondBestMatch = matches.get(1); if (comparator.compare(bestMatch, secondBestMatch) == 0) { Method m1 = bestMatch.handlerMethod.getMethod(); Method m2 = secondBestMatch.handlerMethod.getMethod(); @@ -494,37 +510,36 @@ public class AnnotationMethodMessageHandler implements MessageHandler, Applicati } return bestMatch; } - return null; } - private Comparator getMappingInfoMatchComparator(String destination, - PathMatcher pathMatcher) { - return new Comparator() { + private Comparator getMatchComparator(final String destination, final PathMatcher pathMatcher) { + return new Comparator() { @Override - public int compare(MappingInfoMatch one, MappingInfoMatch other) { + public int compare(Match one, Match other) { Comparator patternComparator = pathMatcher.getPatternComparator(destination); - return patternComparator.compare(one.mappingDestination,other.mappingDestination); + return patternComparator.compare(one.destinationPattern, other.destinationPattern); } }; } + private static class MappingInfo { - private final String[] destinations; + private final String[] destinationPatterns; - public MappingInfo(String[] destinations) { - Assert.notNull(destinations, "No destinations"); - this.destinations = destinations; + public MappingInfo(String[] destinationPatterns) { + Assert.notNull(destinationPatterns, "No destination patterns"); + this.destinationPatterns = destinationPatterns; } - public String[] getDestinations() { - return this.destinations; + public String[] getDestinationPatterns() { + return this.destinationPatterns; } @Override public int hashCode() { - return Arrays.hashCode(this.destinations); + return Arrays.hashCode(this.destinationPatterns); } @Override @@ -534,25 +549,34 @@ public class AnnotationMethodMessageHandler implements MessageHandler, Applicati } if (o != null && getClass().equals(o.getClass())) { MappingInfo other = (MappingInfo) o; - return Arrays.equals(destinations, other.getDestinations()); + return Arrays.equals(destinationPatterns, other.getDestinationPatterns()); } return false; } @Override public String toString() { - return "[destinations=" + Arrays.toString(this.destinations) + "]"; + return "[destinationPatters=" + Arrays.toString(this.destinationPatterns) + "]"; } } - private static class MappingInfoMatch { + private static class Match { + + private final String destinationPattern; - private final String mappingDestination; private final HandlerMethod handlerMethod; - public MappingInfoMatch(String destination, HandlerMethod handlerMethod) { - this.mappingDestination = destination; + public Match(String destinationPattern, HandlerMethod handlerMethod) { + this.destinationPattern = destinationPattern; this.handlerMethod = handlerMethod; } + + public String getMatchedPattern() { + return this.destinationPattern; + } + + public HandlerMethod getHandlerMethod() { + return this.handlerMethod; + } } } diff --git a/spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/support/PathVariableMethodArgumentResolverTests.java b/spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/support/PathVariableMethodArgumentResolverTests.java index 840b6e39ac..4f18b35cd5 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/support/PathVariableMethodArgumentResolverTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/support/PathVariableMethodArgumentResolverTests.java @@ -16,9 +16,12 @@ package org.springframework.messaging.handler.annotation.support; +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; + import org.junit.Before; import org.junit.Test; -import org.springframework.context.support.GenericApplicationContext; import org.springframework.core.DefaultParameterNameDiscoverer; import org.springframework.core.GenericTypeResolver; import org.springframework.core.MethodParameter; @@ -28,13 +31,7 @@ import org.springframework.messaging.handler.annotation.PathVariable; import org.springframework.messaging.simp.handler.AnnotationMethodMessageHandler; import org.springframework.messaging.support.MessageBuilder; -import java.lang.reflect.Method; -import java.util.HashMap; -import java.util.Map; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; +import static org.junit.Assert.*; /** * Test fixture for {@link PathVariableMethodArgumentResolver} tests. @@ -50,9 +47,7 @@ public class PathVariableMethodArgumentResolverTests { @Before public void setup() throws Exception { - GenericApplicationContext cxt = new GenericApplicationContext(); - cxt.refresh(); - this.resolver = new PathVariableMethodArgumentResolver(new DefaultConversionService(), cxt.getBeanFactory()); + this.resolver = new PathVariableMethodArgumentResolver(new DefaultConversionService()); Method method = getClass().getDeclaredMethod("handleMessage", String.class, String.class, String.class);