/* * Copyright 2002-2017 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.web.reactive.result.method; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.locks.ReentrantReadWriteLock; import reactor.core.publisher.Mono; import org.springframework.aop.support.AopUtils; import org.springframework.beans.factory.InitializingBean; import org.springframework.core.MethodIntrospector; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.reactive.CorsUtils; import org.springframework.web.method.HandlerMethod; import org.springframework.web.reactive.HandlerMapping; import org.springframework.web.reactive.handler.AbstractHandlerMapping; import org.springframework.web.server.ServerWebExchange; /** * Abstract base class for {@link HandlerMapping} implementations that define * a mapping between a request and a {@link HandlerMethod}. * * <p>For each registered handler method, a unique mapping is maintained with * subclasses defining the details of the mapping type {@code <T>}. * * @author Rossen Stoyanchev * @since 5.0 * @param <T> The mapping for a {@link HandlerMethod} containing the conditions * needed to match the handler method to incoming request. */ public abstract class AbstractHandlerMethodMapping<T> extends AbstractHandlerMapping implements InitializingBean { /** * Bean name prefix for target beans behind scoped proxies. Used to exclude those * targets from handler method detection, in favor of the corresponding proxies. * <p>We're not checking the autowire-candidate status here, which is how the * proxy target filtering problem is being handled at the autowiring level, * since autowire-candidate may have been turned to {@code false} for other * reasons, while still expecting the bean to be eligible for handler methods. * <p>Originally defined in {@link org.springframework.aop.scope.ScopedProxyUtils} * but duplicated here to avoid a hard dependency on the spring-aop module. */ private static final String SCOPED_TARGET_NAME_PREFIX = "scopedTarget."; /** * HandlerMethod to return on a pre-flight request match when the request * mappings are more nuanced than the access control headers. */ private static final HandlerMethod PREFLIGHT_AMBIGUOUS_MATCH = new HandlerMethod(new PreFlightAmbiguousMatchHandler(), ClassUtils.getMethod(PreFlightAmbiguousMatchHandler.class, "handle")); private static final CorsConfiguration ALLOW_CORS_CONFIG = new CorsConfiguration(); static { ALLOW_CORS_CONFIG.addAllowedOrigin("*"); ALLOW_CORS_CONFIG.addAllowedMethod("*"); ALLOW_CORS_CONFIG.addAllowedHeader("*"); ALLOW_CORS_CONFIG.setAllowCredentials(true); } private final MappingRegistry mappingRegistry = new MappingRegistry(); // TODO: handlerMethodMappingNamingStrategy /** * Return a (read-only) map with all mappings and HandlerMethod's. */ public Map<T, HandlerMethod> getHandlerMethods() { this.mappingRegistry.acquireReadLock(); try { return Collections.unmodifiableMap(this.mappingRegistry.getMappings()); } finally { this.mappingRegistry.releaseReadLock(); } } /** * Return the internal mapping registry. Provided for testing purposes. */ MappingRegistry getMappingRegistry() { return this.mappingRegistry; } /** * Register the given mapping. * <p>This method may be invoked at runtime after initialization has completed. * @param mapping the mapping for the handler method * @param handler the handler * @param method the method */ public void registerMapping(T mapping, Object handler, Method method) { this.mappingRegistry.register(mapping, handler, method); } /** * Un-register the given mapping. * <p>This method may be invoked at runtime after initialization has completed. * @param mapping the mapping to unregister */ public void unregisterMapping(T mapping) { this.mappingRegistry.unregister(mapping); } // Handler method detection /** * Detects handler methods at initialization. */ @Override public void afterPropertiesSet() { initHandlerMethods(); } /** * Scan beans in the ApplicationContext, detect and register handler methods. * @see #isHandler(Class) * @see #getMappingForMethod(Method, Class) * @see #handlerMethodsInitialized(Map) */ protected void initHandlerMethods() { if (logger.isDebugEnabled()) { logger.debug("Looking for request mappings in application context: " + getApplicationContext()); } String[] beanNames = getApplicationContext().getBeanNamesForType(Object.class); for (String beanName : beanNames) { if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) { Class<?> beanType = null; try { beanType = getApplicationContext().getType(beanName); } catch (Throwable ex) { // An unresolvable bean type, probably from a lazy bean - let's ignore it. if (logger.isDebugEnabled()) { logger.debug("Could not resolve target class for bean with name '" + beanName + "'", ex); } } if (beanType != null && isHandler(beanType)) { detectHandlerMethods(beanName); } } } handlerMethodsInitialized(getHandlerMethods()); } /** * Look for handler methods in a handler. * @param handler the bean name of a handler or a handler instance */ protected void detectHandlerMethods(final Object handler) { Class<?> handlerType = (handler instanceof String ? getApplicationContext().getType((String) handler) : handler.getClass()); final Class<?> userType = ClassUtils.getUserClass(handlerType); Map<Method, T> methods = MethodIntrospector.selectMethods(userType, (MethodIntrospector.MetadataLookup<T>) method -> getMappingForMethod(method, userType)); if (logger.isDebugEnabled()) { logger.debug(methods.size() + " request handler methods found on " + userType + ": " + methods); } methods.forEach((key, mapping) -> { Method invocableMethod = AopUtils.selectInvocableMethod(key, userType); registerHandlerMethod(handler, invocableMethod, mapping); }); } /** * Register a handler method and its unique mapping. Invoked at startup for * each detected handler method. * @param handler the bean name of the handler or the handler instance * @param method the method to register * @param mapping the mapping conditions associated with the handler method * @throws IllegalStateException if another method was already registered * under the same mapping */ protected void registerHandlerMethod(Object handler, Method method, T mapping) { this.mappingRegistry.register(mapping, handler, method); } /** * Create the HandlerMethod instance. * @param handler either a bean name or an actual handler instance * @param method the target method * @return the created HandlerMethod */ protected HandlerMethod createHandlerMethod(Object handler, Method method) { HandlerMethod handlerMethod; if (handler instanceof String) { String beanName = (String) handler; handlerMethod = new HandlerMethod(beanName, getApplicationContext().getAutowireCapableBeanFactory(), method); } else { handlerMethod = new HandlerMethod(handler, method); } return handlerMethod; } /** * Extract and return the CORS configuration for the mapping. */ protected CorsConfiguration initCorsConfiguration(Object handler, Method method, T mapping) { return null; } /** * Invoked after all handler methods have been detected. * @param handlerMethods a read-only map with handler methods and mappings. */ protected void handlerMethodsInitialized(Map<T, HandlerMethod> handlerMethods) { } // Handler method lookup /** * Look up a handler method for the given request. * @param exchange the current exchange */ @Override public Mono<HandlerMethod> getHandlerInternal(ServerWebExchange exchange) { String lookupPath = getPathHelper().getLookupPathForRequest(exchange); if (logger.isDebugEnabled()) { logger.debug("Looking up handler method for path " + lookupPath); } this.mappingRegistry.acquireReadLock(); try { HandlerMethod handlerMethod; try { handlerMethod = lookupHandlerMethod(lookupPath, exchange); } catch (Exception ex) { return Mono.error(ex); } if (logger.isDebugEnabled()) { if (handlerMethod != null) { logger.debug("Returning handler method [" + handlerMethod + "]"); } else { logger.debug("Did not find handler method for [" + lookupPath + "]"); } } if (handlerMethod != null) { handlerMethod = handlerMethod.createWithResolvedBean(); } return Mono.justOrEmpty(handlerMethod); } finally { this.mappingRegistry.releaseReadLock(); } } /** * Look up the best-matching handler method for the current request. * If multiple matches are found, the best match is selected. * @param lookupPath mapping lookup path within the current servlet mapping * @param exchange the current exchange * @return the best-matching handler method, or {@code null} if no match * @see #handleMatch(Object, String, ServerWebExchange) * @see #handleNoMatch(Set, String, ServerWebExchange) */ protected HandlerMethod lookupHandlerMethod(String lookupPath, ServerWebExchange exchange) throws Exception { List<Match> matches = new ArrayList<Match>(); List<T> directPathMatches = this.mappingRegistry.getMappingsByUrl(lookupPath); if (directPathMatches != null) { addMatchingMappings(directPathMatches, matches, exchange); } if (matches.isEmpty()) { // No choice but to go through all mappings... addMatchingMappings(this.mappingRegistry.getMappings().keySet(), matches, exchange); } if (!matches.isEmpty()) { Comparator<Match> comparator = new MatchComparator(getMappingComparator(exchange)); Collections.sort(matches, comparator); if (logger.isTraceEnabled()) { logger.trace("Found " + matches.size() + " matching mapping(s) for [" + lookupPath + "] : " + matches); } Match bestMatch = matches.get(0); if (matches.size() > 1) { if (CorsUtils.isPreFlightRequest(exchange.getRequest())) { return PREFLIGHT_AMBIGUOUS_MATCH; } Match secondBestMatch = matches.get(1); if (comparator.compare(bestMatch, secondBestMatch) == 0) { Method m1 = bestMatch.handlerMethod.getMethod(); Method m2 = secondBestMatch.handlerMethod.getMethod(); throw new IllegalStateException("Ambiguous handler methods mapped for HTTP path '" + lookupPath + "': {" + m1 + ", " + m2 + "}"); } } handleMatch(bestMatch.mapping, lookupPath, exchange); return bestMatch.handlerMethod; } else { return handleNoMatch(this.mappingRegistry.getMappings().keySet(), lookupPath, exchange); } } private void addMatchingMappings(Collection<T> mappings, List<Match> matches, ServerWebExchange exchange) { for (T mapping : mappings) { T match = getMatchingMapping(mapping, exchange); if (match != null) { matches.add(new Match(match, this.mappingRegistry.getMappings().get(mapping))); } } } /** * Invoked when a matching mapping is found. * @param mapping the matching mapping * @param lookupPath mapping lookup path within the current servlet mapping * @param exchange the current exchange */ protected void handleMatch(T mapping, String lookupPath, ServerWebExchange exchange) { } /** * Invoked when no matching mapping is not found. * @param mappings all registered mappings * @param lookupPath mapping lookup path within the current servlet mapping * @param exchange the current exchange * @return an alternative HandlerMethod or {@code null} * @throws Exception provides details that can be translated into an error status code */ protected HandlerMethod handleNoMatch(Set<T> mappings, String lookupPath, ServerWebExchange exchange) throws Exception { return null; } @Override protected CorsConfiguration getCorsConfiguration(Object handler, ServerWebExchange exchange) { CorsConfiguration corsConfig = super.getCorsConfiguration(handler, exchange); if (handler instanceof HandlerMethod) { HandlerMethod handlerMethod = (HandlerMethod) handler; if (handlerMethod.equals(PREFLIGHT_AMBIGUOUS_MATCH)) { return ALLOW_CORS_CONFIG; } CorsConfiguration methodConfig = this.mappingRegistry.getCorsConfiguration(handlerMethod); corsConfig = (corsConfig != null ? corsConfig.combine(methodConfig) : methodConfig); } return corsConfig; } // Abstract template methods /** * Whether the given type is a handler with handler methods. * @param beanType the type of the bean being checked * @return "true" if this a handler type, "false" otherwise. */ protected abstract boolean isHandler(Class<?> beanType); /** * Provide the mapping for a handler method. A method for which no * mapping can be provided is not a handler method. * @param method the method to provide a mapping for * @param handlerType the handler type, possibly a sub-type of the method's * declaring class * @return the mapping, or {@code null} if the method is not mapped */ protected abstract T getMappingForMethod(Method method, Class<?> handlerType); /** * Extract and return the URL paths contained in a mapping. */ protected abstract Set<String> getMappingPathPatterns(T mapping); /** * Check if a mapping matches the current request and return a (potentially * new) mapping with conditions relevant to the current request. * @param mapping the mapping to get a match for * @param exchange the current exchange * @return the match, or {@code null} if the mapping doesn't match */ protected abstract T getMatchingMapping(T mapping, ServerWebExchange exchange); /** * Return a comparator for sorting matching mappings. * The returned comparator should sort 'better' matches higher. * @param exchange the current exchange * @return the comparator (never {@code null}) */ protected abstract Comparator<T> getMappingComparator(ServerWebExchange exchange); /** * A registry that maintains all mappings to handler methods, exposing methods * to perform lookups and providing concurrent access. * * <p>Package-private for testing purposes. */ class MappingRegistry { private final Map<T, MappingRegistration<T>> registry = new HashMap<>(); private final Map<T, HandlerMethod> mappingLookup = new LinkedHashMap<>(); private final MultiValueMap<String, T> urlLookup = new LinkedMultiValueMap<>(); private final Map<HandlerMethod, CorsConfiguration> corsLookup = new ConcurrentHashMap<>(); private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock(); /** * Return all mappings and handler methods. Not thread-safe. * @see #acquireReadLock() */ public Map<T, HandlerMethod> getMappings() { return this.mappingLookup; } /** * Return matches for the given URL path. Not thread-safe. * @see #acquireReadLock() */ public List<T> getMappingsByUrl(String urlPath) { return this.urlLookup.get(urlPath); } /** * Return CORS configuration. Thread-safe for concurrent use. */ public CorsConfiguration getCorsConfiguration(HandlerMethod handlerMethod) { HandlerMethod original = handlerMethod.getResolvedFromHandlerMethod(); return this.corsLookup.get(original != null ? original : handlerMethod); } /** * Acquire the read lock when using getMappings and getMappingsByUrl. */ public void acquireReadLock() { this.readWriteLock.readLock().lock(); } /** * Release the read lock after using getMappings and getMappingsByUrl. */ public void releaseReadLock() { this.readWriteLock.readLock().unlock(); } public void register(T mapping, Object handler, Method method) { this.readWriteLock.writeLock().lock(); try { HandlerMethod handlerMethod = createHandlerMethod(handler, method); assertUniqueMethodMapping(handlerMethod, mapping); if (logger.isInfoEnabled()) { logger.info("Mapped \"" + mapping + "\" onto " + handlerMethod); } this.mappingLookup.put(mapping, handlerMethod); List<String> directUrls = getDirectUrls(mapping); for (String url : directUrls) { this.urlLookup.add(url, mapping); } CorsConfiguration corsConfig = initCorsConfiguration(handler, method, mapping); if (corsConfig != null) { this.corsLookup.put(handlerMethod, corsConfig); } this.registry.put(mapping, new MappingRegistration<>(mapping, handlerMethod, directUrls)); } finally { this.readWriteLock.writeLock().unlock(); } } private void assertUniqueMethodMapping(HandlerMethod newHandlerMethod, T mapping) { HandlerMethod handlerMethod = this.mappingLookup.get(mapping); if (handlerMethod != null && !handlerMethod.equals(newHandlerMethod)) { throw new IllegalStateException( "Ambiguous mapping. Cannot map '" + newHandlerMethod.getBean() + "' method \n" + newHandlerMethod + "\nto " + mapping + ": There is already '" + handlerMethod.getBean() + "' bean method\n" + handlerMethod + " mapped."); } } private List<String> getDirectUrls(T mapping) { List<String> urls = new ArrayList<>(1); for (String path : getMappingPathPatterns(mapping)) { if (!getPathMatcher().isPattern(path)) { urls.add(path); } } return urls; } public void unregister(T mapping) { this.readWriteLock.writeLock().lock(); try { MappingRegistration<T> definition = this.registry.remove(mapping); if (definition == null) { return; } this.mappingLookup.remove(definition.getMapping()); for (String url : definition.getDirectUrls()) { List<T> list = this.urlLookup.get(url); if (list != null) { list.remove(definition.getMapping()); if (list.isEmpty()) { this.urlLookup.remove(url); } } } this.corsLookup.remove(definition.getHandlerMethod()); } finally { this.readWriteLock.writeLock().unlock(); } } } private static class MappingRegistration<T> { private final T mapping; private final HandlerMethod handlerMethod; private final List<String> directUrls; public MappingRegistration(T mapping, HandlerMethod handlerMethod, List<String> directUrls) { Assert.notNull(mapping, "Mapping must not be null"); Assert.notNull(handlerMethod, "HandlerMethod must not be null"); this.mapping = mapping; this.handlerMethod = handlerMethod; this.directUrls = (directUrls != null ? directUrls : Collections.emptyList()); } public T getMapping() { return this.mapping; } public HandlerMethod getHandlerMethod() { return this.handlerMethod; } public List<String> getDirectUrls() { return this.directUrls; } } /** * A thin wrapper around a matched HandlerMethod and its mapping, for the purpose of * comparing the best match with a comparator in the context of the current request. */ private class Match { private final T mapping; private final HandlerMethod handlerMethod; public Match(T mapping, HandlerMethod handlerMethod) { this.mapping = mapping; this.handlerMethod = handlerMethod; } @Override public String toString() { return this.mapping.toString(); } } private class MatchComparator implements Comparator<Match> { private final Comparator<T> comparator; public MatchComparator(Comparator<T> comparator) { this.comparator = comparator; } @Override public int compare(Match match1, Match match2) { return this.comparator.compare(match1.mapping, match2.mapping); } } private static class PreFlightAmbiguousMatchHandler { @SuppressWarnings("unused") public void handle() { throw new UnsupportedOperationException("Not implemented"); } } }