Remove HandlerMappingIntrospector

Closes gh-36481
This commit is contained in:
rstoyanchev
2026-07-29 18:03:09 +03:00
parent f53674d582
commit 8bc5e11ec3
8 changed files with 3 additions and 1402 deletions
@@ -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");
}
}
@@ -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
@@ -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} &mdash; obtain a {@code HandlerMapping}
* to check request-matching criteria against.
* <li>{@link #getCorsConfiguration} &mdash; 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);
}
}
}
@@ -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);
}
@@ -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);
}
}
@@ -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));
}
}
@@ -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);
@@ -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);
}
}
}
}