001/* 002 * Copyright 2002-2019 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * https://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.springframework.messaging.simp.annotation.support; 018 019import java.lang.reflect.Method; 020import java.util.ArrayList; 021import java.util.Collection; 022import java.util.Comparator; 023import java.util.LinkedHashSet; 024import java.util.List; 025import java.util.Map; 026import java.util.Set; 027 028import org.apache.commons.logging.Log; 029 030import org.springframework.beans.factory.config.ConfigurableBeanFactory; 031import org.springframework.context.ApplicationContext; 032import org.springframework.context.ConfigurableApplicationContext; 033import org.springframework.context.EmbeddedValueResolverAware; 034import org.springframework.context.SmartLifecycle; 035import org.springframework.core.annotation.AnnotatedElementUtils; 036import org.springframework.core.convert.ConversionService; 037import org.springframework.format.support.DefaultFormattingConversionService; 038import org.springframework.lang.Nullable; 039import org.springframework.messaging.Message; 040import org.springframework.messaging.MessageChannel; 041import org.springframework.messaging.SubscribableChannel; 042import org.springframework.messaging.converter.ByteArrayMessageConverter; 043import org.springframework.messaging.converter.CompositeMessageConverter; 044import org.springframework.messaging.converter.MessageConverter; 045import org.springframework.messaging.converter.StringMessageConverter; 046import org.springframework.messaging.core.AbstractMessageSendingTemplate; 047import org.springframework.messaging.handler.DestinationPatternsMessageCondition; 048import org.springframework.messaging.handler.HandlerMethod; 049import org.springframework.messaging.handler.annotation.MessageMapping; 050import org.springframework.messaging.handler.annotation.support.AnnotationExceptionHandlerMethodResolver; 051import org.springframework.messaging.handler.annotation.support.DestinationVariableMethodArgumentResolver; 052import org.springframework.messaging.handler.annotation.support.HeaderMethodArgumentResolver; 053import org.springframework.messaging.handler.annotation.support.HeadersMethodArgumentResolver; 054import org.springframework.messaging.handler.annotation.support.MessageMethodArgumentResolver; 055import org.springframework.messaging.handler.annotation.support.PayloadMethodArgumentResolver; 056import org.springframework.messaging.handler.invocation.AbstractExceptionHandlerMethodResolver; 057import org.springframework.messaging.handler.invocation.AbstractMethodMessageHandler; 058import org.springframework.messaging.handler.invocation.CompletableFutureReturnValueHandler; 059import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver; 060import org.springframework.messaging.handler.invocation.HandlerMethodReturnValueHandler; 061import org.springframework.messaging.handler.invocation.HandlerMethodReturnValueHandlerComposite; 062import org.springframework.messaging.handler.invocation.ListenableFutureReturnValueHandler; 063import org.springframework.messaging.handler.invocation.ReactiveReturnValueHandler; 064import org.springframework.messaging.simp.SimpAttributesContextHolder; 065import org.springframework.messaging.simp.SimpLogging; 066import org.springframework.messaging.simp.SimpMessageHeaderAccessor; 067import org.springframework.messaging.simp.SimpMessageMappingInfo; 068import org.springframework.messaging.simp.SimpMessageSendingOperations; 069import org.springframework.messaging.simp.SimpMessageTypeMessageCondition; 070import org.springframework.messaging.simp.SimpMessagingTemplate; 071import org.springframework.messaging.simp.annotation.SubscribeMapping; 072import org.springframework.messaging.support.MessageHeaderAccessor; 073import org.springframework.messaging.support.MessageHeaderInitializer; 074import org.springframework.stereotype.Controller; 075import org.springframework.util.AntPathMatcher; 076import org.springframework.util.Assert; 077import org.springframework.util.ClassUtils; 078import org.springframework.util.CollectionUtils; 079import org.springframework.util.PathMatcher; 080import org.springframework.util.StringValueResolver; 081import org.springframework.validation.Validator; 082 083/** 084 * A handler for messages delegating to {@link MessageMapping @MessageMapping} 085 * and {@link SubscribeMapping @SubscribeMapping} annotated methods. 086 * 087 * <p>Supports Ant-style path patterns with template variables. 088 * 089 * @author Rossen Stoyanchev 090 * @author Brian Clozel 091 * @author Juergen Hoeller 092 * @since 4.0 093 */ 094public class SimpAnnotationMethodMessageHandler extends AbstractMethodMessageHandler<SimpMessageMappingInfo> 095 implements EmbeddedValueResolverAware, SmartLifecycle { 096 097 private static final boolean reactorPresent = ClassUtils.isPresent( 098 "reactor.core.publisher.Flux", SimpAnnotationMethodMessageHandler.class.getClassLoader()); 099 100 101 private final SubscribableChannel clientInboundChannel; 102 103 private final SimpMessageSendingOperations clientMessagingTemplate; 104 105 private final SimpMessageSendingOperations brokerTemplate; 106 107 private MessageConverter messageConverter; 108 109 private ConversionService conversionService = new DefaultFormattingConversionService(); 110 111 private PathMatcher pathMatcher = new AntPathMatcher(); 112 113 private boolean slashPathSeparator = true; 114 115 @Nullable 116 private Validator validator; 117 118 @Nullable 119 private StringValueResolver valueResolver; 120 121 @Nullable 122 private MessageHeaderInitializer headerInitializer; 123 124 private volatile boolean running = false; 125 126 private final Object lifecycleMonitor = new Object(); 127 128 129 /** 130 * Create an instance of SimpAnnotationMethodMessageHandler with the given 131 * message channels and broker messaging template. 132 * @param clientInboundChannel the channel for receiving messages from clients (e.g. WebSocket clients) 133 * @param clientOutboundChannel the channel for messages to clients (e.g. WebSocket clients) 134 * @param brokerTemplate a messaging template to send application messages to the broker 135 */ 136 public SimpAnnotationMethodMessageHandler(SubscribableChannel clientInboundChannel, 137 MessageChannel clientOutboundChannel, SimpMessageSendingOperations brokerTemplate) { 138 139 Assert.notNull(clientInboundChannel, "clientInboundChannel must not be null"); 140 Assert.notNull(clientOutboundChannel, "clientOutboundChannel must not be null"); 141 Assert.notNull(brokerTemplate, "brokerTemplate must not be null"); 142 143 this.clientInboundChannel = clientInboundChannel; 144 this.clientMessagingTemplate = new SimpMessagingTemplate(clientOutboundChannel); 145 this.brokerTemplate = brokerTemplate; 146 147 Collection<MessageConverter> converters = new ArrayList<>(); 148 converters.add(new StringMessageConverter()); 149 converters.add(new ByteArrayMessageConverter()); 150 this.messageConverter = new CompositeMessageConverter(converters); 151 } 152 153 154 /** 155 * {@inheritDoc} 156 * <p>Destination prefixes are expected to be slash-separated Strings and 157 * therefore a slash is automatically appended where missing to ensure a 158 * proper prefix-based match (i.e. matching complete segments). 159 * <p>Note however that the remaining portion of a destination after the 160 * prefix may use a different separator (e.g. commonly "." in messaging) 161 * depending on the configured {@code PathMatcher}. 162 */ 163 @Override 164 public void setDestinationPrefixes(@Nullable Collection<String> prefixes) { 165 super.setDestinationPrefixes(appendSlashes(prefixes)); 166 } 167 168 @Nullable 169 private static Collection<String> appendSlashes(@Nullable Collection<String> prefixes) { 170 if (CollectionUtils.isEmpty(prefixes)) { 171 return prefixes; 172 } 173 Collection<String> result = new ArrayList<>(prefixes.size()); 174 for (String prefix : prefixes) { 175 if (!prefix.endsWith("/")) { 176 prefix = prefix + "/"; 177 } 178 result.add(prefix); 179 } 180 return result; 181 } 182 183 /** 184 * Configure a {@link MessageConverter} to use to convert the payload of a message from 185 * its serialized form with a specific MIME type to an Object matching the target method 186 * parameter. The converter is also used when sending a message to the message broker. 187 * @see CompositeMessageConverter 188 */ 189 public void setMessageConverter(MessageConverter converter) { 190 this.messageConverter = converter; 191 ((AbstractMessageSendingTemplate<?>) this.clientMessagingTemplate).setMessageConverter(converter); 192 } 193 194 /** 195 * Return the configured {@link MessageConverter}. 196 */ 197 public MessageConverter getMessageConverter() { 198 return this.messageConverter; 199 } 200 201 /** 202 * Configure a {@link ConversionService} to use when resolving method arguments, 203 * for example message header values. 204 * <p>By default, {@link DefaultFormattingConversionService} is used. 205 */ 206 public void setConversionService(ConversionService conversionService) { 207 this.conversionService = conversionService; 208 } 209 210 /** 211 * Return the configured {@link ConversionService}. 212 */ 213 public ConversionService getConversionService() { 214 return this.conversionService; 215 } 216 217 /** 218 * Set the PathMatcher implementation to use for matching destinations 219 * against configured destination patterns. 220 * <p>By default, {@link AntPathMatcher} is used. 221 */ 222 public void setPathMatcher(PathMatcher pathMatcher) { 223 Assert.notNull(pathMatcher, "PathMatcher must not be null"); 224 this.pathMatcher = pathMatcher; 225 this.slashPathSeparator = this.pathMatcher.combine("a", "a").equals("a/a"); 226 } 227 228 /** 229 * Return the PathMatcher implementation to use for matching destinations. 230 */ 231 public PathMatcher getPathMatcher() { 232 return this.pathMatcher; 233 } 234 235 /** 236 * Return the configured Validator instance. 237 */ 238 @Nullable 239 public Validator getValidator() { 240 return this.validator; 241 } 242 243 /** 244 * Set the Validator instance used for validating {@code @Payload} arguments. 245 * @see org.springframework.validation.annotation.Validated 246 * @see PayloadMethodArgumentResolver 247 */ 248 public void setValidator(@Nullable Validator validator) { 249 this.validator = validator; 250 } 251 252 @Override 253 public void setEmbeddedValueResolver(StringValueResolver resolver) { 254 this.valueResolver = resolver; 255 } 256 257 /** 258 * Configure a {@link MessageHeaderInitializer} to pass on to 259 * {@link HandlerMethodReturnValueHandler HandlerMethodReturnValueHandlers} 260 * that send messages from controller return values. 261 * <p>By default, this property is not set. 262 */ 263 public void setHeaderInitializer(@Nullable MessageHeaderInitializer headerInitializer) { 264 this.headerInitializer = headerInitializer; 265 } 266 267 /** 268 * Return the configured header initializer. 269 */ 270 @Nullable 271 public MessageHeaderInitializer getHeaderInitializer() { 272 return this.headerInitializer; 273 } 274 275 276 @Override 277 public final void start() { 278 synchronized (this.lifecycleMonitor) { 279 this.clientInboundChannel.subscribe(this); 280 this.running = true; 281 } 282 } 283 284 @Override 285 public final void stop() { 286 synchronized (this.lifecycleMonitor) { 287 this.running = false; 288 this.clientInboundChannel.unsubscribe(this); 289 } 290 } 291 292 @Override 293 public final void stop(Runnable callback) { 294 synchronized (this.lifecycleMonitor) { 295 stop(); 296 callback.run(); 297 } 298 } 299 300 @Override 301 public final boolean isRunning() { 302 return this.running; 303 } 304 305 306 @Override 307 protected List<HandlerMethodArgumentResolver> initArgumentResolvers() { 308 ApplicationContext context = getApplicationContext(); 309 ConfigurableBeanFactory beanFactory = (context instanceof ConfigurableApplicationContext ? 310 ((ConfigurableApplicationContext) context).getBeanFactory() : null); 311 312 List<HandlerMethodArgumentResolver> resolvers = new ArrayList<>(); 313 314 // Annotation-based argument resolution 315 resolvers.add(new HeaderMethodArgumentResolver(this.conversionService, beanFactory)); 316 resolvers.add(new HeadersMethodArgumentResolver()); 317 resolvers.add(new DestinationVariableMethodArgumentResolver(this.conversionService)); 318 319 // Type-based argument resolution 320 resolvers.add(new PrincipalMethodArgumentResolver()); 321 resolvers.add(new MessageMethodArgumentResolver(this.messageConverter)); 322 323 resolvers.addAll(getCustomArgumentResolvers()); 324 resolvers.add(new PayloadMethodArgumentResolver(this.messageConverter, this.validator)); 325 326 return resolvers; 327 } 328 329 @Override 330 protected List<? extends HandlerMethodReturnValueHandler> initReturnValueHandlers() { 331 List<HandlerMethodReturnValueHandler> handlers = new ArrayList<>(); 332 333 // Single-purpose return value types 334 335 handlers.add(new ListenableFutureReturnValueHandler()); 336 handlers.add(new CompletableFutureReturnValueHandler()); 337 if (reactorPresent) { 338 handlers.add(new ReactiveReturnValueHandler()); 339 } 340 341 // Annotation-based return value types 342 343 SendToMethodReturnValueHandler sendToHandler = 344 new SendToMethodReturnValueHandler(this.brokerTemplate, true); 345 sendToHandler.setHeaderInitializer(this.headerInitializer); 346 handlers.add(sendToHandler); 347 348 SubscriptionMethodReturnValueHandler subscriptionHandler = 349 new SubscriptionMethodReturnValueHandler(this.clientMessagingTemplate); 350 subscriptionHandler.setHeaderInitializer(this.headerInitializer); 351 handlers.add(subscriptionHandler); 352 353 // Custom return value types 354 355 handlers.addAll(getCustomReturnValueHandlers()); 356 357 // Catch-all 358 359 sendToHandler = new SendToMethodReturnValueHandler(this.brokerTemplate, false); 360 sendToHandler.setHeaderInitializer(this.headerInitializer); 361 handlers.add(sendToHandler); 362 363 return handlers; 364 } 365 366 @Override 367 protected Log getReturnValueHandlerLogger() { 368 return SimpLogging.forLog(HandlerMethodReturnValueHandlerComposite.defaultLogger); 369 } 370 371 @Override 372 protected Log getHandlerMethodLogger() { 373 return SimpLogging.forLog(HandlerMethod.defaultLogger); 374 } 375 376 377 @Override 378 protected boolean isHandler(Class<?> beanType) { 379 return AnnotatedElementUtils.hasAnnotation(beanType, Controller.class); 380 } 381 382 @Override 383 @Nullable 384 protected SimpMessageMappingInfo getMappingForMethod(Method method, Class<?> handlerType) { 385 MessageMapping messageAnn = AnnotatedElementUtils.findMergedAnnotation(method, MessageMapping.class); 386 if (messageAnn != null) { 387 MessageMapping typeAnn = AnnotatedElementUtils.findMergedAnnotation(handlerType, MessageMapping.class); 388 // Only actually register it if there are destinations specified; 389 // otherwise @MessageMapping is just being used as a (meta-annotation) marker. 390 if (messageAnn.value().length > 0 || (typeAnn != null && typeAnn.value().length > 0)) { 391 SimpMessageMappingInfo result = createMessageMappingCondition(messageAnn.value()); 392 if (typeAnn != null) { 393 result = createMessageMappingCondition(typeAnn.value()).combine(result); 394 } 395 return result; 396 } 397 } 398 399 SubscribeMapping subscribeAnn = AnnotatedElementUtils.findMergedAnnotation(method, SubscribeMapping.class); 400 if (subscribeAnn != null) { 401 MessageMapping typeAnn = AnnotatedElementUtils.findMergedAnnotation(handlerType, MessageMapping.class); 402 // Only actually register it if there are destinations specified; 403 // otherwise @SubscribeMapping is just being used as a (meta-annotation) marker. 404 if (subscribeAnn.value().length > 0 || (typeAnn != null && typeAnn.value().length > 0)) { 405 SimpMessageMappingInfo result = createSubscribeMappingCondition(subscribeAnn.value()); 406 if (typeAnn != null) { 407 result = createMessageMappingCondition(typeAnn.value()).combine(result); 408 } 409 return result; 410 } 411 } 412 413 return null; 414 } 415 416 private SimpMessageMappingInfo createMessageMappingCondition(String[] destinations) { 417 String[] resolvedDestinations = resolveEmbeddedValuesInDestinations(destinations); 418 return new SimpMessageMappingInfo(SimpMessageTypeMessageCondition.MESSAGE, 419 new DestinationPatternsMessageCondition(resolvedDestinations, this.pathMatcher)); 420 } 421 422 private SimpMessageMappingInfo createSubscribeMappingCondition(String[] destinations) { 423 String[] resolvedDestinations = resolveEmbeddedValuesInDestinations(destinations); 424 return new SimpMessageMappingInfo(SimpMessageTypeMessageCondition.SUBSCRIBE, 425 new DestinationPatternsMessageCondition(resolvedDestinations, this.pathMatcher)); 426 } 427 428 /** 429 * Resolve placeholder values in the given array of destinations. 430 * @return a new array with updated destinations 431 * @since 4.2 432 */ 433 protected String[] resolveEmbeddedValuesInDestinations(String[] destinations) { 434 if (this.valueResolver == null) { 435 return destinations; 436 } 437 String[] result = new String[destinations.length]; 438 for (int i = 0; i < destinations.length; i++) { 439 result[i] = this.valueResolver.resolveStringValue(destinations[i]); 440 } 441 return result; 442 } 443 444 @Override 445 protected Set<String> getDirectLookupDestinations(SimpMessageMappingInfo mapping) { 446 Set<String> result = new LinkedHashSet<>(); 447 for (String pattern : mapping.getDestinationConditions().getPatterns()) { 448 if (!this.pathMatcher.isPattern(pattern)) { 449 result.add(pattern); 450 } 451 } 452 return result; 453 } 454 455 @Override 456 @Nullable 457 protected String getDestination(Message<?> message) { 458 return SimpMessageHeaderAccessor.getDestination(message.getHeaders()); 459 } 460 461 @Override 462 protected String getLookupDestination(@Nullable String destination) { 463 if (destination == null) { 464 return null; 465 } 466 if (CollectionUtils.isEmpty(getDestinationPrefixes())) { 467 return destination; 468 } 469 for (String prefix : getDestinationPrefixes()) { 470 if (destination.startsWith(prefix)) { 471 if (this.slashPathSeparator) { 472 return destination.substring(prefix.length() - 1); 473 } 474 else { 475 return destination.substring(prefix.length()); 476 } 477 } 478 } 479 return null; 480 } 481 482 @Override 483 @Nullable 484 protected SimpMessageMappingInfo getMatchingMapping(SimpMessageMappingInfo mapping, Message<?> message) { 485 return mapping.getMatchingCondition(message); 486 487 } 488 489 @Override 490 protected Comparator<SimpMessageMappingInfo> getMappingComparator(final Message<?> message) { 491 return (info1, info2) -> info1.compareTo(info2, message); 492 } 493 494 @Override 495 protected void handleMatch(SimpMessageMappingInfo mapping, HandlerMethod handlerMethod, 496 String lookupDestination, Message<?> message) { 497 498 Set<String> patterns = mapping.getDestinationConditions().getPatterns(); 499 if (!CollectionUtils.isEmpty(patterns)) { 500 String pattern = patterns.iterator().next(); 501 Map<String, String> vars = getPathMatcher().extractUriTemplateVariables(pattern, lookupDestination); 502 if (!CollectionUtils.isEmpty(vars)) { 503 MessageHeaderAccessor mha = MessageHeaderAccessor.getAccessor(message, MessageHeaderAccessor.class); 504 Assert.state(mha != null && mha.isMutable(), "Mutable MessageHeaderAccessor required"); 505 mha.setHeader(DestinationVariableMethodArgumentResolver.DESTINATION_TEMPLATE_VARIABLES_HEADER, vars); 506 } 507 } 508 509 try { 510 SimpAttributesContextHolder.setAttributesFromMessage(message); 511 super.handleMatch(mapping, handlerMethod, lookupDestination, message); 512 } 513 finally { 514 SimpAttributesContextHolder.resetAttributes(); 515 } 516 } 517 518 @Override 519 protected AbstractExceptionHandlerMethodResolver createExceptionHandlerMethodResolverFor(Class<?> beanType) { 520 return new AnnotationExceptionHandlerMethodResolver(beanType); 521 } 522 523}