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.web.reactive.result.method; 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.HashMap; 026import java.util.LinkedHashMap; 027import java.util.List; 028import java.util.Map; 029import java.util.Set; 030import java.util.concurrent.ConcurrentHashMap; 031import java.util.concurrent.locks.ReentrantReadWriteLock; 032import java.util.function.Function; 033import java.util.stream.Collectors; 034 035import reactor.core.publisher.Mono; 036 037import org.springframework.aop.support.AopUtils; 038import org.springframework.beans.factory.InitializingBean; 039import org.springframework.core.MethodIntrospector; 040import org.springframework.http.server.RequestPath; 041import org.springframework.lang.Nullable; 042import org.springframework.util.Assert; 043import org.springframework.util.ClassUtils; 044import org.springframework.web.cors.CorsConfiguration; 045import org.springframework.web.cors.reactive.CorsUtils; 046import org.springframework.web.method.HandlerMethod; 047import org.springframework.web.reactive.HandlerMapping; 048import org.springframework.web.reactive.handler.AbstractHandlerMapping; 049import org.springframework.web.server.ServerWebExchange; 050 051/** 052 * Abstract base class for {@link HandlerMapping} implementations that define 053 * a mapping between a request and a {@link HandlerMethod}. 054 * 055 * <p>For each registered handler method, a unique mapping is maintained with 056 * subclasses defining the details of the mapping type {@code <T>}. 057 * 058 * @author Rossen Stoyanchev 059 * @author Brian Clozel 060 * @author Sam Brannen 061 * @since 5.0 062 * @param <T> the mapping for a {@link HandlerMethod} containing the conditions 063 * needed to match the handler method to an incoming request. 064 */ 065public abstract class AbstractHandlerMethodMapping<T> extends AbstractHandlerMapping implements InitializingBean { 066 067 /** 068 * Bean name prefix for target beans behind scoped proxies. Used to exclude those 069 * targets from handler method detection, in favor of the corresponding proxies. 070 * <p>We're not checking the autowire-candidate status here, which is how the 071 * proxy target filtering problem is being handled at the autowiring level, 072 * since autowire-candidate may have been turned to {@code false} for other 073 * reasons, while still expecting the bean to be eligible for handler methods. 074 * <p>Originally defined in {@link org.springframework.aop.scope.ScopedProxyUtils} 075 * but duplicated here to avoid a hard dependency on the spring-aop module. 076 */ 077 private static final String SCOPED_TARGET_NAME_PREFIX = "scopedTarget."; 078 079 /** 080 * HandlerMethod to return on a pre-flight request match when the request 081 * mappings are more nuanced than the access control headers. 082 */ 083 private static final HandlerMethod PREFLIGHT_AMBIGUOUS_MATCH = 084 new HandlerMethod(new PreFlightAmbiguousMatchHandler(), 085 ClassUtils.getMethod(PreFlightAmbiguousMatchHandler.class, "handle")); 086 087 private static final CorsConfiguration ALLOW_CORS_CONFIG = new CorsConfiguration(); 088 089 static { 090 ALLOW_CORS_CONFIG.addAllowedOrigin("*"); 091 ALLOW_CORS_CONFIG.addAllowedMethod("*"); 092 ALLOW_CORS_CONFIG.addAllowedHeader("*"); 093 ALLOW_CORS_CONFIG.setAllowCredentials(true); 094 } 095 096 097 private final MappingRegistry mappingRegistry = new MappingRegistry(); 098 099 100 // TODO: handlerMethodMappingNamingStrategy 101 102 /** 103 * Return a (read-only) map with all mappings and HandlerMethod's. 104 */ 105 public Map<T, HandlerMethod> getHandlerMethods() { 106 this.mappingRegistry.acquireReadLock(); 107 try { 108 return Collections.unmodifiableMap(this.mappingRegistry.getMappings()); 109 } 110 finally { 111 this.mappingRegistry.releaseReadLock(); 112 } 113 } 114 115 /** 116 * Return the internal mapping registry. Provided for testing purposes. 117 */ 118 MappingRegistry getMappingRegistry() { 119 return this.mappingRegistry; 120 } 121 122 /** 123 * Register the given mapping. 124 * <p>This method may be invoked at runtime after initialization has completed. 125 * @param mapping the mapping for the handler method 126 * @param handler the handler 127 * @param method the method 128 */ 129 public void registerMapping(T mapping, Object handler, Method method) { 130 if (logger.isTraceEnabled()) { 131 logger.trace("Register \"" + mapping + "\" to " + method.toGenericString()); 132 } 133 this.mappingRegistry.register(mapping, handler, method); 134 } 135 136 /** 137 * Un-register the given mapping. 138 * <p>This method may be invoked at runtime after initialization has completed. 139 * @param mapping the mapping to unregister 140 */ 141 public void unregisterMapping(T mapping) { 142 if (logger.isTraceEnabled()) { 143 logger.trace("Unregister mapping \"" + mapping); 144 } 145 this.mappingRegistry.unregister(mapping); 146 } 147 148 149 // Handler method detection 150 151 /** 152 * Detects handler methods at initialization. 153 */ 154 @Override 155 public void afterPropertiesSet() { 156 157 initHandlerMethods(); 158 159 // Total includes detected mappings + explicit registrations via registerMapping.. 160 int total = this.getHandlerMethods().size(); 161 162 if ((logger.isTraceEnabled() && total == 0) || (logger.isDebugEnabled() && total > 0) ) { 163 logger.debug(total + " mappings in " + formatMappingName()); 164 } 165 } 166 167 /** 168 * Scan beans in the ApplicationContext, detect and register handler methods. 169 * @see #isHandler(Class) 170 * @see #getMappingForMethod(Method, Class) 171 * @see #handlerMethodsInitialized(Map) 172 */ 173 protected void initHandlerMethods() { 174 String[] beanNames = obtainApplicationContext().getBeanNamesForType(Object.class); 175 176 for (String beanName : beanNames) { 177 if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) { 178 Class<?> beanType = null; 179 try { 180 beanType = obtainApplicationContext().getType(beanName); 181 } 182 catch (Throwable ex) { 183 // An unresolvable bean type, probably from a lazy bean - let's ignore it. 184 if (logger.isTraceEnabled()) { 185 logger.trace("Could not resolve type for bean '" + beanName + "'", ex); 186 } 187 } 188 if (beanType != null && isHandler(beanType)) { 189 detectHandlerMethods(beanName); 190 } 191 } 192 } 193 handlerMethodsInitialized(getHandlerMethods()); 194 } 195 196 /** 197 * Look for handler methods in a handler. 198 * @param handler the bean name of a handler or a handler instance 199 */ 200 protected void detectHandlerMethods(final Object handler) { 201 Class<?> handlerType = (handler instanceof String ? 202 obtainApplicationContext().getType((String) handler) : handler.getClass()); 203 204 if (handlerType != null) { 205 final Class<?> userType = ClassUtils.getUserClass(handlerType); 206 Map<Method, T> methods = MethodIntrospector.selectMethods(userType, 207 (MethodIntrospector.MetadataLookup<T>) method -> getMappingForMethod(method, userType)); 208 if (logger.isTraceEnabled()) { 209 logger.trace(formatMappings(userType, methods)); 210 } 211 methods.forEach((method, mapping) -> { 212 Method invocableMethod = AopUtils.selectInvocableMethod(method, userType); 213 registerHandlerMethod(handler, invocableMethod, mapping); 214 }); 215 } 216 } 217 218 private String formatMappings(Class<?> userType, Map<Method, T> methods) { 219 String formattedType = Arrays.stream(ClassUtils.getPackageName(userType).split("\\.")) 220 .map(p -> p.substring(0, 1)) 221 .collect(Collectors.joining(".", "", "." + userType.getSimpleName())); 222 Function<Method, String> methodFormatter = method -> Arrays.stream(method.getParameterTypes()) 223 .map(Class::getSimpleName) 224 .collect(Collectors.joining(",", "(", ")")); 225 return methods.entrySet().stream() 226 .map(e -> { 227 Method method = e.getKey(); 228 return e.getValue() + ": " + method.getName() + methodFormatter.apply(method); 229 }) 230 .collect(Collectors.joining("\n\t", "\n\t" + formattedType + ":" + "\n\t", "")); 231 } 232 233 /** 234 * Register a handler method and its unique mapping. Invoked at startup for 235 * each detected handler method. 236 * @param handler the bean name of the handler or the handler instance 237 * @param method the method to register 238 * @param mapping the mapping conditions associated with the handler method 239 * @throws IllegalStateException if another method was already registered 240 * under the same mapping 241 */ 242 protected void registerHandlerMethod(Object handler, Method method, T mapping) { 243 this.mappingRegistry.register(mapping, handler, method); 244 } 245 246 /** 247 * Create the HandlerMethod instance. 248 * @param handler either a bean name or an actual handler instance 249 * @param method the target method 250 * @return the created HandlerMethod 251 */ 252 protected HandlerMethod createHandlerMethod(Object handler, Method method) { 253 if (handler instanceof String) { 254 return new HandlerMethod((String) handler, 255 obtainApplicationContext().getAutowireCapableBeanFactory(), method); 256 } 257 return new HandlerMethod(handler, method); 258 } 259 260 /** 261 * Extract and return the CORS configuration for the mapping. 262 */ 263 @Nullable 264 protected CorsConfiguration initCorsConfiguration(Object handler, Method method, T mapping) { 265 return null; 266 } 267 268 /** 269 * Invoked after all handler methods have been detected. 270 * @param handlerMethods a read-only map with handler methods and mappings. 271 */ 272 protected void handlerMethodsInitialized(Map<T, HandlerMethod> handlerMethods) { 273 } 274 275 276 // Handler method lookup 277 278 /** 279 * Look up a handler method for the given request. 280 * @param exchange the current exchange 281 */ 282 @Override 283 public Mono<HandlerMethod> getHandlerInternal(ServerWebExchange exchange) { 284 this.mappingRegistry.acquireReadLock(); 285 try { 286 HandlerMethod handlerMethod; 287 try { 288 handlerMethod = lookupHandlerMethod(exchange); 289 } 290 catch (Exception ex) { 291 return Mono.error(ex); 292 } 293 if (handlerMethod != null) { 294 handlerMethod = handlerMethod.createWithResolvedBean(); 295 } 296 return Mono.justOrEmpty(handlerMethod); 297 } 298 finally { 299 this.mappingRegistry.releaseReadLock(); 300 } 301 } 302 303 /** 304 * Look up the best-matching handler method for the current request. 305 * If multiple matches are found, the best match is selected. 306 * @param exchange the current exchange 307 * @return the best-matching handler method, or {@code null} if no match 308 * @see #handleMatch 309 * @see #handleNoMatch 310 */ 311 @Nullable 312 protected HandlerMethod lookupHandlerMethod(ServerWebExchange exchange) throws Exception { 313 List<Match> matches = new ArrayList<>(); 314 addMatchingMappings(this.mappingRegistry.getMappings().keySet(), matches, exchange); 315 316 if (!matches.isEmpty()) { 317 Comparator<Match> comparator = new MatchComparator(getMappingComparator(exchange)); 318 matches.sort(comparator); 319 Match bestMatch = matches.get(0); 320 if (matches.size() > 1) { 321 if (logger.isTraceEnabled()) { 322 logger.trace(exchange.getLogPrefix() + matches.size() + " matching mappings: " + matches); 323 } 324 if (CorsUtils.isPreFlightRequest(exchange.getRequest())) { 325 return PREFLIGHT_AMBIGUOUS_MATCH; 326 } 327 Match secondBestMatch = matches.get(1); 328 if (comparator.compare(bestMatch, secondBestMatch) == 0) { 329 Method m1 = bestMatch.handlerMethod.getMethod(); 330 Method m2 = secondBestMatch.handlerMethod.getMethod(); 331 RequestPath path = exchange.getRequest().getPath(); 332 throw new IllegalStateException( 333 "Ambiguous handler methods mapped for '" + path + "': {" + m1 + ", " + m2 + "}"); 334 } 335 } 336 handleMatch(bestMatch.mapping, bestMatch.handlerMethod, exchange); 337 return bestMatch.handlerMethod; 338 } 339 else { 340 return handleNoMatch(this.mappingRegistry.getMappings().keySet(), exchange); 341 } 342 } 343 344 private void addMatchingMappings(Collection<T> mappings, List<Match> matches, ServerWebExchange exchange) { 345 for (T mapping : mappings) { 346 T match = getMatchingMapping(mapping, exchange); 347 if (match != null) { 348 matches.add(new Match(match, this.mappingRegistry.getMappings().get(mapping))); 349 } 350 } 351 } 352 353 /** 354 * Invoked when a matching mapping is found. 355 * @param mapping the matching mapping 356 * @param handlerMethod the matching method 357 * @param exchange the current exchange 358 */ 359 protected void handleMatch(T mapping, HandlerMethod handlerMethod, ServerWebExchange exchange) { 360 } 361 362 /** 363 * Invoked when no matching mapping is not found. 364 * @param mappings all registered mappings 365 * @param exchange the current exchange 366 * @return an alternative HandlerMethod or {@code null} 367 * @throws Exception provides details that can be translated into an error status code 368 */ 369 @Nullable 370 protected HandlerMethod handleNoMatch(Set<T> mappings, ServerWebExchange exchange) throws Exception { 371 return null; 372 } 373 374 @Override 375 protected boolean hasCorsConfigurationSource(Object handler) { 376 return super.hasCorsConfigurationSource(handler) || 377 (handler instanceof HandlerMethod && this.mappingRegistry.getCorsConfiguration((HandlerMethod) handler) != null); 378 } 379 380 @Override 381 protected CorsConfiguration getCorsConfiguration(Object handler, ServerWebExchange exchange) { 382 CorsConfiguration corsConfig = super.getCorsConfiguration(handler, exchange); 383 if (handler instanceof HandlerMethod) { 384 HandlerMethod handlerMethod = (HandlerMethod) handler; 385 if (handlerMethod.equals(PREFLIGHT_AMBIGUOUS_MATCH)) { 386 return ALLOW_CORS_CONFIG; 387 } 388 CorsConfiguration methodConfig = this.mappingRegistry.getCorsConfiguration(handlerMethod); 389 corsConfig = (corsConfig != null ? corsConfig.combine(methodConfig) : methodConfig); 390 } 391 return corsConfig; 392 } 393 394 395 // Abstract template methods 396 397 /** 398 * Whether the given type is a handler with handler methods. 399 * @param beanType the type of the bean being checked 400 * @return "true" if this a handler type, "false" otherwise. 401 */ 402 protected abstract boolean isHandler(Class<?> beanType); 403 404 /** 405 * Provide the mapping for a handler method. A method for which no 406 * mapping can be provided is not a handler method. 407 * @param method the method to provide a mapping for 408 * @param handlerType the handler type, possibly a sub-type of the method's 409 * declaring class 410 * @return the mapping, or {@code null} if the method is not mapped 411 */ 412 @Nullable 413 protected abstract T getMappingForMethod(Method method, Class<?> handlerType); 414 415 /** 416 * Check if a mapping matches the current request and return a (potentially 417 * new) mapping with conditions relevant to the current request. 418 * @param mapping the mapping to get a match for 419 * @param exchange the current exchange 420 * @return the match, or {@code null} if the mapping doesn't match 421 */ 422 @Nullable 423 protected abstract T getMatchingMapping(T mapping, ServerWebExchange exchange); 424 425 /** 426 * Return a comparator for sorting matching mappings. 427 * The returned comparator should sort 'better' matches higher. 428 * @param exchange the current exchange 429 * @return the comparator (never {@code null}) 430 */ 431 protected abstract Comparator<T> getMappingComparator(ServerWebExchange exchange); 432 433 434 /** 435 * A registry that maintains all mappings to handler methods, exposing methods 436 * to perform lookups and providing concurrent access. 437 * 438 * <p>Package-private for testing purposes. 439 */ 440 class MappingRegistry { 441 442 private final Map<T, MappingRegistration<T>> registry = new HashMap<>(); 443 444 private final Map<T, HandlerMethod> mappingLookup = new LinkedHashMap<>(); 445 446 private final Map<HandlerMethod, CorsConfiguration> corsLookup = new ConcurrentHashMap<>(); 447 448 private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock(); 449 450 /** 451 * Return all mappings and handler methods. Not thread-safe. 452 * @see #acquireReadLock() 453 */ 454 public Map<T, HandlerMethod> getMappings() { 455 return this.mappingLookup; 456 } 457 458 /** 459 * Return CORS configuration. Thread-safe for concurrent use. 460 */ 461 @Nullable 462 public CorsConfiguration getCorsConfiguration(HandlerMethod handlerMethod) { 463 HandlerMethod original = handlerMethod.getResolvedFromHandlerMethod(); 464 return this.corsLookup.get(original != null ? original : handlerMethod); 465 } 466 467 /** 468 * Acquire the read lock when using getMappings and getMappingsByUrl. 469 */ 470 public void acquireReadLock() { 471 this.readWriteLock.readLock().lock(); 472 } 473 474 /** 475 * Release the read lock after using getMappings and getMappingsByUrl. 476 */ 477 public void releaseReadLock() { 478 this.readWriteLock.readLock().unlock(); 479 } 480 481 public void register(T mapping, Object handler, Method method) { 482 this.readWriteLock.writeLock().lock(); 483 try { 484 HandlerMethod handlerMethod = createHandlerMethod(handler, method); 485 validateMethodMapping(handlerMethod, mapping); 486 this.mappingLookup.put(mapping, handlerMethod); 487 488 CorsConfiguration corsConfig = initCorsConfiguration(handler, method, mapping); 489 if (corsConfig != null) { 490 this.corsLookup.put(handlerMethod, corsConfig); 491 } 492 493 this.registry.put(mapping, new MappingRegistration<>(mapping, handlerMethod)); 494 } 495 finally { 496 this.readWriteLock.writeLock().unlock(); 497 } 498 } 499 500 private void validateMethodMapping(HandlerMethod handlerMethod, T mapping) { 501 // Assert that the supplied mapping is unique. 502 HandlerMethod existingHandlerMethod = this.mappingLookup.get(mapping); 503 if (existingHandlerMethod != null && !existingHandlerMethod.equals(handlerMethod)) { 504 throw new IllegalStateException( 505 "Ambiguous mapping. Cannot map '" + handlerMethod.getBean() + "' method \n" + 506 handlerMethod + "\nto " + mapping + ": There is already '" + 507 existingHandlerMethod.getBean() + "' bean method\n" + existingHandlerMethod + " mapped."); 508 } 509 } 510 511 public void unregister(T mapping) { 512 this.readWriteLock.writeLock().lock(); 513 try { 514 MappingRegistration<T> definition = this.registry.remove(mapping); 515 if (definition == null) { 516 return; 517 } 518 519 this.mappingLookup.remove(definition.getMapping()); 520 this.corsLookup.remove(definition.getHandlerMethod()); 521 } 522 finally { 523 this.readWriteLock.writeLock().unlock(); 524 } 525 } 526 } 527 528 529 private static class MappingRegistration<T> { 530 531 private final T mapping; 532 533 private final HandlerMethod handlerMethod; 534 535 public MappingRegistration(T mapping, HandlerMethod handlerMethod) { 536 Assert.notNull(mapping, "Mapping must not be null"); 537 Assert.notNull(handlerMethod, "HandlerMethod must not be null"); 538 this.mapping = mapping; 539 this.handlerMethod = handlerMethod; 540 } 541 542 public T getMapping() { 543 return this.mapping; 544 } 545 546 public HandlerMethod getHandlerMethod() { 547 return this.handlerMethod; 548 } 549 550 } 551 552 553 /** 554 * A thin wrapper around a matched HandlerMethod and its mapping, for the purpose of 555 * comparing the best match with a comparator in the context of the current request. 556 */ 557 private class Match { 558 559 private final T mapping; 560 561 private final HandlerMethod handlerMethod; 562 563 public Match(T mapping, HandlerMethod handlerMethod) { 564 this.mapping = mapping; 565 this.handlerMethod = handlerMethod; 566 } 567 568 @Override 569 public String toString() { 570 return this.mapping.toString(); 571 } 572 } 573 574 575 private class MatchComparator implements Comparator<Match> { 576 577 private final Comparator<T> comparator; 578 579 public MatchComparator(Comparator<T> comparator) { 580 this.comparator = comparator; 581 } 582 583 @Override 584 public int compare(Match match1, Match match2) { 585 return this.comparator.compare(match1.mapping, match2.mapping); 586 } 587 } 588 589 590 private static class PreFlightAmbiguousMatchHandler { 591 592 @SuppressWarnings("unused") 593 public void handle() { 594 throw new UnsupportedOperationException("Not implemented"); 595 } 596 } 597 598}