All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.springframework.web.servlet.handler.AbstractHandlerMethodMapping Maven / Gradle / Ivy

There is a newer version: 6.1.6
Show newest version
/*
 * Copyright 2002-2021 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
 *
 *      https://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.servlet.handler;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.function.Function;
import java.util.stream.Collectors;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;

import org.springframework.aop.support.AopUtils;
import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.core.MethodIntrospector;
import org.springframework.lang.Nullable;
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.CorsUtils;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerMapping;
import org.springframework.web.util.pattern.PathPatternParser;

/**
 * Abstract base class for {@link HandlerMapping} implementations that define
 * a mapping between a request and a {@link HandlerMethod}.
 *
 * 

For each registered handler method, a unique mapping is maintained with * subclasses defining the details of the mapping type {@code }. * * @author Arjen Poutsma * @author Rossen Stoyanchev * @author Juergen Hoeller * @author Sam Brannen * @since 3.1 * @param the mapping for a {@link HandlerMethod} containing the conditions * needed to match the handler method to an incoming request. */ public abstract class AbstractHandlerMethodMapping 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. *

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. *

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."; private static final HandlerMethod PREFLIGHT_AMBIGUOUS_MATCH = new HandlerMethod(new EmptyHandler(), ClassUtils.getMethod(EmptyHandler.class, "handle")); private static final CorsConfiguration ALLOW_CORS_CONFIG = new CorsConfiguration(); static { ALLOW_CORS_CONFIG.addAllowedOriginPattern("*"); ALLOW_CORS_CONFIG.addAllowedMethod("*"); ALLOW_CORS_CONFIG.addAllowedHeader("*"); ALLOW_CORS_CONFIG.setAllowCredentials(true); } private boolean detectHandlerMethodsInAncestorContexts = false; @Nullable private HandlerMethodMappingNamingStrategy namingStrategy; private final MappingRegistry mappingRegistry = new MappingRegistry(); @Override public void setPatternParser(PathPatternParser patternParser) { Assert.state(this.mappingRegistry.getRegistrations().isEmpty(), "PathPatternParser must be set before the initialization of " + "request mappings through InitializingBean#afterPropertiesSet."); super.setPatternParser(patternParser); } /** * Whether to detect handler methods in beans in ancestor ApplicationContexts. *

Default is "false": Only beans in the current ApplicationContext are * considered, i.e. only in the context that this HandlerMapping itself * is defined in (typically the current DispatcherServlet's context). *

Switch this flag on to detect handler beans in ancestor contexts * (typically the Spring root WebApplicationContext) as well. * @see #getCandidateBeanNames() */ public void setDetectHandlerMethodsInAncestorContexts(boolean detectHandlerMethodsInAncestorContexts) { this.detectHandlerMethodsInAncestorContexts = detectHandlerMethodsInAncestorContexts; } /** * Configure the naming strategy to use for assigning a default name to every * mapped handler method. *

The default naming strategy is based on the capital letters of the * class name followed by "#" and then the method name, e.g. "TC#getFoo" * for a class named TestController with method getFoo. */ public void setHandlerMethodMappingNamingStrategy(HandlerMethodMappingNamingStrategy namingStrategy) { this.namingStrategy = namingStrategy; } /** * Return the configured naming strategy or {@code null}. */ @Nullable public HandlerMethodMappingNamingStrategy getNamingStrategy() { return this.namingStrategy; } /** * Return a (read-only) map with all mappings and HandlerMethod's. */ public Map getHandlerMethods() { this.mappingRegistry.acquireReadLock(); try { return Collections.unmodifiableMap( this.mappingRegistry.getRegistrations().entrySet().stream() .collect(Collectors.toMap(Map.Entry::getKey, entry -> entry.getValue().handlerMethod))); } finally { this.mappingRegistry.releaseReadLock(); } } /** * Return the handler methods for the given mapping name. * @param mappingName the mapping name * @return a list of matching HandlerMethod's or {@code null}; the returned * list will never be modified and is safe to iterate. * @see #setHandlerMethodMappingNamingStrategy */ @Nullable public List getHandlerMethodsForMappingName(String mappingName) { return this.mappingRegistry.getHandlerMethodsByMappingName(mappingName); } /** * Return the internal mapping registry. Provided for testing purposes. */ MappingRegistry getMappingRegistry() { return this.mappingRegistry; } /** * Register the given mapping. *

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) { if (logger.isTraceEnabled()) { logger.trace("Register \"" + mapping + "\" to " + method.toGenericString()); } this.mappingRegistry.register(mapping, handler, method); } /** * Un-register the given mapping. *

This method may be invoked at runtime after initialization has completed. * @param mapping the mapping to unregister */ public void unregisterMapping(T mapping) { if (logger.isTraceEnabled()) { logger.trace("Unregister mapping \"" + mapping + "\""); } this.mappingRegistry.unregister(mapping); } // Handler method detection /** * Detects handler methods at initialization. * @see #initHandlerMethods */ @Override public void afterPropertiesSet() { initHandlerMethods(); } /** * Scan beans in the ApplicationContext, detect and register handler methods. * @see #getCandidateBeanNames() * @see #processCandidateBean * @see #handlerMethodsInitialized */ protected void initHandlerMethods() { for (String beanName : getCandidateBeanNames()) { if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) { processCandidateBean(beanName); } } handlerMethodsInitialized(getHandlerMethods()); } /** * Determine the names of candidate beans in the application context. * @since 5.1 * @see #setDetectHandlerMethodsInAncestorContexts * @see BeanFactoryUtils#beanNamesForTypeIncludingAncestors */ protected String[] getCandidateBeanNames() { return (this.detectHandlerMethodsInAncestorContexts ? BeanFactoryUtils.beanNamesForTypeIncludingAncestors(obtainApplicationContext(), Object.class) : obtainApplicationContext().getBeanNamesForType(Object.class)); } /** * Determine the type of the specified candidate bean and call * {@link #detectHandlerMethods} if identified as a handler type. *

This implementation avoids bean creation through checking * {@link org.springframework.beans.factory.BeanFactory#getType} * and calling {@link #detectHandlerMethods} with the bean name. * @param beanName the name of the candidate bean * @since 5.1 * @see #isHandler * @see #detectHandlerMethods */ protected void processCandidateBean(String beanName) { Class beanType = null; try { beanType = obtainApplicationContext().getType(beanName); } catch (Throwable ex) { // An unresolvable bean type, probably from a lazy bean - let's ignore it. if (logger.isTraceEnabled()) { logger.trace("Could not resolve type for bean '" + beanName + "'", ex); } } if (beanType != null && isHandler(beanType)) { detectHandlerMethods(beanName); } } /** * Look for handler methods in the specified handler bean. * @param handler either a bean name or an actual handler instance * @see #getMappingForMethod */ protected void detectHandlerMethods(Object handler) { Class handlerType = (handler instanceof String ? obtainApplicationContext().getType((String) handler) : handler.getClass()); if (handlerType != null) { Class userType = ClassUtils.getUserClass(handlerType); Map methods = MethodIntrospector.selectMethods(userType, (MethodIntrospector.MetadataLookup) method -> { try { return getMappingForMethod(method, userType); } catch (Throwable ex) { throw new IllegalStateException("Invalid mapping on handler class [" + userType.getName() + "]: " + method, ex); } }); if (logger.isTraceEnabled()) { logger.trace(formatMappings(userType, methods)); } methods.forEach((method, mapping) -> { Method invocableMethod = AopUtils.selectInvocableMethod(method, userType); registerHandlerMethod(handler, invocableMethod, mapping); }); } } private String formatMappings(Class userType, Map methods) { String formattedType = Arrays.stream(ClassUtils.getPackageName(userType).split("\\.")) .map(p -> p.substring(0, 1)) .collect(Collectors.joining(".", "", "." + userType.getSimpleName())); Function methodFormatter = method -> Arrays.stream(method.getParameterTypes()) .map(Class::getSimpleName) .collect(Collectors.joining(",", "(", ")")); return methods.entrySet().stream() .map(e -> { Method method = e.getKey(); return e.getValue() + ": " + method.getName() + methodFormatter.apply(method); }) .collect(Collectors.joining("\n\t", "\n\t" + formattedType + ":" + "\n\t", "")); } /** * 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) { if (handler instanceof String) { return new HandlerMethod((String) handler, obtainApplicationContext().getAutowireCapableBeanFactory(), method); } return new HandlerMethod(handler, method); } /** * Extract and return the CORS configuration for the mapping. */ @Nullable 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 handlerMethods) { // Total includes detected mappings + explicit registrations via registerMapping int total = handlerMethods.size(); if ((logger.isTraceEnabled() && total == 0) || (logger.isDebugEnabled() && total > 0) ) { logger.debug(total + " mappings in " + formatMappingName()); } } // Handler method lookup /** * Look up a handler method for the given request. */ @Override protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception { String lookupPath = initLookupPath(request); this.mappingRegistry.acquireReadLock(); try { HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, request); return (handlerMethod != null ? handlerMethod.createWithResolvedBean() : null); } 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 request the current request * @return the best-matching handler method, or {@code null} if no match * @see #handleMatch(Object, String, HttpServletRequest) * @see #handleNoMatch(Set, String, HttpServletRequest) */ @Nullable protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception { List matches = new ArrayList<>(); List directPathMatches = this.mappingRegistry.getMappingsByDirectPath(lookupPath); if (directPathMatches != null) { addMatchingMappings(directPathMatches, matches, request); } if (matches.isEmpty()) { addMatchingMappings(this.mappingRegistry.getRegistrations().keySet(), matches, request); } if (!matches.isEmpty()) { Match bestMatch = matches.get(0); if (matches.size() > 1) { Comparator comparator = new MatchComparator(getMappingComparator(request)); matches.sort(comparator); bestMatch = matches.get(0); if (logger.isTraceEnabled()) { logger.trace(matches.size() + " matching mappings: " + matches); } if (CorsUtils.isPreFlightRequest(request)) { for (Match match : matches) { if (match.hasCorsConfig()) { return PREFLIGHT_AMBIGUOUS_MATCH; } } } else { Match secondBestMatch = matches.get(1); if (comparator.compare(bestMatch, secondBestMatch) == 0) { Method m1 = bestMatch.getHandlerMethod().getMethod(); Method m2 = secondBestMatch.getHandlerMethod().getMethod(); String uri = request.getRequestURI(); throw new IllegalStateException( "Ambiguous handler methods mapped for '" + uri + "': {" + m1 + ", " + m2 + "}"); } } } request.setAttribute(BEST_MATCHING_HANDLER_ATTRIBUTE, bestMatch.getHandlerMethod()); handleMatch(bestMatch.mapping, lookupPath, request); return bestMatch.getHandlerMethod(); } else { return handleNoMatch(this.mappingRegistry.getRegistrations().keySet(), lookupPath, request); } } private void addMatchingMappings(Collection mappings, List matches, HttpServletRequest request) { for (T mapping : mappings) { T match = getMatchingMapping(mapping, request); if (match != null) { matches.add(new Match(match, this.mappingRegistry.getRegistrations().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 request the current request */ protected void handleMatch(T mapping, String lookupPath, HttpServletRequest request) { request.setAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, lookupPath); } /** * Invoked when no matching mapping is not found. * @param mappings all registered mappings * @param lookupPath mapping lookup path within the current servlet mapping * @param request the current request * @throws ServletException in case of errors */ @Nullable protected HandlerMethod handleNoMatch(Set mappings, String lookupPath, HttpServletRequest request) throws Exception { return null; } @Override protected boolean hasCorsConfigurationSource(Object handler) { return super.hasCorsConfigurationSource(handler) || (handler instanceof HandlerMethod && this.mappingRegistry.getCorsConfiguration((HandlerMethod) handler) != null); } @Override protected CorsConfiguration getCorsConfiguration(Object handler, HttpServletRequest request) { CorsConfiguration corsConfig = super.getCorsConfiguration(handler, request); if (handler instanceof HandlerMethod) { HandlerMethod handlerMethod = (HandlerMethod) handler; if (handlerMethod.equals(PREFLIGHT_AMBIGUOUS_MATCH)) { return AbstractHandlerMethodMapping.ALLOW_CORS_CONFIG; } else { CorsConfiguration corsConfigFromMethod = this.mappingRegistry.getCorsConfiguration(handlerMethod); corsConfig = (corsConfig != null ? corsConfig.combine(corsConfigFromMethod) : corsConfigFromMethod); } } 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 */ @Nullable protected abstract T getMappingForMethod(Method method, Class handlerType); /** * Extract and return the URL paths contained in the supplied mapping. * @deprecated as of 5.3 in favor of providing non-pattern mappings via * {@link #getDirectPaths(Object)} instead */ @Deprecated protected Set getMappingPathPatterns(T mapping) { return Collections.emptySet(); } /** * Return the request mapping paths that are not patterns. * @since 5.3 */ protected Set getDirectPaths(T mapping) { Set urls = Collections.emptySet(); for (String path : getMappingPathPatterns(mapping)) { if (!getPathMatcher().isPattern(path)) { urls = (urls.isEmpty() ? new HashSet<>(1) : urls); urls.add(path); } } return urls; } /** * 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 request the current HTTP servlet request * @return the match, or {@code null} if the mapping doesn't match */ @Nullable protected abstract T getMatchingMapping(T mapping, HttpServletRequest request); /** * Return a comparator for sorting matching mappings. * The returned comparator should sort 'better' matches higher. * @param request the current request * @return the comparator (never {@code null}) */ protected abstract Comparator getMappingComparator(HttpServletRequest request); /** * A registry that maintains all mappings to handler methods, exposing methods * to perform lookups and providing concurrent access. *

Package-private for testing purposes. */ class MappingRegistry { private final Map> registry = new HashMap<>(); private final MultiValueMap pathLookup = new LinkedMultiValueMap<>(); private final Map> nameLookup = new ConcurrentHashMap<>(); private final Map corsLookup = new ConcurrentHashMap<>(); private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock(); /** * Return all registrations. * @since 5.3 */ public Map> getRegistrations() { return this.registry; } /** * Return matches for the given URL path. Not thread-safe. * @see #acquireReadLock() */ @Nullable public List getMappingsByDirectPath(String urlPath) { return this.pathLookup.get(urlPath); } /** * Return handler methods by mapping name. Thread-safe for concurrent use. */ public List getHandlerMethodsByMappingName(String mappingName) { return this.nameLookup.get(mappingName); } /** * Return CORS configuration. Thread-safe for concurrent use. */ @Nullable 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); validateMethodMapping(handlerMethod, mapping); Set directPaths = AbstractHandlerMethodMapping.this.getDirectPaths(mapping); for (String path : directPaths) { this.pathLookup.add(path, mapping); } String name = null; if (getNamingStrategy() != null) { name = getNamingStrategy().getName(handlerMethod, mapping); addMappingName(name, handlerMethod); } CorsConfiguration corsConfig = initCorsConfiguration(handler, method, mapping); if (corsConfig != null) { corsConfig.validateAllowCredentials(); this.corsLookup.put(handlerMethod, corsConfig); } this.registry.put(mapping, new MappingRegistration<>(mapping, handlerMethod, directPaths, name, corsConfig != null)); } finally { this.readWriteLock.writeLock().unlock(); } } private void validateMethodMapping(HandlerMethod handlerMethod, T mapping) { MappingRegistration registration = this.registry.get(mapping); HandlerMethod existingHandlerMethod = (registration != null ? registration.getHandlerMethod() : null); if (existingHandlerMethod != null && !existingHandlerMethod.equals(handlerMethod)) { throw new IllegalStateException( "Ambiguous mapping. Cannot map '" + handlerMethod.getBean() + "' method \n" + handlerMethod + "\nto " + mapping + ": There is already '" + existingHandlerMethod.getBean() + "' bean method\n" + existingHandlerMethod + " mapped."); } } private void addMappingName(String name, HandlerMethod handlerMethod) { List oldList = this.nameLookup.get(name); if (oldList == null) { oldList = Collections.emptyList(); } for (HandlerMethod current : oldList) { if (handlerMethod.equals(current)) { return; } } List newList = new ArrayList<>(oldList.size() + 1); newList.addAll(oldList); newList.add(handlerMethod); this.nameLookup.put(name, newList); } public void unregister(T mapping) { this.readWriteLock.writeLock().lock(); try { MappingRegistration registration = this.registry.remove(mapping); if (registration == null) { return; } for (String path : registration.getDirectPaths()) { List mappings = this.pathLookup.get(path); if (mappings != null) { mappings.remove(registration.getMapping()); if (mappings.isEmpty()) { this.pathLookup.remove(path); } } } removeMappingName(registration); this.corsLookup.remove(registration.getHandlerMethod()); } finally { this.readWriteLock.writeLock().unlock(); } } private void removeMappingName(MappingRegistration definition) { String name = definition.getMappingName(); if (name == null) { return; } HandlerMethod handlerMethod = definition.getHandlerMethod(); List oldList = this.nameLookup.get(name); if (oldList == null) { return; } if (oldList.size() <= 1) { this.nameLookup.remove(name); return; } List newList = new ArrayList<>(oldList.size() - 1); for (HandlerMethod current : oldList) { if (!current.equals(handlerMethod)) { newList.add(current); } } this.nameLookup.put(name, newList); } } static class MappingRegistration { private final T mapping; private final HandlerMethod handlerMethod; private final Set directPaths; @Nullable private final String mappingName; private final boolean corsConfig; public MappingRegistration(T mapping, HandlerMethod handlerMethod, @Nullable Set directPaths, @Nullable String mappingName, boolean corsConfig) { Assert.notNull(mapping, "Mapping must not be null"); Assert.notNull(handlerMethod, "HandlerMethod must not be null"); this.mapping = mapping; this.handlerMethod = handlerMethod; this.directPaths = (directPaths != null ? directPaths : Collections.emptySet()); this.mappingName = mappingName; this.corsConfig = corsConfig; } public T getMapping() { return this.mapping; } public HandlerMethod getHandlerMethod() { return this.handlerMethod; } public Set getDirectPaths() { return this.directPaths; } @Nullable public String getMappingName() { return this.mappingName; } public boolean hasCorsConfig() { return this.corsConfig; } } /** * 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 MappingRegistration registration; public Match(T mapping, MappingRegistration registration) { this.mapping = mapping; this.registration = registration; } public T getMapping() { return this.mapping; } public HandlerMethod getHandlerMethod() { return this.registration.getHandlerMethod(); } public boolean hasCorsConfig() { return this.registration.hasCorsConfig(); } @Override public String toString() { return this.mapping.toString(); } } private class MatchComparator implements Comparator { private final Comparator comparator; public MatchComparator(Comparator comparator) { this.comparator = comparator; } @Override public int compare(Match match1, Match match2) { return this.comparator.compare(match1.mapping, match2.mapping); } } private static class EmptyHandler { @SuppressWarnings("unused") public void handle() { throw new UnsupportedOperationException("Not implemented"); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy