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.servlet.handler; 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 javax.servlet.ServletException; 036import javax.servlet.http.HttpServletRequest; 037 038import kotlin.reflect.KFunction; 039import kotlin.reflect.jvm.ReflectJvmMapping; 040 041import org.springframework.aop.support.AopUtils; 042import org.springframework.beans.factory.BeanFactoryUtils; 043import org.springframework.beans.factory.InitializingBean; 044import org.springframework.core.KotlinDetector; 045import org.springframework.core.MethodIntrospector; 046import org.springframework.lang.Nullable; 047import org.springframework.util.Assert; 048import org.springframework.util.ClassUtils; 049import org.springframework.util.LinkedMultiValueMap; 050import org.springframework.util.MultiValueMap; 051import org.springframework.web.cors.CorsConfiguration; 052import org.springframework.web.cors.CorsUtils; 053import org.springframework.web.method.HandlerMethod; 054import org.springframework.web.servlet.HandlerMapping; 055 056/** 057 * Abstract base class for {@link HandlerMapping} implementations that define 058 * a mapping between a request and a {@link HandlerMethod}. 059 * 060 * <p>For each registered handler method, a unique mapping is maintained with 061 * subclasses defining the details of the mapping type {@code <T>}. 062 * 063 * @author Arjen Poutsma 064 * @author Rossen Stoyanchev 065 * @author Juergen Hoeller 066 * @author Sam Brannen 067 * @since 3.1 068 * @param <T> the mapping for a {@link HandlerMethod} containing the conditions 069 * needed to match the handler method to an incoming request. 070 */ 071public abstract class AbstractHandlerMethodMapping<T> extends AbstractHandlerMapping implements InitializingBean { 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 private static final HandlerMethod PREFLIGHT_AMBIGUOUS_MATCH = 086 new HandlerMethod(new EmptyHandler(), ClassUtils.getMethod(EmptyHandler.class, "handle")); 087 088 private static final CorsConfiguration ALLOW_CORS_CONFIG = new CorsConfiguration(); 089 090 static { 091 ALLOW_CORS_CONFIG.addAllowedOrigin("*"); 092 ALLOW_CORS_CONFIG.addAllowedMethod("*"); 093 ALLOW_CORS_CONFIG.addAllowedHeader("*"); 094 ALLOW_CORS_CONFIG.setAllowCredentials(true); 095 } 096 097 098 private boolean detectHandlerMethodsInAncestorContexts = false; 099 100 @Nullable 101 private HandlerMethodMappingNamingStrategy<T> namingStrategy; 102 103 private final MappingRegistry mappingRegistry = new MappingRegistry(); 104 105 106 /** 107 * Whether to detect handler methods in beans in ancestor ApplicationContexts. 108 * <p>Default is "false": Only beans in the current ApplicationContext are 109 * considered, i.e. only in the context that this HandlerMapping itself 110 * is defined in (typically the current DispatcherServlet's context). 111 * <p>Switch this flag on to detect handler beans in ancestor contexts 112 * (typically the Spring root WebApplicationContext) as well. 113 * @see #getCandidateBeanNames() 114 */ 115 public void setDetectHandlerMethodsInAncestorContexts(boolean detectHandlerMethodsInAncestorContexts) { 116 this.detectHandlerMethodsInAncestorContexts = detectHandlerMethodsInAncestorContexts; 117 } 118 119 /** 120 * Configure the naming strategy to use for assigning a default name to every 121 * mapped handler method. 122 * <p>The default naming strategy is based on the capital letters of the 123 * class name followed by "#" and then the method name, e.g. "TC#getFoo" 124 * for a class named TestController with method getFoo. 125 */ 126 public void setHandlerMethodMappingNamingStrategy(HandlerMethodMappingNamingStrategy<T> namingStrategy) { 127 this.namingStrategy = namingStrategy; 128 } 129 130 /** 131 * Return the configured naming strategy or {@code null}. 132 */ 133 @Nullable 134 public HandlerMethodMappingNamingStrategy<T> getNamingStrategy() { 135 return this.namingStrategy; 136 } 137 138 /** 139 * Return a (read-only) map with all mappings and HandlerMethod's. 140 */ 141 public Map<T, HandlerMethod> getHandlerMethods() { 142 this.mappingRegistry.acquireReadLock(); 143 try { 144 return Collections.unmodifiableMap(this.mappingRegistry.getMappings()); 145 } 146 finally { 147 this.mappingRegistry.releaseReadLock(); 148 } 149 } 150 151 /** 152 * Return the handler methods for the given mapping name. 153 * @param mappingName the mapping name 154 * @return a list of matching HandlerMethod's or {@code null}; the returned 155 * list will never be modified and is safe to iterate. 156 * @see #setHandlerMethodMappingNamingStrategy 157 */ 158 @Nullable 159 public List<HandlerMethod> getHandlerMethodsForMappingName(String mappingName) { 160 return this.mappingRegistry.getHandlerMethodsByMappingName(mappingName); 161 } 162 163 /** 164 * Return the internal mapping registry. Provided for testing purposes. 165 */ 166 MappingRegistry getMappingRegistry() { 167 return this.mappingRegistry; 168 } 169 170 /** 171 * Register the given mapping. 172 * <p>This method may be invoked at runtime after initialization has completed. 173 * @param mapping the mapping for the handler method 174 * @param handler the handler 175 * @param method the method 176 */ 177 public void registerMapping(T mapping, Object handler, Method method) { 178 if (logger.isTraceEnabled()) { 179 logger.trace("Register \"" + mapping + "\" to " + method.toGenericString()); 180 } 181 this.mappingRegistry.register(mapping, handler, method); 182 } 183 184 /** 185 * Un-register the given mapping. 186 * <p>This method may be invoked at runtime after initialization has completed. 187 * @param mapping the mapping to unregister 188 */ 189 public void unregisterMapping(T mapping) { 190 if (logger.isTraceEnabled()) { 191 logger.trace("Unregister mapping \"" + mapping + "\""); 192 } 193 this.mappingRegistry.unregister(mapping); 194 } 195 196 197 // Handler method detection 198 199 /** 200 * Detects handler methods at initialization. 201 * @see #initHandlerMethods 202 */ 203 @Override 204 public void afterPropertiesSet() { 205 initHandlerMethods(); 206 } 207 208 /** 209 * Scan beans in the ApplicationContext, detect and register handler methods. 210 * @see #getCandidateBeanNames() 211 * @see #processCandidateBean 212 * @see #handlerMethodsInitialized 213 */ 214 protected void initHandlerMethods() { 215 for (String beanName : getCandidateBeanNames()) { 216 if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) { 217 processCandidateBean(beanName); 218 } 219 } 220 handlerMethodsInitialized(getHandlerMethods()); 221 } 222 223 /** 224 * Determine the names of candidate beans in the application context. 225 * @since 5.1 226 * @see #setDetectHandlerMethodsInAncestorContexts 227 * @see BeanFactoryUtils#beanNamesForTypeIncludingAncestors 228 */ 229 protected String[] getCandidateBeanNames() { 230 return (this.detectHandlerMethodsInAncestorContexts ? 231 BeanFactoryUtils.beanNamesForTypeIncludingAncestors(obtainApplicationContext(), Object.class) : 232 obtainApplicationContext().getBeanNamesForType(Object.class)); 233 } 234 235 /** 236 * Determine the type of the specified candidate bean and call 237 * {@link #detectHandlerMethods} if identified as a handler type. 238 * <p>This implementation avoids bean creation through checking 239 * {@link org.springframework.beans.factory.BeanFactory#getType} 240 * and calling {@link #detectHandlerMethods} with the bean name. 241 * @param beanName the name of the candidate bean 242 * @since 5.1 243 * @see #isHandler 244 * @see #detectHandlerMethods 245 */ 246 protected void processCandidateBean(String beanName) { 247 Class<?> beanType = null; 248 try { 249 beanType = obtainApplicationContext().getType(beanName); 250 } 251 catch (Throwable ex) { 252 // An unresolvable bean type, probably from a lazy bean - let's ignore it. 253 if (logger.isTraceEnabled()) { 254 logger.trace("Could not resolve type for bean '" + beanName + "'", ex); 255 } 256 } 257 if (beanType != null && isHandler(beanType)) { 258 detectHandlerMethods(beanName); 259 } 260 } 261 262 /** 263 * Look for handler methods in the specified handler bean. 264 * @param handler either a bean name or an actual handler instance 265 * @see #getMappingForMethod 266 */ 267 protected void detectHandlerMethods(Object handler) { 268 Class<?> handlerType = (handler instanceof String ? 269 obtainApplicationContext().getType((String) handler) : handler.getClass()); 270 271 if (handlerType != null) { 272 Class<?> userType = ClassUtils.getUserClass(handlerType); 273 Map<Method, T> methods = MethodIntrospector.selectMethods(userType, 274 (MethodIntrospector.MetadataLookup<T>) method -> { 275 try { 276 return getMappingForMethod(method, userType); 277 } 278 catch (Throwable ex) { 279 throw new IllegalStateException("Invalid mapping on handler class [" + 280 userType.getName() + "]: " + method, ex); 281 } 282 }); 283 if (logger.isTraceEnabled()) { 284 logger.trace(formatMappings(userType, methods)); 285 } 286 methods.forEach((method, mapping) -> { 287 Method invocableMethod = AopUtils.selectInvocableMethod(method, userType); 288 registerHandlerMethod(handler, invocableMethod, mapping); 289 }); 290 } 291 } 292 293 private String formatMappings(Class<?> userType, Map<Method, T> methods) { 294 String formattedType = Arrays.stream(ClassUtils.getPackageName(userType).split("\\.")) 295 .map(p -> p.substring(0, 1)) 296 .collect(Collectors.joining(".", "", "." + userType.getSimpleName())); 297 Function<Method, String> methodFormatter = method -> Arrays.stream(method.getParameterTypes()) 298 .map(Class::getSimpleName) 299 .collect(Collectors.joining(",", "(", ")")); 300 return methods.entrySet().stream() 301 .map(e -> { 302 Method method = e.getKey(); 303 return e.getValue() + ": " + method.getName() + methodFormatter.apply(method); 304 }) 305 .collect(Collectors.joining("\n\t", "\n\t" + formattedType + ":" + "\n\t", "")); 306 } 307 308 /** 309 * Register a handler method and its unique mapping. Invoked at startup for 310 * each detected handler method. 311 * @param handler the bean name of the handler or the handler instance 312 * @param method the method to register 313 * @param mapping the mapping conditions associated with the handler method 314 * @throws IllegalStateException if another method was already registered 315 * under the same mapping 316 */ 317 protected void registerHandlerMethod(Object handler, Method method, T mapping) { 318 this.mappingRegistry.register(mapping, handler, method); 319 } 320 321 /** 322 * Create the HandlerMethod instance. 323 * @param handler either a bean name or an actual handler instance 324 * @param method the target method 325 * @return the created HandlerMethod 326 */ 327 protected HandlerMethod createHandlerMethod(Object handler, Method method) { 328 if (handler instanceof String) { 329 return new HandlerMethod((String) handler, 330 obtainApplicationContext().getAutowireCapableBeanFactory(), method); 331 } 332 return new HandlerMethod(handler, method); 333 } 334 335 /** 336 * Extract and return the CORS configuration for the mapping. 337 */ 338 @Nullable 339 protected CorsConfiguration initCorsConfiguration(Object handler, Method method, T mapping) { 340 return null; 341 } 342 343 /** 344 * Invoked after all handler methods have been detected. 345 * @param handlerMethods a read-only map with handler methods and mappings. 346 */ 347 protected void handlerMethodsInitialized(Map<T, HandlerMethod> handlerMethods) { 348 // Total includes detected mappings + explicit registrations via registerMapping 349 int total = handlerMethods.size(); 350 if ((logger.isTraceEnabled() && total == 0) || (logger.isDebugEnabled() && total > 0) ) { 351 logger.debug(total + " mappings in " + formatMappingName()); 352 } 353 } 354 355 356 // Handler method lookup 357 358 /** 359 * Look up a handler method for the given request. 360 */ 361 @Override 362 protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception { 363 String lookupPath = getUrlPathHelper().getLookupPathForRequest(request); 364 request.setAttribute(LOOKUP_PATH, lookupPath); 365 this.mappingRegistry.acquireReadLock(); 366 try { 367 HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, request); 368 return (handlerMethod != null ? handlerMethod.createWithResolvedBean() : null); 369 } 370 finally { 371 this.mappingRegistry.releaseReadLock(); 372 } 373 } 374 375 /** 376 * Look up the best-matching handler method for the current request. 377 * If multiple matches are found, the best match is selected. 378 * @param lookupPath mapping lookup path within the current servlet mapping 379 * @param request the current request 380 * @return the best-matching handler method, or {@code null} if no match 381 * @see #handleMatch(Object, String, HttpServletRequest) 382 * @see #handleNoMatch(Set, String, HttpServletRequest) 383 */ 384 @Nullable 385 protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception { 386 List<Match> matches = new ArrayList<>(); 387 List<T> directPathMatches = this.mappingRegistry.getMappingsByUrl(lookupPath); 388 if (directPathMatches != null) { 389 addMatchingMappings(directPathMatches, matches, request); 390 } 391 if (matches.isEmpty()) { 392 // No choice but to go through all mappings... 393 addMatchingMappings(this.mappingRegistry.getMappings().keySet(), matches, request); 394 } 395 396 if (!matches.isEmpty()) { 397 Match bestMatch = matches.get(0); 398 if (matches.size() > 1) { 399 Comparator<Match> comparator = new MatchComparator(getMappingComparator(request)); 400 matches.sort(comparator); 401 bestMatch = matches.get(0); 402 if (logger.isTraceEnabled()) { 403 logger.trace(matches.size() + " matching mappings: " + matches); 404 } 405 if (CorsUtils.isPreFlightRequest(request)) { 406 return PREFLIGHT_AMBIGUOUS_MATCH; 407 } 408 Match secondBestMatch = matches.get(1); 409 if (comparator.compare(bestMatch, secondBestMatch) == 0) { 410 Method m1 = bestMatch.handlerMethod.getMethod(); 411 Method m2 = secondBestMatch.handlerMethod.getMethod(); 412 String uri = request.getRequestURI(); 413 throw new IllegalStateException( 414 "Ambiguous handler methods mapped for '" + uri + "': {" + m1 + ", " + m2 + "}"); 415 } 416 } 417 request.setAttribute(BEST_MATCHING_HANDLER_ATTRIBUTE, bestMatch.handlerMethod); 418 handleMatch(bestMatch.mapping, lookupPath, request); 419 return bestMatch.handlerMethod; 420 } 421 else { 422 return handleNoMatch(this.mappingRegistry.getMappings().keySet(), lookupPath, request); 423 } 424 } 425 426 private void addMatchingMappings(Collection<T> mappings, List<Match> matches, HttpServletRequest request) { 427 for (T mapping : mappings) { 428 T match = getMatchingMapping(mapping, request); 429 if (match != null) { 430 matches.add(new Match(match, this.mappingRegistry.getMappings().get(mapping))); 431 } 432 } 433 } 434 435 /** 436 * Invoked when a matching mapping is found. 437 * @param mapping the matching mapping 438 * @param lookupPath mapping lookup path within the current servlet mapping 439 * @param request the current request 440 */ 441 protected void handleMatch(T mapping, String lookupPath, HttpServletRequest request) { 442 request.setAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, lookupPath); 443 } 444 445 /** 446 * Invoked when no matching mapping is not found. 447 * @param mappings all registered mappings 448 * @param lookupPath mapping lookup path within the current servlet mapping 449 * @param request the current request 450 * @throws ServletException in case of errors 451 */ 452 @Nullable 453 protected HandlerMethod handleNoMatch(Set<T> mappings, String lookupPath, HttpServletRequest request) 454 throws Exception { 455 456 return null; 457 } 458 459 @Override 460 protected boolean hasCorsConfigurationSource(Object handler) { 461 return super.hasCorsConfigurationSource(handler) || 462 (handler instanceof HandlerMethod && this.mappingRegistry.getCorsConfiguration((HandlerMethod) handler) != null); 463 } 464 465 @Override 466 protected CorsConfiguration getCorsConfiguration(Object handler, HttpServletRequest request) { 467 CorsConfiguration corsConfig = super.getCorsConfiguration(handler, request); 468 if (handler instanceof HandlerMethod) { 469 HandlerMethod handlerMethod = (HandlerMethod) handler; 470 if (handlerMethod.equals(PREFLIGHT_AMBIGUOUS_MATCH)) { 471 return AbstractHandlerMethodMapping.ALLOW_CORS_CONFIG; 472 } 473 else { 474 CorsConfiguration corsConfigFromMethod = this.mappingRegistry.getCorsConfiguration(handlerMethod); 475 corsConfig = (corsConfig != null ? corsConfig.combine(corsConfigFromMethod) : corsConfigFromMethod); 476 } 477 } 478 return corsConfig; 479 } 480 481 482 // Abstract template methods 483 484 /** 485 * Whether the given type is a handler with handler methods. 486 * @param beanType the type of the bean being checked 487 * @return "true" if this a handler type, "false" otherwise. 488 */ 489 protected abstract boolean isHandler(Class<?> beanType); 490 491 /** 492 * Provide the mapping for a handler method. A method for which no 493 * mapping can be provided is not a handler method. 494 * @param method the method to provide a mapping for 495 * @param handlerType the handler type, possibly a sub-type of the method's 496 * declaring class 497 * @return the mapping, or {@code null} if the method is not mapped 498 */ 499 @Nullable 500 protected abstract T getMappingForMethod(Method method, Class<?> handlerType); 501 502 /** 503 * Extract and return the URL paths contained in the supplied mapping. 504 */ 505 protected abstract Set<String> getMappingPathPatterns(T mapping); 506 507 /** 508 * Check if a mapping matches the current request and return a (potentially 509 * new) mapping with conditions relevant to the current request. 510 * @param mapping the mapping to get a match for 511 * @param request the current HTTP servlet request 512 * @return the match, or {@code null} if the mapping doesn't match 513 */ 514 @Nullable 515 protected abstract T getMatchingMapping(T mapping, HttpServletRequest request); 516 517 /** 518 * Return a comparator for sorting matching mappings. 519 * The returned comparator should sort 'better' matches higher. 520 * @param request the current request 521 * @return the comparator (never {@code null}) 522 */ 523 protected abstract Comparator<T> getMappingComparator(HttpServletRequest request); 524 525 526 /** 527 * A registry that maintains all mappings to handler methods, exposing methods 528 * to perform lookups and providing concurrent access. 529 * <p>Package-private for testing purposes. 530 */ 531 class MappingRegistry { 532 533 private final Map<T, MappingRegistration<T>> registry = new HashMap<>(); 534 535 private final Map<T, HandlerMethod> mappingLookup = new LinkedHashMap<>(); 536 537 private final MultiValueMap<String, T> urlLookup = new LinkedMultiValueMap<>(); 538 539 private final Map<String, List<HandlerMethod>> nameLookup = new ConcurrentHashMap<>(); 540 541 private final Map<HandlerMethod, CorsConfiguration> corsLookup = new ConcurrentHashMap<>(); 542 543 private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock(); 544 545 /** 546 * Return all mappings and handler methods. Not thread-safe. 547 * @see #acquireReadLock() 548 */ 549 public Map<T, HandlerMethod> getMappings() { 550 return this.mappingLookup; 551 } 552 553 /** 554 * Return matches for the given URL path. Not thread-safe. 555 * @see #acquireReadLock() 556 */ 557 @Nullable 558 public List<T> getMappingsByUrl(String urlPath) { 559 return this.urlLookup.get(urlPath); 560 } 561 562 /** 563 * Return handler methods by mapping name. Thread-safe for concurrent use. 564 */ 565 public List<HandlerMethod> getHandlerMethodsByMappingName(String mappingName) { 566 return this.nameLookup.get(mappingName); 567 } 568 569 /** 570 * Return CORS configuration. Thread-safe for concurrent use. 571 */ 572 @Nullable 573 public CorsConfiguration getCorsConfiguration(HandlerMethod handlerMethod) { 574 HandlerMethod original = handlerMethod.getResolvedFromHandlerMethod(); 575 return this.corsLookup.get(original != null ? original : handlerMethod); 576 } 577 578 /** 579 * Acquire the read lock when using getMappings and getMappingsByUrl. 580 */ 581 public void acquireReadLock() { 582 this.readWriteLock.readLock().lock(); 583 } 584 585 /** 586 * Release the read lock after using getMappings and getMappingsByUrl. 587 */ 588 public void releaseReadLock() { 589 this.readWriteLock.readLock().unlock(); 590 } 591 592 public void register(T mapping, Object handler, Method method) { 593 // Assert that the handler method is not a suspending one. 594 if (KotlinDetector.isKotlinType(method.getDeclaringClass())) { 595 Class<?>[] parameterTypes = method.getParameterTypes(); 596 if ((parameterTypes.length > 0) && "kotlin.coroutines.Continuation".equals(parameterTypes[parameterTypes.length - 1].getName())) { 597 throw new IllegalStateException("Unsupported suspending handler method detected: " + method); 598 } 599 } 600 this.readWriteLock.writeLock().lock(); 601 try { 602 HandlerMethod handlerMethod = createHandlerMethod(handler, method); 603 validateMethodMapping(handlerMethod, mapping); 604 this.mappingLookup.put(mapping, handlerMethod); 605 606 List<String> directUrls = getDirectUrls(mapping); 607 for (String url : directUrls) { 608 this.urlLookup.add(url, mapping); 609 } 610 611 String name = null; 612 if (getNamingStrategy() != null) { 613 name = getNamingStrategy().getName(handlerMethod, mapping); 614 addMappingName(name, handlerMethod); 615 } 616 617 CorsConfiguration corsConfig = initCorsConfiguration(handler, method, mapping); 618 if (corsConfig != null) { 619 this.corsLookup.put(handlerMethod, corsConfig); 620 } 621 622 this.registry.put(mapping, new MappingRegistration<>(mapping, handlerMethod, directUrls, name)); 623 } 624 finally { 625 this.readWriteLock.writeLock().unlock(); 626 } 627 } 628 629 private void validateMethodMapping(HandlerMethod handlerMethod, T mapping) { 630 // Assert that the supplied mapping is unique. 631 HandlerMethod existingHandlerMethod = this.mappingLookup.get(mapping); 632 if (existingHandlerMethod != null && !existingHandlerMethod.equals(handlerMethod)) { 633 throw new IllegalStateException( 634 "Ambiguous mapping. Cannot map '" + handlerMethod.getBean() + "' method \n" + 635 handlerMethod + "\nto " + mapping + ": There is already '" + 636 existingHandlerMethod.getBean() + "' bean method\n" + existingHandlerMethod + " mapped."); 637 } 638 } 639 640 private List<String> getDirectUrls(T mapping) { 641 List<String> urls = new ArrayList<>(1); 642 for (String path : getMappingPathPatterns(mapping)) { 643 if (!getPathMatcher().isPattern(path)) { 644 urls.add(path); 645 } 646 } 647 return urls; 648 } 649 650 private void addMappingName(String name, HandlerMethod handlerMethod) { 651 List<HandlerMethod> oldList = this.nameLookup.get(name); 652 if (oldList == null) { 653 oldList = Collections.emptyList(); 654 } 655 656 for (HandlerMethod current : oldList) { 657 if (handlerMethod.equals(current)) { 658 return; 659 } 660 } 661 662 List<HandlerMethod> newList = new ArrayList<>(oldList.size() + 1); 663 newList.addAll(oldList); 664 newList.add(handlerMethod); 665 this.nameLookup.put(name, newList); 666 } 667 668 public void unregister(T mapping) { 669 this.readWriteLock.writeLock().lock(); 670 try { 671 MappingRegistration<T> definition = this.registry.remove(mapping); 672 if (definition == null) { 673 return; 674 } 675 676 this.mappingLookup.remove(definition.getMapping()); 677 678 for (String url : definition.getDirectUrls()) { 679 List<T> list = this.urlLookup.get(url); 680 if (list != null) { 681 list.remove(definition.getMapping()); 682 if (list.isEmpty()) { 683 this.urlLookup.remove(url); 684 } 685 } 686 } 687 688 removeMappingName(definition); 689 690 this.corsLookup.remove(definition.getHandlerMethod()); 691 } 692 finally { 693 this.readWriteLock.writeLock().unlock(); 694 } 695 } 696 697 private void removeMappingName(MappingRegistration<T> definition) { 698 String name = definition.getMappingName(); 699 if (name == null) { 700 return; 701 } 702 HandlerMethod handlerMethod = definition.getHandlerMethod(); 703 List<HandlerMethod> oldList = this.nameLookup.get(name); 704 if (oldList == null) { 705 return; 706 } 707 if (oldList.size() <= 1) { 708 this.nameLookup.remove(name); 709 return; 710 } 711 List<HandlerMethod> newList = new ArrayList<>(oldList.size() - 1); 712 for (HandlerMethod current : oldList) { 713 if (!current.equals(handlerMethod)) { 714 newList.add(current); 715 } 716 } 717 this.nameLookup.put(name, newList); 718 } 719 } 720 721 722 private static class MappingRegistration<T> { 723 724 private final T mapping; 725 726 private final HandlerMethod handlerMethod; 727 728 private final List<String> directUrls; 729 730 @Nullable 731 private final String mappingName; 732 733 public MappingRegistration(T mapping, HandlerMethod handlerMethod, 734 @Nullable List<String> directUrls, @Nullable String mappingName) { 735 736 Assert.notNull(mapping, "Mapping must not be null"); 737 Assert.notNull(handlerMethod, "HandlerMethod must not be null"); 738 this.mapping = mapping; 739 this.handlerMethod = handlerMethod; 740 this.directUrls = (directUrls != null ? directUrls : Collections.emptyList()); 741 this.mappingName = mappingName; 742 } 743 744 public T getMapping() { 745 return this.mapping; 746 } 747 748 public HandlerMethod getHandlerMethod() { 749 return this.handlerMethod; 750 } 751 752 public List<String> getDirectUrls() { 753 return this.directUrls; 754 } 755 756 @Nullable 757 public String getMappingName() { 758 return this.mappingName; 759 } 760 } 761 762 763 /** 764 * A thin wrapper around a matched HandlerMethod and its mapping, for the purpose of 765 * comparing the best match with a comparator in the context of the current request. 766 */ 767 private class Match { 768 769 private final T mapping; 770 771 private final HandlerMethod handlerMethod; 772 773 public Match(T mapping, HandlerMethod handlerMethod) { 774 this.mapping = mapping; 775 this.handlerMethod = handlerMethod; 776 } 777 778 @Override 779 public String toString() { 780 return this.mapping.toString(); 781 } 782 } 783 784 785 private class MatchComparator implements Comparator<Match> { 786 787 private final Comparator<T> comparator; 788 789 public MatchComparator(Comparator<T> comparator) { 790 this.comparator = comparator; 791 } 792 793 @Override 794 public int compare(Match match1, Match match2) { 795 return this.comparator.compare(match1.mapping, match2.mapping); 796 } 797 } 798 799 800 private static class EmptyHandler { 801 802 @SuppressWarnings("unused") 803 public void handle() { 804 throw new UnsupportedOperationException("Not implemented"); 805 } 806 } 807 808 /** 809 * Inner class to avoid a hard dependency on Kotlin at runtime. 810 */ 811 private static class KotlinDelegate { 812 813 static private boolean isSuspend(Method method) { 814 KFunction<?> function = ReflectJvmMapping.getKotlinFunction(method); 815 return function != null && function.isSuspend(); 816 } 817 } 818 819}