mirror of
https://github.com/spring-projects/spring-framework.git
synced 2026-09-17 16:39:29 +00:00
Remove HandlerMappingIntrospector
Closes gh-36481
This commit is contained in:
+1
-9
@@ -38,8 +38,6 @@ import org.springframework.web.servlet.function.RouterFunction;
|
||||
import org.springframework.web.servlet.function.RouterFunctions;
|
||||
import org.springframework.web.servlet.function.ServerRequest;
|
||||
import org.springframework.web.servlet.handler.AbstractHandlerMapping;
|
||||
import org.springframework.web.servlet.handler.MatchableHandlerMapping;
|
||||
import org.springframework.web.servlet.handler.RequestMatchResult;
|
||||
import org.springframework.web.util.pattern.PathPattern;
|
||||
import org.springframework.web.util.pattern.PathPatternParser;
|
||||
|
||||
@@ -57,7 +55,7 @@ import org.springframework.web.util.pattern.PathPatternParser;
|
||||
* @since 5.2
|
||||
*/
|
||||
@SuppressWarnings("removal")
|
||||
public class RouterFunctionMapping extends AbstractHandlerMapping implements InitializingBean, MatchableHandlerMapping {
|
||||
public class RouterFunctionMapping extends AbstractHandlerMapping implements InitializingBean {
|
||||
|
||||
private @Nullable RouterFunction<?> routerFunction;
|
||||
|
||||
@@ -230,10 +228,4 @@ public class RouterFunctionMapping extends AbstractHandlerMapping implements Ini
|
||||
servletRequest.setAttribute(RouterFunctions.REQUEST_ATTRIBUTE, request);
|
||||
}
|
||||
|
||||
@SuppressWarnings("removal")
|
||||
@Deprecated(since = "7.0", forRemoval = true)
|
||||
@Override
|
||||
public @Nullable RequestMatchResult match(HttpServletRequest request, String pattern) {
|
||||
throw new UnsupportedOperationException("This HandlerMapping uses PathPatterns");
|
||||
}
|
||||
}
|
||||
|
||||
+1
-14
@@ -62,7 +62,7 @@ import org.springframework.web.util.pattern.PathPatternParser;
|
||||
* @since 16.04.2003
|
||||
*/
|
||||
@SuppressWarnings("removal")
|
||||
public abstract class AbstractUrlHandlerMapping extends AbstractHandlerMapping implements MatchableHandlerMapping {
|
||||
public abstract class AbstractUrlHandlerMapping extends AbstractHandlerMapping {
|
||||
|
||||
private @Nullable Object rootHandler;
|
||||
|
||||
@@ -465,19 +465,6 @@ public abstract class AbstractUrlHandlerMapping extends AbstractHandlerMapping i
|
||||
request.setAttribute(URI_TEMPLATE_VARIABLES_ATTRIBUTE, uriTemplateVariables);
|
||||
}
|
||||
|
||||
@SuppressWarnings("removal")
|
||||
@Override
|
||||
@Deprecated(since = "7.0", forRemoval = true)
|
||||
public @Nullable RequestMatchResult match(HttpServletRequest request, String pattern) {
|
||||
Assert.state(getPatternParser() == null, "This HandlerMapping uses PathPatterns.");
|
||||
String lookupPath = UrlPathHelper.getResolvedLookupPath(request);
|
||||
if (getPathMatcher().match(pattern, lookupPath)) {
|
||||
return new RequestMatchResult(pattern, lookupPath, getPathMatcher());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return the handler mappings as a read-only Map, with the registered path
|
||||
* or pattern as key and the handler object (or handler bean name in case of
|
||||
|
||||
-631
@@ -1,631 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-present 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.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Enumeration;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import jakarta.servlet.DispatcherType;
|
||||
import jakarta.servlet.Filter;
|
||||
import jakarta.servlet.ServletRequest;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletRequestWrapper;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.beans.factory.BeanFactoryUtils;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ApplicationContextAware;
|
||||
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.support.PropertiesLoaderUtils;
|
||||
import org.springframework.http.server.RequestPath;
|
||||
import org.springframework.http.server.ServletServerHttpRequest;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.cors.CorsConfiguration;
|
||||
import org.springframework.web.cors.CorsConfigurationSource;
|
||||
import org.springframework.web.cors.CorsUtils;
|
||||
import org.springframework.web.cors.PreFlightRequestHandler;
|
||||
import org.springframework.web.servlet.DispatcherServlet;
|
||||
import org.springframework.web.servlet.HandlerExecutionChain;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
import org.springframework.web.servlet.HandlerMapping;
|
||||
import org.springframework.web.servlet.NoHandlerFoundException;
|
||||
import org.springframework.web.util.ServletRequestPathUtils;
|
||||
import org.springframework.web.util.UrlPathHelper;
|
||||
import org.springframework.web.util.pattern.PathPatternParser;
|
||||
|
||||
/**
|
||||
* Helper class to get information from the {@code HandlerMapping} that would
|
||||
* serve a specific request.
|
||||
*
|
||||
* <p>Provides the following methods:
|
||||
* <ul>
|
||||
* <li>{@link #getMatchableHandlerMapping} — obtain a {@code HandlerMapping}
|
||||
* to check request-matching criteria against.
|
||||
* <li>{@link #getCorsConfiguration} — obtain the CORS configuration for the
|
||||
* request.
|
||||
* </ul>
|
||||
*
|
||||
* <p>Note that this is primarily an SPI to allow Spring Security
|
||||
* to align its pattern matching with the same pattern matching that would be
|
||||
* used in Spring MVC for a given request, in order to avoid security issues.
|
||||
*
|
||||
* <p>Use of this component incurs the performance overhead of mapping the
|
||||
* request, and should not be repeated multiple times per request.
|
||||
* {@link #createCacheFilter()} exposes a Filter to cache the results.
|
||||
* Applications that rely on Spring Security don't need to deploy this Filter
|
||||
* since Spring Security doe that. However, other custom security layers, used
|
||||
* in place of Spring Security that use this component should deploy the cache
|
||||
* Filter with requirements described in the Javadoc for the method.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 4.3.1
|
||||
* @deprecated in favor of using just {@link PathPatternParser}; when
|
||||
* {@link #allHandlerMappingsUsePathPatternParser} returns true, it is sufficient
|
||||
* to use that to align with handler mappings.
|
||||
*/
|
||||
@SuppressWarnings("removal")
|
||||
@Deprecated(since = "7.0", forRemoval = true)
|
||||
public class HandlerMappingIntrospector
|
||||
implements CorsConfigurationSource, PreFlightRequestHandler, ApplicationContextAware, InitializingBean {
|
||||
|
||||
private static final Log logger = LogFactory.getLog(HandlerMappingIntrospector.class.getName());
|
||||
|
||||
private static final String CACHED_RESULT_ATTRIBUTE =
|
||||
HandlerMappingIntrospector.class.getName() + ".CachedResult";
|
||||
|
||||
private static final int DEFAULT_CACHE_LIMIT = 2048;
|
||||
|
||||
|
||||
private @Nullable ApplicationContext applicationContext;
|
||||
|
||||
private @Nullable List<HandlerMapping> handlerMappings;
|
||||
|
||||
private Map<HandlerMapping, PathPatternMatchableHandlerMapping> pathPatternMappings = Collections.emptyMap();
|
||||
|
||||
private int patternCacheLimit = DEFAULT_CACHE_LIMIT;
|
||||
|
||||
private final CacheResultLogHelper cacheLogHelper = new CacheResultLogHelper();
|
||||
|
||||
|
||||
/**
|
||||
* Set a limit on the maximum number of security patterns passed into
|
||||
* {@link MatchableHandlerMapping#match} at runtime to cache.
|
||||
* <p>By default, this is set to 2048.
|
||||
* @param patternCacheLimit the limit to use
|
||||
* @since 6.2.8
|
||||
*/
|
||||
public void setPatternCacheLimit(int patternCacheLimit) {
|
||||
this.patternCacheLimit = patternCacheLimit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the configured limit on security patterns to cache.
|
||||
* @since 6.2.8
|
||||
*/
|
||||
public int getPatternCacheLimit() {
|
||||
return this.patternCacheLimit;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setApplicationContext(ApplicationContext applicationContext) {
|
||||
this.applicationContext = applicationContext;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() {
|
||||
if (this.handlerMappings == null) {
|
||||
Assert.notNull(this.applicationContext, "No ApplicationContext");
|
||||
this.handlerMappings = initHandlerMappings(this.applicationContext);
|
||||
|
||||
this.pathPatternMappings = this.handlerMappings.stream()
|
||||
.filter(m -> m instanceof MatchableHandlerMapping hm && hm.getPatternParser() != null)
|
||||
.map(hm -> (MatchableHandlerMapping) hm)
|
||||
.collect(Collectors.toMap(hm -> hm, (MatchableHandlerMapping delegate) ->
|
||||
new PathPatternMatchableHandlerMapping(delegate, getPatternCacheLimit())));
|
||||
}
|
||||
}
|
||||
|
||||
private static List<HandlerMapping> initHandlerMappings(ApplicationContext context) {
|
||||
|
||||
Map<String, HandlerMapping> beans =
|
||||
BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false);
|
||||
|
||||
if (!beans.isEmpty()) {
|
||||
List<HandlerMapping> mappings = new ArrayList<>(beans.values());
|
||||
AnnotationAwareOrderComparator.sort(mappings);
|
||||
return Collections.unmodifiableList(mappings);
|
||||
}
|
||||
|
||||
return Collections.unmodifiableList(initFallback(context));
|
||||
}
|
||||
|
||||
private static List<HandlerMapping> initFallback(ApplicationContext applicationContext) {
|
||||
Properties properties;
|
||||
try {
|
||||
Resource resource = new ClassPathResource("DispatcherServlet.properties", DispatcherServlet.class);
|
||||
properties = PropertiesLoaderUtils.loadProperties(resource);
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new IllegalStateException("Could not load DispatcherServlet.properties: " + ex.getMessage());
|
||||
}
|
||||
|
||||
String value = properties.getProperty(HandlerMapping.class.getName());
|
||||
String[] names = StringUtils.commaDelimitedListToStringArray(value);
|
||||
List<HandlerMapping> result = new ArrayList<>(names.length);
|
||||
for (String name : names) {
|
||||
try {
|
||||
Class<?> clazz = ClassUtils.forName(name, DispatcherServlet.class.getClassLoader());
|
||||
Object mapping = applicationContext.getAutowireCapableBeanFactory().createBean(clazz);
|
||||
result.add((HandlerMapping) mapping);
|
||||
}
|
||||
catch (ClassNotFoundException ex) {
|
||||
throw new IllegalStateException("Could not find default HandlerMapping [" + name + "]");
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return the configured or detected {@code HandlerMapping}s.
|
||||
*/
|
||||
public List<HandlerMapping> getHandlerMappings() {
|
||||
return (this.handlerMappings != null ? this.handlerMappings : Collections.emptyList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Return {@code true} if all {@link HandlerMapping} beans
|
||||
* {@link HandlerMapping#usesPathPatterns() use parsed PathPatterns},
|
||||
* and {@code false} if any don't.
|
||||
* @since 6.2
|
||||
*/
|
||||
public boolean allHandlerMappingsUsePathPatternParser() {
|
||||
Assert.state(this.handlerMappings != null, "Not yet initialized via afterPropertiesSet.");
|
||||
return getHandlerMappings().stream().allMatch(HandlerMapping::usesPathPatterns);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Find the matching {@link HandlerMapping} for the request, and invoke the
|
||||
* handler it returns as a {@link PreFlightRequestHandler}.
|
||||
* @throws NoHandlerFoundException if no handler matches the request
|
||||
* @since 6.2
|
||||
*/
|
||||
@Override
|
||||
public void handlePreFlight(HttpServletRequest request, HttpServletResponse response) throws Exception {
|
||||
Assert.state(this.handlerMappings != null, "Not yet initialized via afterPropertiesSet.");
|
||||
Assert.state(CorsUtils.isPreFlightRequest(request), "Not a pre-flight request.");
|
||||
RequestPath previousPath = (RequestPath) request.getAttribute(ServletRequestPathUtils.PATH_ATTRIBUTE);
|
||||
try {
|
||||
ServletRequestPathUtils.parseAndCache(request);
|
||||
for (HandlerMapping mapping : this.handlerMappings) {
|
||||
HandlerExecutionChain chain = mapping.getHandler(request);
|
||||
if (chain != null) {
|
||||
Object handler = chain.getHandler();
|
||||
if (handler instanceof PreFlightRequestHandler preFlightHandler) {
|
||||
preFlightHandler.handlePreFlight(request, response);
|
||||
return;
|
||||
}
|
||||
throw new IllegalStateException("Expected PreFlightRequestHandler: " + handler.getClass());
|
||||
}
|
||||
}
|
||||
throw new NoHandlerFoundException(
|
||||
request.getMethod(), request.getRequestURI(), new ServletServerHttpRequest(request).getHeaders());
|
||||
}
|
||||
finally {
|
||||
ServletRequestPathUtils.setParsedRequestPath(previousPath, request);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* {@link Filter} that looks up the {@code MatchableHandlerMapping} and
|
||||
* {@link CorsConfiguration} for the request proactively before delegating
|
||||
* to the rest of the chain, caching the result in a request attribute, and
|
||||
* restoring it after the chain returns.
|
||||
* <p><strong>Note:</strong> Applications that rely on Spring Security do
|
||||
* not use this component directly and should not deploy the filter instead
|
||||
* allowing Spring Security to do it. Other custom security layers used in
|
||||
* place of Spring Security that also rely on {@code HandlerMappingIntrospector}
|
||||
* should deploy this filter ahead of other filters where lookups are
|
||||
* performed, and should also make sure the filter is configured to handle
|
||||
* all dispatcher types.
|
||||
* @return the Filter instance to use
|
||||
* @since 6.0.14
|
||||
*/
|
||||
public Filter createCacheFilter() {
|
||||
return (request, response, chain) -> {
|
||||
CachedResult previous = setCache((HttpServletRequest) request);
|
||||
try {
|
||||
chain.doFilter(request, response);
|
||||
}
|
||||
finally {
|
||||
resetCache(request, previous);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a lookup and save the {@link CachedResult} as a request attribute.
|
||||
* This method can be invoked from a filter before subsequent calls to
|
||||
* {@link #getMatchableHandlerMapping(HttpServletRequest)} and
|
||||
* {@link #getCorsConfiguration(HttpServletRequest)} to avoid repeated lookups.
|
||||
* @param request the current request
|
||||
* @return the previous {@link CachedResult}, if there is one from a parent dispatch
|
||||
* @since 6.0.14
|
||||
*/
|
||||
public @Nullable CachedResult setCache(HttpServletRequest request) {
|
||||
CachedResult previous = (CachedResult) request.getAttribute(CACHED_RESULT_ATTRIBUTE);
|
||||
if (previous == null || !previous.matches(request)) {
|
||||
HttpServletRequest wrapped = new AttributesPreservingRequest(request);
|
||||
CachedResult result;
|
||||
try {
|
||||
// Try to get both in one lookup (with ignoringException=false)
|
||||
result = doWithHandlerMapping(wrapped, false, (mapping, executionChain) -> {
|
||||
MatchableHandlerMapping matchableMapping = createMatchableHandlerMapping(mapping, wrapped);
|
||||
CorsConfiguration corsConfig = getCorsConfiguration(executionChain, wrapped);
|
||||
return new CachedResult(request, matchableMapping, corsConfig, null, null);
|
||||
});
|
||||
}
|
||||
catch (Exception ex) {
|
||||
try {
|
||||
// Try CorsConfiguration at least with ignoreException=true
|
||||
AttributesPreservingRequest requestToUse = new AttributesPreservingRequest(request);
|
||||
result = doWithHandlerMapping(requestToUse, true, (mapping, executionChain) -> {
|
||||
CorsConfiguration corsConfig = getCorsConfiguration(executionChain, wrapped);
|
||||
return new CachedResult(request, null, corsConfig, ex, null);
|
||||
});
|
||||
}
|
||||
catch (Exception ex2) {
|
||||
result = new CachedResult(request, null, null, ex, new IllegalStateException(ex2));
|
||||
}
|
||||
}
|
||||
if (result == null) {
|
||||
result = new CachedResult(request, null, null, null, null);
|
||||
}
|
||||
request.setAttribute(CACHED_RESULT_ATTRIBUTE, result);
|
||||
}
|
||||
return previous;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore a previous {@link CachedResult}. This method can be invoked from
|
||||
* a filter after delegating to the rest of the chain.
|
||||
* @since 6.0.14
|
||||
*/
|
||||
public void resetCache(ServletRequest request, @Nullable CachedResult cachedResult) {
|
||||
request.setAttribute(CACHED_RESULT_ATTRIBUTE, cachedResult);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the {@link HandlerMapping} that would handle the given request and
|
||||
* return a {@link MatchableHandlerMapping} to use for path matching.
|
||||
* @param request the current request
|
||||
* @return the resolved {@code MatchableHandlerMapping}, or {@code null}
|
||||
* @throws IllegalStateException if the matching HandlerMapping is not an
|
||||
* instance of {@link MatchableHandlerMapping}
|
||||
* @throws Exception if any of the HandlerMapping's raise an exception
|
||||
*/
|
||||
public @Nullable MatchableHandlerMapping getMatchableHandlerMapping(HttpServletRequest request) throws Exception {
|
||||
CachedResult result = CachedResult.getResultFor(request);
|
||||
if (result != null) {
|
||||
return result.getHandlerMapping();
|
||||
}
|
||||
this.cacheLogHelper.logHandlerMappingCacheMiss(request);
|
||||
HttpServletRequest requestToUse = new AttributesPreservingRequest(request);
|
||||
return doWithHandlerMapping(requestToUse, false,
|
||||
(mapping, executionChain) -> createMatchableHandlerMapping(mapping, requestToUse));
|
||||
}
|
||||
|
||||
private MatchableHandlerMapping createMatchableHandlerMapping(HandlerMapping mapping, HttpServletRequest request) {
|
||||
if (mapping instanceof MatchableHandlerMapping) {
|
||||
PathPatternMatchableHandlerMapping pathPatternMapping = this.pathPatternMappings.get(mapping);
|
||||
if (pathPatternMapping != null) {
|
||||
RequestPath requestPath = ServletRequestPathUtils.getParsedRequestPath(request);
|
||||
return new LookupPathMatchableHandlerMapping(pathPatternMapping, requestPath);
|
||||
}
|
||||
else {
|
||||
String lookupPath = (String) request.getAttribute(UrlPathHelper.PATH_ATTRIBUTE);
|
||||
return new LookupPathMatchableHandlerMapping((MatchableHandlerMapping) mapping, lookupPath);
|
||||
}
|
||||
}
|
||||
throw new IllegalStateException("HandlerMapping is not a MatchableHandlerMapping");
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable CorsConfiguration getCorsConfiguration(HttpServletRequest request) {
|
||||
CachedResult result = CachedResult.getResultFor(request);
|
||||
if (result != null) {
|
||||
return result.getCorsConfig();
|
||||
}
|
||||
this.cacheLogHelper.logCorsConfigCacheMiss(request);
|
||||
try {
|
||||
boolean ignoreException = true;
|
||||
AttributesPreservingRequest requestToUse = new AttributesPreservingRequest(request);
|
||||
return doWithHandlerMapping(requestToUse, ignoreException,
|
||||
(handlerMapping, executionChain) -> getCorsConfiguration(executionChain, requestToUse));
|
||||
}
|
||||
catch (Exception ex) {
|
||||
// HandlerMapping exceptions are ignored. More basic error like parsing the request path.
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static @Nullable CorsConfiguration getCorsConfiguration(HandlerExecutionChain chain, HttpServletRequest request) {
|
||||
for (HandlerInterceptor interceptor : chain.getInterceptorList()) {
|
||||
if (interceptor instanceof CorsConfigurationSource source) {
|
||||
return source.getCorsConfiguration(request);
|
||||
}
|
||||
}
|
||||
if (chain.getHandler() instanceof CorsConfigurationSource source) {
|
||||
return source.getCorsConfiguration(request);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private <T> @Nullable T doWithHandlerMapping(
|
||||
HttpServletRequest request, boolean ignoreException,
|
||||
BiFunction<HandlerMapping, HandlerExecutionChain, T> extractor) throws Exception {
|
||||
|
||||
Assert.state(this.handlerMappings != null, "HandlerMapping's not initialized");
|
||||
|
||||
boolean parsePath = !this.pathPatternMappings.isEmpty();
|
||||
RequestPath previousPath = null;
|
||||
if (parsePath) {
|
||||
previousPath = (RequestPath) request.getAttribute(ServletRequestPathUtils.PATH_ATTRIBUTE);
|
||||
ServletRequestPathUtils.parseAndCache(request);
|
||||
}
|
||||
try {
|
||||
for (HandlerMapping handlerMapping : this.handlerMappings) {
|
||||
HandlerExecutionChain chain = null;
|
||||
try {
|
||||
chain = handlerMapping.getHandler(request);
|
||||
}
|
||||
catch (Exception ex) {
|
||||
if (!ignoreException) {
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
if (chain == null) {
|
||||
continue;
|
||||
}
|
||||
return extractor.apply(handlerMapping, chain);
|
||||
}
|
||||
}
|
||||
finally {
|
||||
if (parsePath) {
|
||||
ServletRequestPathUtils.setParsedRequestPath(previousPath, request);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Container for a {@link MatchableHandlerMapping} and {@link CorsConfiguration}
|
||||
* for a given request matched by dispatcher type and requestURI.
|
||||
* @since 6.0.14
|
||||
*/
|
||||
@SuppressWarnings("serial")
|
||||
public static final class CachedResult {
|
||||
|
||||
private final DispatcherType dispatcherType;
|
||||
|
||||
private final String requestURI;
|
||||
|
||||
private final @Nullable MatchableHandlerMapping handlerMapping;
|
||||
|
||||
private final @Nullable CorsConfiguration corsConfig;
|
||||
|
||||
private final @Nullable Exception failure;
|
||||
|
||||
private final @Nullable IllegalStateException corsConfigFailure;
|
||||
|
||||
private CachedResult(HttpServletRequest request,
|
||||
@Nullable MatchableHandlerMapping mapping, @Nullable CorsConfiguration config,
|
||||
@Nullable Exception failure, @Nullable IllegalStateException corsConfigFailure) {
|
||||
|
||||
this.dispatcherType = request.getDispatcherType();
|
||||
this.requestURI = request.getRequestURI();
|
||||
this.handlerMapping = mapping;
|
||||
this.corsConfig = config;
|
||||
this.failure = failure;
|
||||
this.corsConfigFailure = corsConfigFailure;
|
||||
}
|
||||
|
||||
public boolean matches(HttpServletRequest request) {
|
||||
return (this.dispatcherType.equals(request.getDispatcherType()) &&
|
||||
this.requestURI.equals(request.getRequestURI()));
|
||||
}
|
||||
|
||||
public @Nullable MatchableHandlerMapping getHandlerMapping() throws Exception {
|
||||
if (this.failure != null) {
|
||||
throw this.failure;
|
||||
}
|
||||
return this.handlerMapping;
|
||||
}
|
||||
|
||||
public @Nullable CorsConfiguration getCorsConfig() {
|
||||
if (this.corsConfigFailure != null) {
|
||||
throw this.corsConfigFailure;
|
||||
}
|
||||
return this.corsConfig;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "CachedResult for " + this.dispatcherType + " dispatch to '" + this.requestURI + "'";
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return a {@link CachedResult} that matches the given request.
|
||||
*/
|
||||
public static @Nullable CachedResult getResultFor(HttpServletRequest request) {
|
||||
CachedResult result = (CachedResult) request.getAttribute(CACHED_RESULT_ATTRIBUTE);
|
||||
return (result != null && result.matches(request) ? result : null);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static class CacheResultLogHelper {
|
||||
|
||||
private final Map<String, AtomicInteger> counters =
|
||||
Map.of("MatchableHandlerMapping", new AtomicInteger(), "CorsConfiguration", new AtomicInteger());
|
||||
|
||||
public void logHandlerMappingCacheMiss(HttpServletRequest request) {
|
||||
logCacheMiss("MatchableHandlerMapping", request);
|
||||
}
|
||||
|
||||
public void logCorsConfigCacheMiss(HttpServletRequest request) {
|
||||
logCacheMiss("CorsConfiguration", request);
|
||||
}
|
||||
|
||||
private void logCacheMiss(String label, HttpServletRequest request) {
|
||||
AtomicInteger counter = this.counters.get(label);
|
||||
Assert.notNull(counter, "Expected '" + label + "' counter.");
|
||||
|
||||
String message = getLogMessage(label, request);
|
||||
|
||||
if (logger.isDebugEnabled() && counter.getAndIncrement() == 0) {
|
||||
logger.debug(message + " This is logged once only at DEBUG level, and every time at TRACE.");
|
||||
}
|
||||
else if (logger.isTraceEnabled()) {
|
||||
logger.trace("No CachedResult, performing " + label + " lookup instead.");
|
||||
}
|
||||
}
|
||||
|
||||
private static String getLogMessage(String label, HttpServletRequest request) {
|
||||
return "Cache miss for " + request.getDispatcherType() + " dispatch to '" + request.getRequestURI() + "' " +
|
||||
"(previous " + request.getAttribute(CACHED_RESULT_ATTRIBUTE) + "). " +
|
||||
"Performing " + label + " lookup. If there are repeated lookups per request, " +
|
||||
"consider using HandlerMappingIntrospector#createCacheFilter()" +
|
||||
"to create a Servlet Filter to set the cache for the request.";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Request wrapper that buffers request attributes in order protect the
|
||||
* underlying request from attribute changes.
|
||||
*/
|
||||
private static class AttributesPreservingRequest extends HttpServletRequestWrapper {
|
||||
|
||||
private final Map<String, Object> attributes;
|
||||
|
||||
AttributesPreservingRequest(HttpServletRequest request) {
|
||||
super(request);
|
||||
this.attributes = initAttributes(request);
|
||||
this.attributes.put(AbstractHandlerMapping.SUPPRESS_LOGGING_ATTRIBUTE, Boolean.TRUE);
|
||||
}
|
||||
|
||||
private Map<String, Object> initAttributes(HttpServletRequest request) {
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
Enumeration<String> names = request.getAttributeNames();
|
||||
while (names.hasMoreElements()) {
|
||||
String name = names.nextElement();
|
||||
map.put(name, request.getAttribute(name));
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAttribute(String name, Object value) {
|
||||
this.attributes.put(name, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Object getAttribute(String name) {
|
||||
return this.attributes.get(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Enumeration<String> getAttributeNames() {
|
||||
return Collections.enumeration(this.attributes.keySet());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeAttribute(String name) {
|
||||
this.attributes.remove(name);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static class LookupPathMatchableHandlerMapping implements MatchableHandlerMapping {
|
||||
|
||||
private final MatchableHandlerMapping delegate;
|
||||
|
||||
private final Object lookupPath;
|
||||
|
||||
private final String pathAttributeName;
|
||||
|
||||
LookupPathMatchableHandlerMapping(MatchableHandlerMapping delegate, Object lookupPath) {
|
||||
this.delegate = delegate;
|
||||
this.lookupPath = lookupPath;
|
||||
this.pathAttributeName = (lookupPath instanceof RequestPath ?
|
||||
ServletRequestPathUtils.PATH_ATTRIBUTE : UrlPathHelper.PATH_ATTRIBUTE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable PathPatternParser getPatternParser() {
|
||||
return this.delegate.getPatternParser();
|
||||
}
|
||||
|
||||
@SuppressWarnings("removal")
|
||||
@Deprecated(since = "7.0", forRemoval = true)
|
||||
@Override
|
||||
public @Nullable RequestMatchResult match(HttpServletRequest request, String pattern) {
|
||||
pattern = initFullPathPattern(pattern);
|
||||
Object previousPath = request.getAttribute(this.pathAttributeName);
|
||||
request.setAttribute(this.pathAttributeName, this.lookupPath);
|
||||
try {
|
||||
return this.delegate.match(request, pattern);
|
||||
}
|
||||
finally {
|
||||
request.setAttribute(this.pathAttributeName, previousPath);
|
||||
}
|
||||
}
|
||||
|
||||
private String initFullPathPattern(String pattern) {
|
||||
PathPatternParser parser = (getPatternParser() != null ? getPatternParser() : PathPatternParser.defaultInstance);
|
||||
return parser.initFullPathPattern(pattern);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
|
||||
return this.delegate.getHandler(request);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
-63
@@ -1,63 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-present 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 jakarta.servlet.http.HttpServletRequest;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.util.PathMatcher;
|
||||
import org.springframework.web.servlet.HandlerMapping;
|
||||
import org.springframework.web.util.UrlPathHelper;
|
||||
import org.springframework.web.util.pattern.PathPatternParser;
|
||||
|
||||
/**
|
||||
* Additional interface that a {@link HandlerMapping} can implement to expose
|
||||
* a request matching API aligned with its internal request matching
|
||||
* configuration and implementation.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 4.3.1
|
||||
* @deprecated together with {@link HandlerMappingIntrospector} without a replacement.
|
||||
*/
|
||||
@Deprecated(since = "7.0", forRemoval = true)
|
||||
public interface MatchableHandlerMapping extends HandlerMapping {
|
||||
|
||||
/**
|
||||
* Return the parser of this {@code HandlerMapping}, if configured in which
|
||||
* case pre-parsed patterns are used.
|
||||
* @since 5.3
|
||||
*/
|
||||
default @Nullable PathPatternParser getPatternParser() {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the request matches the given pattern. Use this method
|
||||
* when {@link #getPatternParser()} returns {@code null} which means that the
|
||||
* {@code HandlerMapping} is using String pattern matching.
|
||||
* @param request the current request
|
||||
* @param pattern the pattern to match
|
||||
* @return the result from request matching, or {@code null} if none
|
||||
* @deprecated use of {@link PathMatcher} and {@link UrlPathHelper} is deprecated
|
||||
* for use at runtime in web modules in favor of parsed patterns with
|
||||
* {@link PathPatternParser}.
|
||||
*/
|
||||
@SuppressWarnings("removal")
|
||||
@Deprecated(since = "7.0", forRemoval = true)
|
||||
@Nullable RequestMatchResult match(HttpServletRequest request, String pattern);
|
||||
|
||||
}
|
||||
-79
@@ -1,79 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-present 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.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.http.server.PathContainer;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.servlet.HandlerExecutionChain;
|
||||
import org.springframework.web.util.ServletRequestPathUtils;
|
||||
import org.springframework.web.util.pattern.PathPattern;
|
||||
import org.springframework.web.util.pattern.PathPatternParser;
|
||||
|
||||
/**
|
||||
* Decorate another {@link MatchableHandlerMapping} that's configured with a
|
||||
* {@link PathPatternParser} in order to parse and cache String patterns passed
|
||||
* into the {@code match} method.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 5.3
|
||||
* @deprecated together with {@link HandlerMappingIntrospector} without a replacement.
|
||||
*/
|
||||
@SuppressWarnings("removal")
|
||||
@Deprecated(since = "7.0", forRemoval = true)
|
||||
class PathPatternMatchableHandlerMapping implements MatchableHandlerMapping {
|
||||
|
||||
private final MatchableHandlerMapping delegate;
|
||||
|
||||
private final PathPatternParser parser;
|
||||
|
||||
private final Map<String, PathPattern> pathPatternCache = new ConcurrentHashMap<>();
|
||||
|
||||
private final int cacheLimit;
|
||||
|
||||
|
||||
public PathPatternMatchableHandlerMapping(MatchableHandlerMapping delegate, int cacheLimit) {
|
||||
Assert.notNull(delegate, "HandlerMapping to delegate to is required.");
|
||||
Assert.notNull(delegate.getPatternParser(), "Expected HandlerMapping configured to use PatternParser.");
|
||||
this.delegate = delegate;
|
||||
this.parser = delegate.getPatternParser();
|
||||
this.cacheLimit = cacheLimit;
|
||||
}
|
||||
|
||||
@SuppressWarnings("removal")
|
||||
@Deprecated(since = "7.0", forRemoval = true)
|
||||
@Override
|
||||
public @Nullable RequestMatchResult match(HttpServletRequest request, String pattern) {
|
||||
PathPattern pathPattern = this.pathPatternCache.computeIfAbsent(pattern, value -> {
|
||||
Assert.state(this.pathPatternCache.size() < this.cacheLimit, "Max size for pattern cache exceeded.");
|
||||
return this.parser.parse(pattern);
|
||||
});
|
||||
PathContainer path = ServletRequestPathUtils.getParsedRequestPath(request).pathWithinApplication();
|
||||
return (pathPattern.matches(path) ? new RequestMatchResult(pathPattern, path) : null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
|
||||
return this.delegate.getHandler(request);
|
||||
}
|
||||
|
||||
}
|
||||
-108
@@ -1,108 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-present 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.util.Map;
|
||||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.http.server.PathContainer;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.PathMatcher;
|
||||
import org.springframework.web.util.UrlPathHelper;
|
||||
import org.springframework.web.util.pattern.PathPattern;
|
||||
import org.springframework.web.util.pattern.PathPatternParser;
|
||||
|
||||
/**
|
||||
* Container for the result from request pattern matching via
|
||||
* {@link MatchableHandlerMapping} with a method to further extract
|
||||
* URI template variables from the pattern.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 4.3.1
|
||||
* @deprecated together with {@link HandlerMappingIntrospector} without a replacement.
|
||||
*/
|
||||
@SuppressWarnings("removal")
|
||||
@Deprecated(since = "7.0", forRemoval = true)
|
||||
public class RequestMatchResult {
|
||||
|
||||
private final @Nullable PathPattern pathPattern;
|
||||
|
||||
private final @Nullable PathContainer lookupPathContainer;
|
||||
|
||||
|
||||
private final @Nullable String pattern;
|
||||
|
||||
private final @Nullable String lookupPath;
|
||||
|
||||
private final @Nullable PathMatcher pathMatcher;
|
||||
|
||||
|
||||
/**
|
||||
* Create an instance with the matched {@code PathPattern}.
|
||||
* @param pathPattern the pattern that was matched
|
||||
* @param lookupPath the mapping path
|
||||
* @since 5.3
|
||||
*/
|
||||
public RequestMatchResult(PathPattern pathPattern, PathContainer lookupPath) {
|
||||
Assert.notNull(pathPattern, "PathPattern is required");
|
||||
Assert.notNull(lookupPath, "PathContainer is required");
|
||||
|
||||
this.pattern = null;
|
||||
this.lookupPath = null;
|
||||
this.pathMatcher = null;
|
||||
|
||||
this.pathPattern = pathPattern;
|
||||
this.lookupPathContainer = lookupPath;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an instance with the matched String pattern.
|
||||
* @param pattern the pattern that was matched, possibly with a '/' appended
|
||||
* @param lookupPath the mapping path
|
||||
* @param pathMatcher the PathMatcher instance used for the match
|
||||
* @deprecated use of {@link PathMatcher} and {@link UrlPathHelper} is deprecated
|
||||
* for use at runtime in web modules in favor of parsed patterns with
|
||||
* {@link PathPatternParser}.
|
||||
*/
|
||||
@Deprecated(since = "7.0", forRemoval = true)
|
||||
public RequestMatchResult(String pattern, String lookupPath, PathMatcher pathMatcher) {
|
||||
Assert.hasText(pattern, "'matchingPattern' is required");
|
||||
Assert.hasText(lookupPath, "'lookupPath' is required");
|
||||
Assert.notNull(pathMatcher, "PathMatcher is required");
|
||||
|
||||
this.pattern = pattern;
|
||||
this.lookupPath = lookupPath;
|
||||
this.pathMatcher = pathMatcher;
|
||||
|
||||
this.pathPattern = null;
|
||||
this.lookupPathContainer = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract URI template variables from the matching pattern as defined in
|
||||
* {@link PathPattern#matchAndExtract(PathContainer)}.
|
||||
* @return a map with URI template variables
|
||||
*/
|
||||
@SuppressWarnings({"ConstantConditions", "NullAway"})
|
||||
public Map<String, String> extractUriTemplateVariables() {
|
||||
return (this.pathPattern != null ?
|
||||
this.pathPattern.matchAndExtract(this.lookupPathContainer).getUriVariables() :
|
||||
this.pathMatcher.extractUriTemplateVariables(this.pattern, this.lookupPath));
|
||||
}
|
||||
}
|
||||
+1
-19
@@ -28,7 +28,6 @@ import java.util.Objects;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.context.EmbeddedValueResolverAware;
|
||||
@@ -55,15 +54,12 @@ import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.cors.CorsConfiguration;
|
||||
import org.springframework.web.method.HandlerMethod;
|
||||
import org.springframework.web.service.annotation.HttpExchange;
|
||||
import org.springframework.web.servlet.handler.MatchableHandlerMapping;
|
||||
import org.springframework.web.servlet.handler.RequestMatchResult;
|
||||
import org.springframework.web.servlet.mvc.condition.AbstractRequestCondition;
|
||||
import org.springframework.web.servlet.mvc.condition.CompositeRequestCondition;
|
||||
import org.springframework.web.servlet.mvc.condition.ConsumesRequestCondition;
|
||||
import org.springframework.web.servlet.mvc.condition.RequestCondition;
|
||||
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
|
||||
import org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping;
|
||||
import org.springframework.web.util.UrlPathHelper;
|
||||
|
||||
/**
|
||||
* Creates {@link RequestMappingInfo} instances from type-level and method-level
|
||||
@@ -79,7 +75,7 @@ import org.springframework.web.util.UrlPathHelper;
|
||||
*/
|
||||
@SuppressWarnings("removal")
|
||||
public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMapping
|
||||
implements MatchableHandlerMapping, EmbeddedValueResolverAware {
|
||||
implements EmbeddedValueResolverAware {
|
||||
|
||||
private static final String[] EMPTY_STRING_ARRAY = new String[0];
|
||||
|
||||
@@ -474,20 +470,6 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("removal")
|
||||
@Deprecated(since = "7.0", forRemoval = true)
|
||||
@Override
|
||||
public @Nullable RequestMatchResult match(HttpServletRequest request, String pattern) {
|
||||
Assert.state(getPatternParser() == null, "This HandlerMapping uses PathPatterns.");
|
||||
RequestMappingInfo info = RequestMappingInfo.paths(pattern).options(this.config).build();
|
||||
RequestMappingInfo match = info.getMatchingCondition(request);
|
||||
return (match != null && match.getPatternsCondition() != null ?
|
||||
new RequestMatchResult(
|
||||
match.getPatternsCondition().getPatterns().iterator().next(),
|
||||
UrlPathHelper.getResolvedLookupPath(request),
|
||||
getPathMatcher()) : null);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @Nullable CorsConfiguration initCorsConfiguration(Object handler, Method method, RequestMappingInfo mappingInfo) {
|
||||
HandlerMethod handlerMethod = createHandlerMethod(handler, method);
|
||||
|
||||
-479
@@ -1,479 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-present 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.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import jakarta.servlet.Filter;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.ServletRequest;
|
||||
import jakarta.servlet.ServletResponse;
|
||||
import jakarta.servlet.http.HttpServlet;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.CrossOrigin;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.context.WebApplicationContext;
|
||||
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
|
||||
import org.springframework.web.context.support.GenericWebApplicationContext;
|
||||
import org.springframework.web.context.support.StaticWebApplicationContext;
|
||||
import org.springframework.web.cors.CorsConfiguration;
|
||||
import org.springframework.web.cors.CorsConfigurationSource;
|
||||
import org.springframework.web.servlet.HandlerExecutionChain;
|
||||
import org.springframework.web.servlet.HandlerMapping;
|
||||
import org.springframework.web.servlet.NoHandlerFoundException;
|
||||
import org.springframework.web.servlet.function.RouterFunction;
|
||||
import org.springframework.web.servlet.function.RouterFunctions;
|
||||
import org.springframework.web.servlet.function.ServerResponse;
|
||||
import org.springframework.web.servlet.function.support.RouterFunctionMapping;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
|
||||
import org.springframework.web.testfixture.servlet.MockFilterChain;
|
||||
import org.springframework.web.testfixture.servlet.MockHttpServletRequest;
|
||||
import org.springframework.web.testfixture.servlet.MockHttpServletResponse;
|
||||
import org.springframework.web.util.ServletRequestPathUtils;
|
||||
import org.springframework.web.util.pattern.PathPattern;
|
||||
import org.springframework.web.util.pattern.PathPatternParser;
|
||||
import org.springframework.web.util.pattern.PatternParseException;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.springframework.web.servlet.HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE;
|
||||
|
||||
/**
|
||||
* Tests for {@link HandlerMappingIntrospector}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 4.3.1
|
||||
*/
|
||||
@SuppressWarnings("removal")
|
||||
class HandlerMappingIntrospectorTests {
|
||||
|
||||
@Test
|
||||
void detectHandlerMappings() {
|
||||
StaticWebApplicationContext context = new StaticWebApplicationContext();
|
||||
context.registerSingleton("A", SimpleUrlHandlerMapping.class);
|
||||
context.registerSingleton("B", SimpleUrlHandlerMapping.class);
|
||||
context.registerSingleton("C", SimpleUrlHandlerMapping.class);
|
||||
context.refresh();
|
||||
|
||||
List<?> expected = Arrays.asList(context.getBean("A"), context.getBean("B"), context.getBean("C"));
|
||||
List<HandlerMapping> actual = initIntrospector(context).getHandlerMappings();
|
||||
|
||||
assertThat(actual).isEqualTo(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
void detectHandlerMappingsOrdered() {
|
||||
GenericWebApplicationContext context = new GenericWebApplicationContext();
|
||||
context.registerBean("B", SimpleUrlHandlerMapping.class, () -> {
|
||||
SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping();
|
||||
mapping.setOrder(2);
|
||||
return mapping;
|
||||
});
|
||||
context.registerBean("C", SimpleUrlHandlerMapping.class, () -> {
|
||||
SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping();
|
||||
mapping.setOrder(3);
|
||||
return mapping;
|
||||
});
|
||||
context.registerBean("A", SimpleUrlHandlerMapping.class, () -> {
|
||||
SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping();
|
||||
mapping.setOrder(1);
|
||||
return mapping;
|
||||
});
|
||||
context.refresh();
|
||||
|
||||
List<?> expected = Arrays.asList(context.getBean("A"), context.getBean("B"), context.getBean("C"));
|
||||
List<HandlerMapping> actual = initIntrospector(context).getHandlerMappings();
|
||||
|
||||
assertThat(actual).isEqualTo(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
void useParsedPatternsOnly() {
|
||||
GenericWebApplicationContext context = new GenericWebApplicationContext();
|
||||
context.registerBean("A", SimpleUrlHandlerMapping.class);
|
||||
context.registerBean("B", SimpleUrlHandlerMapping.class);
|
||||
context.registerBean("C", SimpleUrlHandlerMapping.class);
|
||||
context.refresh();
|
||||
|
||||
assertThat(initIntrospector(context).allHandlerMappingsUsePathPatternParser()).isTrue();
|
||||
|
||||
context = new GenericWebApplicationContext();
|
||||
context.registerBean("A", SimpleUrlHandlerMapping.class);
|
||||
context.registerBean("B", SimpleUrlHandlerMapping.class);
|
||||
context.registerBean("C", SimpleUrlHandlerMapping.class, () -> {
|
||||
SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping();
|
||||
mapping.setPatternParser(null);
|
||||
return mapping;
|
||||
});
|
||||
context.refresh();
|
||||
|
||||
assertThat(initIntrospector(context).allHandlerMappingsUsePathPatternParser()).isFalse();
|
||||
}
|
||||
|
||||
@SuppressWarnings("removal")
|
||||
@ParameterizedTest
|
||||
@ValueSource(booleans = {true, false})
|
||||
void getMatchable(boolean usePathPatterns) throws Exception {
|
||||
|
||||
TestPathPatternParser parser = new TestPathPatternParser();
|
||||
|
||||
GenericWebApplicationContext context = new GenericWebApplicationContext();
|
||||
context.registerBean("mapping", SimpleUrlHandlerMapping.class, () -> {
|
||||
SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping();
|
||||
if (usePathPatterns) {
|
||||
mapping.setPatternParser(parser);
|
||||
}
|
||||
mapping.setUrlMap(Collections.singletonMap("/path/*", new Object()));
|
||||
return mapping;
|
||||
});
|
||||
context.refresh();
|
||||
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/path/123");
|
||||
MatchableHandlerMapping mapping = initIntrospector(context).getMatchableHandlerMapping(request);
|
||||
|
||||
assertThat(mapping).isNotNull();
|
||||
assertThat(request.getAttribute(BEST_MATCHING_PATTERN_ATTRIBUTE)).as("Attribute changes not ignored").isNull();
|
||||
assertThat(request.getAttribute(ServletRequestPathUtils.PATH_ATTRIBUTE)).as("Parsed path not cleaned").isNull();
|
||||
|
||||
assertThat(mapping.match(request, "/p*/*")).isNotNull();
|
||||
assertThat(mapping.match(request, "/b*/*")).isNull();
|
||||
|
||||
if (usePathPatterns) {
|
||||
assertThat(parser.getParsedPatterns()).containsExactly("/path/*", "/p*/*", "/b*/*");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void getMatchableWhereHandlerMappingDoesNotImplementMatchableInterface() {
|
||||
StaticWebApplicationContext cxt = new StaticWebApplicationContext();
|
||||
cxt.registerBean("mapping", HandlerMapping.class, () -> request -> new HandlerExecutionChain(new Object()));
|
||||
cxt.refresh();
|
||||
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
assertThatIllegalStateException().isThrownBy(() -> initIntrospector(cxt).getMatchableHandlerMapping(request));
|
||||
}
|
||||
|
||||
@Test // gh-26833
|
||||
void getMatchablePreservesRequestAttributes() throws Exception {
|
||||
AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
|
||||
context.register(TestConfig.class);
|
||||
context.refresh();
|
||||
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("POST", "/path");
|
||||
request.setAttribute("name", "value");
|
||||
|
||||
MatchableHandlerMapping matchable = initIntrospector(context).getMatchableHandlerMapping(request);
|
||||
assertThat(matchable).isNotNull();
|
||||
|
||||
// RequestPredicates.restoreAttributes clears and re-adds attributes
|
||||
assertThat(request.getAttribute("name")).isEqualTo("value");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getCorsConfigurationPreFlight() {
|
||||
AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
|
||||
context.register(TestConfig.class);
|
||||
context.refresh();
|
||||
|
||||
// PRE-FLIGHT
|
||||
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("OPTIONS", "/path");
|
||||
request.addHeader("Origin", "http://localhost:9000");
|
||||
request.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "POST");
|
||||
CorsConfiguration corsConfig = initIntrospector(context).getCorsConfiguration(request);
|
||||
|
||||
assertThat(corsConfig).isNotNull();
|
||||
assertThat(corsConfig.getAllowedOrigins()).isEqualTo(Collections.singletonList("http://localhost:9000"));
|
||||
assertThat(corsConfig.getAllowedMethods()).isEqualTo(Collections.singletonList("POST"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getCorsConfigurationActual() {
|
||||
AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
|
||||
context.register(TestConfig.class);
|
||||
context.refresh();
|
||||
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("POST", "/path");
|
||||
request.addHeader("Origin", "http://localhost:9000");
|
||||
CorsConfiguration corsConfig = initIntrospector(context).getCorsConfiguration(request);
|
||||
|
||||
assertThat(corsConfig).isNotNull();
|
||||
assertThat(corsConfig.getAllowedOrigins()).isEqualTo(Collections.singletonList("http://localhost:9000"));
|
||||
assertThat(corsConfig.getAllowedMethods()).isEqualTo(Collections.singletonList("POST"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void handlePreFlight() throws Exception {
|
||||
AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
|
||||
context.register(TestConfig.class);
|
||||
context.refresh();
|
||||
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("OPTIONS", "/path");
|
||||
request.addHeader("Origin", "http://localhost:9000");
|
||||
request.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "POST");
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
|
||||
initIntrospector(context).handlePreFlight(request, response);
|
||||
|
||||
assertThat(response.getHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)).isEqualTo("http://localhost:9000");
|
||||
assertThat(response.getHeaders(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS)).containsExactly("POST");
|
||||
}
|
||||
|
||||
@Test
|
||||
void handlePreFlightWithNoHandlerFoundException() {
|
||||
AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
|
||||
context.register(TestConfig.class);
|
||||
context.refresh();
|
||||
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("OPTIONS", "/unknownPath");
|
||||
request.addHeader("Origin", "http://localhost:9000");
|
||||
request.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "POST");
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
|
||||
assertThatThrownBy(() -> initIntrospector(context).handlePreFlight(request, response))
|
||||
.isInstanceOf(NoHandlerFoundException.class);
|
||||
|
||||
assertThat(response.getHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)).isNull();
|
||||
assertThat(response.getHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS)).isNull();
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {"/test", "/resource/1234****"}) // gh-31937
|
||||
void cacheFilter(String uri) throws Exception {
|
||||
CorsConfiguration corsConfig = new CorsConfiguration();
|
||||
TestMatchableHandlerMapping mapping = new TestMatchableHandlerMapping();
|
||||
mapping.registerHandler("/*", new TestHandler(corsConfig));
|
||||
|
||||
HandlerMappingIntrospector introspector = initIntrospector(mapping);
|
||||
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", uri);
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
|
||||
MockFilterChain filterChain = new MockFilterChain(
|
||||
new TestServlet(), introspector.createCacheFilter(), new AuthFilter(introspector, corsConfig));
|
||||
|
||||
filterChain.doFilter(request, response);
|
||||
|
||||
assertThat(response.getContentAsString()).isEqualTo("Success");
|
||||
assertThat(mapping.getInvocationCount()).isEqualTo(1);
|
||||
assertThat(mapping.getMatchCount()).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void cacheFilterWithNestedDispatch() throws Exception {
|
||||
CorsConfiguration corsConfig1 = new CorsConfiguration();
|
||||
CorsConfiguration corsConfig2 = new CorsConfiguration();
|
||||
|
||||
TestMatchableHandlerMapping mapping1 = new TestMatchableHandlerMapping();
|
||||
TestMatchableHandlerMapping mapping2 = new TestMatchableHandlerMapping();
|
||||
|
||||
mapping1.registerHandler("/1", new TestHandler(corsConfig1));
|
||||
mapping2.registerHandler("/2", new TestHandler(corsConfig2));
|
||||
|
||||
HandlerMappingIntrospector introspector = initIntrospector(mapping1, mapping2);
|
||||
|
||||
MockFilterChain filterChain = new MockFilterChain(
|
||||
new TestServlet(),
|
||||
introspector.createCacheFilter(),
|
||||
new AuthFilter(introspector, corsConfig1),
|
||||
(req, res, chain) -> chain.doFilter(new MockHttpServletRequest("GET", "/2"), res),
|
||||
introspector.createCacheFilter(),
|
||||
new AuthFilter(introspector, corsConfig2));
|
||||
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
filterChain.doFilter(new MockHttpServletRequest("GET", "/1"), response);
|
||||
|
||||
assertThat(response.getContentAsString()).isEqualTo("Success");
|
||||
assertThat(mapping1.getInvocationCount()).isEqualTo(2);
|
||||
assertThat(mapping2.getInvocationCount()).isEqualTo(1);
|
||||
assertThat(mapping1.getMatchCount()).isEqualTo(1);
|
||||
assertThat(mapping2.getMatchCount()).isEqualTo(1);
|
||||
}
|
||||
|
||||
private HandlerMappingIntrospector initIntrospector(TestMatchableHandlerMapping... mappings) {
|
||||
StaticWebApplicationContext context = new StaticWebApplicationContext();
|
||||
int index = 0;
|
||||
for (TestMatchableHandlerMapping mapping : mappings) {
|
||||
context.registerBean("mapping" + index++, TestMatchableHandlerMapping.class, () -> mapping);
|
||||
}
|
||||
context.refresh();
|
||||
return initIntrospector(context);
|
||||
}
|
||||
|
||||
private static HandlerMappingIntrospector initIntrospector(WebApplicationContext context) {
|
||||
HandlerMappingIntrospector introspector = new HandlerMappingIntrospector();
|
||||
introspector.setApplicationContext(context);
|
||||
introspector.afterPropertiesSet();
|
||||
return introspector;
|
||||
}
|
||||
|
||||
|
||||
@Configuration
|
||||
static class TestConfig {
|
||||
|
||||
@Bean
|
||||
public RouterFunctionMapping routerFunctionMapping() {
|
||||
RouterFunctionMapping mapping = new RouterFunctionMapping();
|
||||
mapping.setOrder(1);
|
||||
return mapping;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public RequestMappingHandlerMapping handlerMapping() {
|
||||
RequestMappingHandlerMapping mapping = new RequestMappingHandlerMapping();
|
||||
mapping.setOrder(2);
|
||||
return mapping;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public TestController testController() {
|
||||
return new TestController();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public RouterFunction<?> routerFunction() {
|
||||
return RouterFunctions.route().GET("/fn-path", request -> ServerResponse.ok().build()).build();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@CrossOrigin("http://localhost:9000")
|
||||
@Controller
|
||||
private static class TestController {
|
||||
|
||||
@PostMapping("/path")
|
||||
void handle() {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static class TestPathPatternParser extends PathPatternParser {
|
||||
|
||||
private final List<String> parsedPatterns = new ArrayList<>();
|
||||
|
||||
|
||||
public List<String> getParsedPatterns() {
|
||||
return this.parsedPatterns;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PathPattern parse(String pathPattern) throws PatternParseException {
|
||||
this.parsedPatterns.add(pathPattern);
|
||||
return super.parse(pathPattern);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static class TestMatchableHandlerMapping extends SimpleUrlHandlerMapping {
|
||||
|
||||
private int invocationCount;
|
||||
|
||||
private int matchCount;
|
||||
|
||||
public int getInvocationCount() {
|
||||
return this.invocationCount;
|
||||
}
|
||||
|
||||
public int getMatchCount() {
|
||||
return this.matchCount;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object getHandlerInternal(HttpServletRequest request) throws Exception {
|
||||
this.invocationCount++;
|
||||
Object handler = super.getHandlerInternal(request);
|
||||
if (handler != null) {
|
||||
this.matchCount++;
|
||||
}
|
||||
return handler;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static class TestHandler implements CorsConfigurationSource {
|
||||
|
||||
private final CorsConfiguration corsConfig;
|
||||
|
||||
private TestHandler(CorsConfiguration corsConfig) {
|
||||
this.corsConfig = corsConfig;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CorsConfiguration getCorsConfiguration(HttpServletRequest request) {
|
||||
return this.corsConfig;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static class AuthFilter implements Filter {
|
||||
|
||||
private final HandlerMappingIntrospector introspector;
|
||||
|
||||
private final CorsConfiguration corsConfig;
|
||||
|
||||
private AuthFilter(HandlerMappingIntrospector introspector, CorsConfiguration corsConfig) {
|
||||
this.introspector = introspector;
|
||||
this.corsConfig = corsConfig;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
|
||||
try {
|
||||
for (int i = 0; i < 10; i++) {
|
||||
HttpServletRequest httpRequest = (HttpServletRequest) req;
|
||||
assertThat(introspector.getMatchableHandlerMapping(httpRequest)).isNotNull();
|
||||
assertThat(introspector.getCorsConfiguration(httpRequest)).isSameAs(corsConfig);
|
||||
}
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
chain.doFilter(req, res);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
private static class TestServlet extends HttpServlet {
|
||||
|
||||
@Override
|
||||
protected void service(HttpServletRequest req, HttpServletResponse res) {
|
||||
try {
|
||||
res.getWriter().print("Success");
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user