org.springframework.web.servlet.handler.AbstractHandlerMethodMapping Maven / Gradle / Ivy
/*
* 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.servlet.handler;
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 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.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;
/**
* 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
* @since 3.1
* @param The mapping for a {@link HandlerMethod} containing the conditions
* needed to match the handler method to 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.addAllowedOrigin("*");
ALLOW_CORS_CONFIG.addAllowedMethod("*");
ALLOW_CORS_CONFIG.addAllowedHeader("*");
ALLOW_CORS_CONFIG.setAllowCredentials(true);
}
private boolean detectHandlerMethodsInAncestorContexts = false;
private HandlerMethodMappingNamingStrategy namingStrategy;
private final MappingRegistry mappingRegistry = new MappingRegistry();
/**
* 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.
*/
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}.
*/
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.getMappings());
}
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
*/
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) {
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) {
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 = (this.detectHandlerMethodsInAncestorContexts ?
BeanFactoryUtils.beanNamesForTypeIncludingAncestors(getApplicationContext(), Object.class) :
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 methods = MethodIntrospector.selectMethods(userType,
new MethodIntrospector.MetadataLookup() {
@Override
public T inspect(Method method) {
try {
return getMappingForMethod(method, userType);
}
catch (Throwable ex) {
throw new IllegalStateException("Invalid mapping on handler class [" +
userType.getName() + "]: " + method, ex);
}
}
});
if (logger.isDebugEnabled()) {
logger.debug(methods.size() + " request handler methods found on " + userType + ": " + methods);
}
for (Map.Entry entry : methods.entrySet()) {
Method invocableMethod = AopUtils.selectInvocableMethod(entry.getKey(), userType);
T mapping = entry.getValue();
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 handlerMethods) {
}
// Handler method lookup
/**
* Look up a handler method for the given request.
*/
@Override
protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception {
String lookupPath = getUrlPathHelper().getLookupPathForRequest(request);
if (logger.isDebugEnabled()) {
logger.debug("Looking up handler method for path " + lookupPath);
}
this.mappingRegistry.acquireReadLock();
try {
HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, request);
if (logger.isDebugEnabled()) {
if (handlerMethod != null) {
logger.debug("Returning handler method [" + handlerMethod + "]");
}
else {
logger.debug("Did not find handler method for [" + lookupPath + "]");
}
}
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)
*/
protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {
List matches = new ArrayList();
List directPathMatches = this.mappingRegistry.getMappingsByUrl(lookupPath);
if (directPathMatches != null) {
addMatchingMappings(directPathMatches, matches, request);
}
if (matches.isEmpty()) {
// No choice but to go through all mappings...
addMatchingMappings(this.mappingRegistry.getMappings().keySet(), matches, request);
}
if (!matches.isEmpty()) {
Comparator comparator = new MatchComparator(getMappingComparator(request));
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(request)) {
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 '" +
request.getRequestURL() + "': {" + m1 + ", " + m2 + "}");
}
}
handleMatch(bestMatch.mapping, lookupPath, request);
return bestMatch.handlerMethod;
}
else {
return handleNoMatch(this.mappingRegistry.getMappings().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.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 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
*/
protected HandlerMethod handleNoMatch(Set mappings, String lookupPath, HttpServletRequest request)
throws Exception {
return 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
*/
protected abstract T getMappingForMethod(Method method, Class handlerType);
/**
* Extract and return the URL paths contained in a mapping.
*/
protected abstract Set 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 request the current HTTP servlet request
* @return the match, or {@code null} if the mapping doesn't match
*/
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 Map mappingLookup = new LinkedHashMap();
private final MultiValueMap urlLookup = new LinkedMultiValueMap();
private final Map> nameLookup =
new ConcurrentHashMap>();
private final Map corsLookup =
new ConcurrentHashMap();
private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
/**
* Return all mappings and handler methods. Not thread-safe.
* @see #acquireReadLock()
*/
public Map getMappings() {
return this.mappingLookup;
}
/**
* Return matches for the given URL path. Not thread-safe.
* @see #acquireReadLock()
*/
public List getMappingsByUrl(String urlPath) {
return this.urlLookup.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.
*/
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 directUrls = getDirectUrls(mapping);
for (String url : directUrls) {
this.urlLookup.add(url, mapping);
}
String name = null;
if (getNamingStrategy() != null) {
name = getNamingStrategy().getName(handlerMethod, mapping);
addMappingName(name, handlerMethod);
}
CorsConfiguration corsConfig = initCorsConfiguration(handler, method, mapping);
if (corsConfig != null) {
this.corsLookup.put(handlerMethod, corsConfig);
}
this.registry.put(mapping, new MappingRegistration(mapping, handlerMethod, directUrls, name));
}
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 getDirectUrls(T mapping) {
List urls = new ArrayList(1);
for (String path : getMappingPathPatterns(mapping)) {
if (!getPathMatcher().isPattern(path)) {
urls.add(path);
}
}
return urls;
}
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;
}
}
if (logger.isTraceEnabled()) {
logger.trace("Mapping name '" + name + "'");
}
List newList = new ArrayList(oldList.size() + 1);
newList.addAll(oldList);
newList.add(handlerMethod);
this.nameLookup.put(name, newList);
if (newList.size() > 1) {
if (logger.isTraceEnabled()) {
logger.trace("Mapping name clash for handlerMethods " + newList +
". Consider assigning explicit names.");
}
}
}
public void unregister(T mapping) {
this.readWriteLock.writeLock().lock();
try {
MappingRegistration definition = this.registry.remove(mapping);
if (definition == null) {
return;
}
this.mappingLookup.remove(definition.getMapping());
for (String url : definition.getDirectUrls()) {
List list = this.urlLookup.get(url);
if (list != null) {
list.remove(definition.getMapping());
if (list.isEmpty()) {
this.urlLookup.remove(url);
}
}
}
removeMappingName(definition);
this.corsLookup.remove(definition.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);
}
}
private static class MappingRegistration {
private final T mapping;
private final HandlerMethod handlerMethod;
private final List directUrls;
private final String mappingName;
public MappingRegistration(T mapping, HandlerMethod handlerMethod, List directUrls, String mappingName) {
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());
this.mappingName = mappingName;
}
public T getMapping() {
return this.mapping;
}
public HandlerMethod getHandlerMethod() {
return this.handlerMethod;
}
public List getDirectUrls() {
return this.directUrls;
}
public String getMappingName() {
return this.mappingName;
}
}
/**
* 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 {
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 {
public void handle() {
throw new UnsupportedOperationException("Not implemented");
}
}
}