001/* 002 * Copyright 2002-2020 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.handler.invocation.reactive; 018 019import java.lang.reflect.Method; 020import java.util.ArrayList; 021import java.util.Arrays; 022import java.util.Collection; 023import java.util.Collections; 024import java.util.Comparator; 025import java.util.LinkedHashMap; 026import java.util.List; 027import java.util.Map; 028import java.util.Set; 029import java.util.function.Function; 030import java.util.function.Predicate; 031import java.util.stream.Collectors; 032 033import org.apache.commons.logging.Log; 034import org.apache.commons.logging.LogFactory; 035import reactor.core.publisher.Mono; 036 037import org.springframework.beans.factory.BeanNameAware; 038import org.springframework.beans.factory.InitializingBean; 039import org.springframework.context.ApplicationContext; 040import org.springframework.context.ApplicationContextAware; 041import org.springframework.core.MethodIntrospector; 042import org.springframework.core.ReactiveAdapterRegistry; 043import org.springframework.lang.Nullable; 044import org.springframework.messaging.Message; 045import org.springframework.messaging.MessagingException; 046import org.springframework.messaging.ReactiveMessageHandler; 047import org.springframework.messaging.handler.HandlerMethod; 048import org.springframework.messaging.handler.MessagingAdviceBean; 049import org.springframework.messaging.handler.invocation.AbstractExceptionHandlerMethodResolver; 050import org.springframework.util.Assert; 051import org.springframework.util.ClassUtils; 052import org.springframework.util.CollectionUtils; 053import org.springframework.util.LinkedMultiValueMap; 054import org.springframework.util.MultiValueMap; 055import org.springframework.util.ObjectUtils; 056import org.springframework.util.RouteMatcher; 057 058/** 059 * Abstract base class for reactive HandlerMethod-based message handling. 060 * Provides most of the logic required to discover handler methods at startup, 061 * find a matching handler method at runtime for a given message and invoke it. 062 * 063 * <p>Also supports discovering and invoking exception handling methods to process 064 * exceptions raised during message handling. 065 * 066 * @author Rossen Stoyanchev 067 * @since 5.2 068 * @param <T> the type of the Object that contains information mapping information 069 */ 070public abstract class AbstractMethodMessageHandler<T> 071 implements ReactiveMessageHandler, ApplicationContextAware, InitializingBean, BeanNameAware { 072 073 /** 074 * Bean name prefix for target beans behind scoped proxies. Used to exclude those 075 * targets from handler method detection, in favor of the corresponding proxies. 076 * <p>We're not checking the autowire-candidate status here, which is how the 077 * proxy target filtering problem is being handled at the autowiring level, 078 * since autowire-candidate may have been turned to {@code false} for other 079 * reasons, while still expecting the bean to be eligible for handler methods. 080 * <p>Originally defined in {@link org.springframework.aop.scope.ScopedProxyUtils} 081 * but duplicated here to avoid a hard dependency on the spring-aop module. 082 */ 083 private static final String SCOPED_TARGET_NAME_PREFIX = "scopedTarget."; 084 085 086 protected final Log logger = LogFactory.getLog(getClass()); 087 088 089 @Nullable 090 private Predicate<Class<?>> handlerPredicate; 091 092 @Nullable 093 List<Object> handlers; 094 095 private ArgumentResolverConfigurer argumentResolverConfigurer = new ArgumentResolverConfigurer(); 096 097 private ReturnValueHandlerConfigurer returnValueHandlerConfigurer = new ReturnValueHandlerConfigurer(); 098 099 private final InvocableHelper invocableHelper = new InvocableHelper(this::createExceptionMethodResolverFor); 100 101 @Nullable 102 private ApplicationContext applicationContext; 103 104 @Nullable 105 private String beanName; 106 107 private final Map<T, HandlerMethod> handlerMethods = new LinkedHashMap<>(64); 108 109 private final MultiValueMap<String, T> destinationLookup = new LinkedMultiValueMap<>(64); 110 111 112 /** 113 * Configure a predicate for selecting which Spring beans to check for the 114 * presence of message handler methods. 115 * <p>This is not set by default. However sub-classes may initialize it to 116 * some default strategy (e.g. {@code @Controller} classes). 117 * @see #setHandlers(List) 118 */ 119 public void setHandlerPredicate(@Nullable Predicate<Class<?>> handlerPredicate) { 120 this.handlerPredicate = handlerPredicate; 121 } 122 123 /** 124 * Return the {@link #setHandlerPredicate configured} handler predicate. 125 */ 126 @Nullable 127 public Predicate<Class<?>> getHandlerPredicate() { 128 return this.handlerPredicate; 129 } 130 131 /** 132 * Manually configure the handlers to check for the presence of message 133 * handling methods, which also disables auto-detection via a 134 * {@link #setHandlerPredicate(Predicate) handlerPredicate}. If you do not 135 * want to disable auto-detection, then call this method first, and then set 136 * the handler predicate. 137 * @param handlers the handlers to check 138 */ 139 public void setHandlers(List<Object> handlers) { 140 this.handlers = handlers; 141 this.handlerPredicate = null; 142 } 143 144 /** 145 * Configure custom resolvers for handler method arguments. 146 */ 147 public void setArgumentResolverConfigurer(ArgumentResolverConfigurer configurer) { 148 Assert.notNull(configurer, "HandlerMethodArgumentResolver is required"); 149 this.argumentResolverConfigurer = configurer; 150 } 151 152 /** 153 * Return the configured custom resolvers for handler method arguments. 154 */ 155 public ArgumentResolverConfigurer getArgumentResolverConfigurer() { 156 return this.argumentResolverConfigurer; 157 } 158 159 /** 160 * Configure custom return value handlers for handler methods. 161 */ 162 public void setReturnValueHandlerConfigurer(ReturnValueHandlerConfigurer configurer) { 163 Assert.notNull(configurer, "ReturnValueHandlerConfigurer is required"); 164 this.returnValueHandlerConfigurer = configurer; 165 } 166 167 /** 168 * Return the configured return value handlers. 169 */ 170 public ReturnValueHandlerConfigurer getReturnValueHandlerConfigurer() { 171 return this.returnValueHandlerConfigurer; 172 } 173 174 /** 175 * Configure the registry for adapting various reactive types. 176 * <p>By default this is an instance of {@link ReactiveAdapterRegistry} with 177 * default settings. 178 */ 179 public void setReactiveAdapterRegistry(ReactiveAdapterRegistry registry) { 180 this.invocableHelper.setReactiveAdapterRegistry(registry); 181 } 182 183 /** 184 * Return the configured registry for adapting reactive types. 185 */ 186 public ReactiveAdapterRegistry getReactiveAdapterRegistry() { 187 return this.invocableHelper.getReactiveAdapterRegistry(); 188 } 189 190 @Override 191 public void setApplicationContext(@Nullable ApplicationContext applicationContext) { 192 this.applicationContext = applicationContext; 193 } 194 195 @Nullable 196 public ApplicationContext getApplicationContext() { 197 return this.applicationContext; 198 } 199 200 @Override 201 public void setBeanName(String name) { 202 this.beanName = name; 203 } 204 205 public String getBeanName() { 206 return (this.beanName != null ? this.beanName : 207 getClass().getSimpleName() + "@" + ObjectUtils.getIdentityHexString(this)); 208 } 209 210 /** 211 * Subclasses can invoke this method to populate the MessagingAdviceBean cache 212 * (e.g. to support "global" {@code @MessageExceptionHandler}). 213 */ 214 protected void registerExceptionHandlerAdvice( 215 MessagingAdviceBean bean, AbstractExceptionHandlerMethodResolver resolver) { 216 217 this.invocableHelper.registerExceptionHandlerAdvice(bean, resolver); 218 } 219 220 /** 221 * Return a read-only map with all handler methods and their mappings. 222 */ 223 public Map<T, HandlerMethod> getHandlerMethods() { 224 return Collections.unmodifiableMap(this.handlerMethods); 225 } 226 227 /** 228 * Return a read-only multi-value map with a direct lookup of mappings, 229 * (e.g. for non-pattern destinations). 230 */ 231 public MultiValueMap<String, T> getDestinationLookup() { 232 return CollectionUtils.unmodifiableMultiValueMap(this.destinationLookup); 233 } 234 235 /** 236 * Return the argument resolvers initialized during {@link #afterPropertiesSet()}. 237 * Primarily for internal use in sub-classes. 238 * @since 5.2.2 239 */ 240 protected HandlerMethodArgumentResolverComposite getArgumentResolvers() { 241 return this.invocableHelper.getArgumentResolvers(); 242 } 243 244 245 @Override 246 public void afterPropertiesSet() { 247 248 List<? extends HandlerMethodArgumentResolver> resolvers = initArgumentResolvers(); 249 if (resolvers.isEmpty()) { 250 resolvers = new ArrayList<>(this.argumentResolverConfigurer.getCustomResolvers()); 251 } 252 this.invocableHelper.addArgumentResolvers(resolvers); 253 254 List<? extends HandlerMethodReturnValueHandler> handlers = initReturnValueHandlers(); 255 if (handlers.isEmpty()) { 256 handlers = new ArrayList<>(this.returnValueHandlerConfigurer.getCustomHandlers()); 257 } 258 this.invocableHelper.addReturnValueHandlers(handlers); 259 260 initHandlerMethods(); 261 } 262 263 /** 264 * Return the list of argument resolvers to use. 265 * <p>Subclasses should also take into account custom argument types configured via 266 * {@link #setArgumentResolverConfigurer}. 267 */ 268 protected abstract List<? extends HandlerMethodArgumentResolver> initArgumentResolvers(); 269 270 /** 271 * Return the list of return value handlers to use. 272 * <p>Subclasses should also take into account custom return value types configured 273 * via {@link #setReturnValueHandlerConfigurer}. 274 */ 275 protected abstract List<? extends HandlerMethodReturnValueHandler> initReturnValueHandlers(); 276 277 278 private void initHandlerMethods() { 279 if (this.handlers != null) { 280 for (Object handler : this.handlers) { 281 detectHandlerMethods(handler); 282 } 283 } 284 Predicate<Class<?>> predicate = this.handlerPredicate; 285 if (predicate == null) { 286 if (logger.isDebugEnabled()) { 287 logger.debug("[" + getBeanName() + "] Skip auto-detection of message handling methods"); 288 } 289 return; 290 } 291 if (this.applicationContext == null) { 292 logger.warn("No ApplicationContext for auto-detection of beans with message handling methods."); 293 return; 294 } 295 for (String beanName : this.applicationContext.getBeanNamesForType(Object.class)) { 296 if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) { 297 Class<?> beanType = null; 298 try { 299 beanType = this.applicationContext.getType(beanName); 300 } 301 catch (Throwable ex) { 302 // An unresolvable bean type, probably from a lazy bean - let's ignore it. 303 if (logger.isDebugEnabled()) { 304 logger.debug("Could not resolve target class for bean with name '" + beanName + "'", ex); 305 } 306 } 307 if (beanType != null && predicate.test(beanType)) { 308 detectHandlerMethods(beanName); 309 } 310 } 311 } 312 } 313 314 /** 315 * Detect if the given handler has any methods that can handle messages and if 316 * so register it with the extracted mapping information. 317 * <p><strong>Note:</strong> This method is protected and can be invoked by 318 * subclasses, but this should be done on startup only as documented in 319 * {@link #registerHandlerMethod}. 320 * @param handler the handler to check, either an instance of a Spring bean name 321 */ 322 protected final void detectHandlerMethods(Object handler) { 323 Class<?> handlerType; 324 if (handler instanceof String) { 325 ApplicationContext context = getApplicationContext(); 326 Assert.state(context != null, "ApplicationContext is required for resolving handler bean names"); 327 handlerType = context.getType((String) handler); 328 } 329 else { 330 handlerType = handler.getClass(); 331 } 332 if (handlerType != null) { 333 final Class<?> userType = ClassUtils.getUserClass(handlerType); 334 Map<Method, T> methods = MethodIntrospector.selectMethods(userType, 335 (MethodIntrospector.MetadataLookup<T>) method -> getMappingForMethod(method, userType)); 336 if (logger.isDebugEnabled()) { 337 logger.debug(formatMappings(userType, methods)); 338 } 339 methods.forEach((key, value) -> registerHandlerMethod(handler, key, value)); 340 } 341 } 342 343 private String formatMappings(Class<?> userType, Map<Method, T> methods) { 344 String formattedType = Arrays.stream(ClassUtils.getPackageName(userType).split("\\.")) 345 .map(p -> p.substring(0, 1)) 346 .collect(Collectors.joining(".", "", "." + userType.getSimpleName())); 347 Function<Method, String> methodFormatter = method -> Arrays.stream(method.getParameterTypes()) 348 .map(Class::getSimpleName) 349 .collect(Collectors.joining(",", "(", ")")); 350 return methods.entrySet().stream() 351 .map(e -> { 352 Method method = e.getKey(); 353 return e.getValue() + ": " + method.getName() + methodFormatter.apply(method); 354 }) 355 .collect(Collectors.joining("\n\t", "\n\t" + formattedType + ":" + "\n\t", "")); 356 } 357 358 /** 359 * Obtain the mapping for the given method, if any. 360 * @param method the method to check 361 * @param handlerType the handler type, possibly a sub-type of the method's declaring class 362 * @return the mapping, or {@code null} if the method is not mapped 363 */ 364 @Nullable 365 protected abstract T getMappingForMethod(Method method, Class<?> handlerType); 366 367 /** 368 * Register a handler method and its unique mapping. 369 * <p><strong>Note:</strong> This method is protected and can be invoked by 370 * subclasses. Keep in mind however that the registration is not protected 371 * for concurrent use, and is expected to be done on startup. 372 * @param handler the bean name of the handler or the handler instance 373 * @param method the method to register 374 * @param mapping the mapping conditions associated with the handler method 375 * @throws IllegalStateException if another method was already registered 376 * under the same mapping 377 */ 378 protected final void registerHandlerMethod(Object handler, Method method, T mapping) { 379 Assert.notNull(mapping, "Mapping must not be null"); 380 HandlerMethod newHandlerMethod = createHandlerMethod(handler, method); 381 HandlerMethod oldHandlerMethod = this.handlerMethods.get(mapping); 382 383 if (oldHandlerMethod != null && !oldHandlerMethod.equals(newHandlerMethod)) { 384 throw new IllegalStateException("Ambiguous mapping found. Cannot map '" + newHandlerMethod.getBean() + 385 "' bean method \n" + newHandlerMethod + "\nto " + mapping + ": There is already '" + 386 oldHandlerMethod.getBean() + "' bean method\n" + oldHandlerMethod + " mapped."); 387 } 388 389 mapping = extendMapping(mapping, newHandlerMethod); 390 this.handlerMethods.put(mapping, newHandlerMethod); 391 392 for (String pattern : getDirectLookupMappings(mapping)) { 393 this.destinationLookup.add(pattern, mapping); 394 } 395 } 396 397 /** 398 * Create a HandlerMethod instance from an Object handler that is either a handler 399 * instance or a String-based bean name. 400 */ 401 private HandlerMethod createHandlerMethod(Object handler, Method method) { 402 HandlerMethod handlerMethod; 403 if (handler instanceof String) { 404 ApplicationContext context = getApplicationContext(); 405 Assert.state(context != null, "ApplicationContext is required for resolving handler bean names"); 406 String beanName = (String) handler; 407 handlerMethod = new HandlerMethod(beanName, context.getAutowireCapableBeanFactory(), method); 408 } 409 else { 410 handlerMethod = new HandlerMethod(handler, method); 411 } 412 return handlerMethod; 413 } 414 415 /** 416 * This method is invoked just before mappings are added. It allows 417 * sub-classes to update the mapping with the {@link HandlerMethod} in mind. 418 * This can be useful when the method signature is used to refine the 419 * mapping, e.g. based on the cardinality of input and output. 420 * <p>By default this method returns the mapping that is passed in. 421 * @param mapping the mapping to be added 422 * @param handlerMethod the target handler for the mapping 423 * @return a new mapping or the same 424 * @since 5.2.2 425 */ 426 protected T extendMapping(T mapping, HandlerMethod handlerMethod) { 427 return mapping; 428 } 429 430 /** 431 * Return String-based destinations for the given mapping, if any, that can 432 * be used to find matches with a direct lookup (i.e. non-patterns). 433 * <p><strong>Note:</strong> This is completely optional. The mapping 434 * metadata for a subclass may support neither direct lookups, nor String 435 * based destinations. 436 */ 437 protected abstract Set<String> getDirectLookupMappings(T mapping); 438 439 440 @Override 441 public Mono<Void> handleMessage(Message<?> message) throws MessagingException { 442 Match<T> match = null; 443 try { 444 match = getHandlerMethod(message); 445 } 446 catch (Exception ex) { 447 return Mono.error(ex); 448 } 449 if (match == null) { 450 // handleNoMatch would have been invoked already 451 return Mono.empty(); 452 } 453 return handleMatch(match.mapping, match.handlerMethod, message); 454 } 455 456 protected Mono<Void> handleMatch(T mapping, HandlerMethod handlerMethod, Message<?> message) { 457 handlerMethod = handlerMethod.createWithResolvedBean(); 458 return this.invocableHelper.handleMessage(handlerMethod, message); 459 } 460 461 @Nullable 462 private Match<T> getHandlerMethod(Message<?> message) { 463 List<Match<T>> matches = new ArrayList<>(); 464 465 RouteMatcher.Route destination = getDestination(message); 466 List<T> mappingsByUrl = (destination != null ? this.destinationLookup.get(destination.value()) : null); 467 if (mappingsByUrl != null) { 468 addMatchesToCollection(mappingsByUrl, message, matches); 469 } 470 if (matches.isEmpty()) { 471 // No direct hits, go through all mappings 472 Set<T> allMappings = this.handlerMethods.keySet(); 473 addMatchesToCollection(allMappings, message, matches); 474 } 475 if (matches.isEmpty()) { 476 handleNoMatch(destination, message); 477 return null; 478 } 479 Comparator<Match<T>> comparator = new MatchComparator(getMappingComparator(message)); 480 matches.sort(comparator); 481 if (logger.isTraceEnabled()) { 482 logger.trace("Found " + matches.size() + " handler methods: " + matches); 483 } 484 Match<T> bestMatch = matches.get(0); 485 if (matches.size() > 1) { 486 Match<T> secondBestMatch = matches.get(1); 487 if (comparator.compare(bestMatch, secondBestMatch) == 0) { 488 HandlerMethod m1 = bestMatch.handlerMethod; 489 HandlerMethod m2 = secondBestMatch.handlerMethod; 490 throw new IllegalStateException("Ambiguous handler methods mapped for destination '" + 491 (destination != null ? destination.value() : "") + "': {" + 492 m1.getShortLogMessage() + ", " + m2.getShortLogMessage() + "}"); 493 } 494 } 495 return bestMatch; 496 } 497 498 /** 499 * Extract the destination from the given message. 500 * @see #getDirectLookupMappings(Object) 501 */ 502 @Nullable 503 protected abstract RouteMatcher.Route getDestination(Message<?> message); 504 505 private void addMatchesToCollection( 506 Collection<T> mappingsToCheck, Message<?> message, List<Match<T>> matches) { 507 508 for (T mapping : mappingsToCheck) { 509 T match = getMatchingMapping(mapping, message); 510 if (match != null) { 511 matches.add(new Match<T>(match, this.handlerMethods.get(mapping))); 512 } 513 } 514 } 515 516 /** 517 * Check if a mapping matches the current message and return a possibly 518 * new mapping with conditions relevant to the current request. 519 * @param mapping the mapping to get a match for 520 * @param message the message being handled 521 * @return the match or {@code null} if there is no match 522 */ 523 @Nullable 524 protected abstract T getMatchingMapping(T mapping, Message<?> message); 525 526 /** 527 * Return a comparator for sorting matching mappings. 528 * The returned comparator should sort 'better' matches higher. 529 * @param message the current Message 530 * @return the comparator, never {@code null} 531 */ 532 protected abstract Comparator<T> getMappingComparator(Message<?> message); 533 534 /** 535 * Invoked when no matching handler is found. 536 * @param destination the destination 537 * @param message the message 538 */ 539 protected void handleNoMatch(@Nullable RouteMatcher.Route destination, Message<?> message) { 540 logger.debug("No handlers for destination '" + 541 (destination != null ? destination.value() : "") + "'"); 542 } 543 544 /** 545 * Create a concrete instance of {@link AbstractExceptionHandlerMethodResolver} 546 * that finds exception handling methods based on some criteria, e.g. based 547 * on the presence of {@code @MessageExceptionHandler}. 548 * @param beanType the class in which an exception occurred during handling 549 * @return the resolver to use 550 */ 551 protected abstract AbstractExceptionHandlerMethodResolver createExceptionMethodResolverFor(Class<?> beanType); 552 553 554 /** 555 * Container for matched mapping and HandlerMethod. Used for best match 556 * comparison and for access to mapping information. 557 */ 558 private static class Match<T> { 559 560 private final T mapping; 561 562 private final HandlerMethod handlerMethod; 563 564 Match(T mapping, HandlerMethod handlerMethod) { 565 this.mapping = mapping; 566 this.handlerMethod = handlerMethod; 567 } 568 569 @Override 570 public String toString() { 571 return this.mapping.toString(); 572 } 573 } 574 575 576 private class MatchComparator implements Comparator<Match<T>> { 577 578 private final Comparator<T> comparator; 579 580 MatchComparator(Comparator<T> comparator) { 581 this.comparator = comparator; 582 } 583 584 @Override 585 public int compare(Match<T> match1, Match<T> match2) { 586 return this.comparator.compare(match1.mapping, match2.mapping); 587 } 588 } 589 590}