diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/setup/StandaloneMockMvcBuilder.java b/spring-test/src/main/java/org/springframework/test/web/servlet/setup/StandaloneMockMvcBuilder.java index 7a547b5fe1..c1661584f1 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/setup/StandaloneMockMvcBuilder.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/setup/StandaloneMockMvcBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 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. @@ -127,6 +127,8 @@ public class StandaloneMockMvcBuilder extends AbstractMockMvcBuilderIn Spring MVC, either a resolved String lookupPath or a parsed * {@code RequestPath} is always available within {@code DispatcherServlet} - * processing. However in a Servlet {@code Filter} such as {@code CorsFilter} + * processing. However, in a Servlet {@code Filter} such as {@code CorsFilter} * that may or may not be the case. *

By default this is set to {@code true} in which case lazy lookupPath * initialization is allowed. Set this to {@code false} when an diff --git a/spring-web/src/main/java/org/springframework/web/util/ServletRequestPathUtils.java b/spring-web/src/main/java/org/springframework/web/util/ServletRequestPathUtils.java index 99a431a4f1..8dd025f67e 100644 --- a/spring-web/src/main/java/org/springframework/web/util/ServletRequestPathUtils.java +++ b/spring-web/src/main/java/org/springframework/web/util/ServletRequestPathUtils.java @@ -252,6 +252,9 @@ public abstract class ServletRequestPathUtils { if (UrlPathHelper.servlet4Present) { String servletPathPrefix = Servlet4Delegate.getServletPathPrefix(request); if (StringUtils.hasText(servletPathPrefix)) { + if (servletPathPrefix.endsWith("/")) { + servletPathPrefix = servletPathPrefix.substring(0, servletPathPrefix.length() - 1); + } return new ServletRequestPath(requestUri, request.getContextPath(), servletPathPrefix); } } @@ -272,8 +275,7 @@ public abstract class ServletRequestPathUtils { if (mapping == null) { mapping = request.getHttpServletMapping(); } - MappingMatch match = mapping.getMappingMatch(); - if (!ObjectUtils.nullSafeEquals(match, MappingMatch.PATH)) { + if (!ObjectUtils.nullSafeEquals(mapping.getMappingMatch(), MappingMatch.PATH)) { return null; } String servletPath = (String) request.getAttribute(WebUtils.INCLUDE_SERVLET_PATH_ATTRIBUTE); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/AnnotationDrivenBeanDefinitionParser.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/AnnotationDrivenBeanDefinitionParser.java index a4e4fa85ee..8ee86bae79 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/AnnotationDrivenBeanDefinitionParser.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/AnnotationDrivenBeanDefinitionParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 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. @@ -405,22 +405,28 @@ class AnnotationDrivenBeanDefinitionParser implements BeanDefinitionParser { if (pathMatchingElement != null) { Object source = context.extractSource(element); - if (pathMatchingElement.hasAttribute("suffix-pattern")) { - Boolean useSuffixPatternMatch = Boolean.valueOf(pathMatchingElement.getAttribute("suffix-pattern")); - handlerMappingDef.getPropertyValues().add("useSuffixPatternMatch", useSuffixPatternMatch); - } if (pathMatchingElement.hasAttribute("trailing-slash")) { - Boolean useTrailingSlashMatch = Boolean.valueOf(pathMatchingElement.getAttribute("trailing-slash")); + boolean useTrailingSlashMatch = Boolean.parseBoolean(pathMatchingElement.getAttribute("trailing-slash")); handlerMappingDef.getPropertyValues().add("useTrailingSlashMatch", useTrailingSlashMatch); } + + boolean preferPathMatcher = false; + + if (pathMatchingElement.hasAttribute("suffix-pattern")) { + boolean useSuffixPatternMatch = Boolean.parseBoolean(pathMatchingElement.getAttribute("suffix-pattern")); + handlerMappingDef.getPropertyValues().add("useSuffixPatternMatch", useSuffixPatternMatch); + preferPathMatcher |= useSuffixPatternMatch; + } if (pathMatchingElement.hasAttribute("registered-suffixes-only")) { - Boolean useRegisteredSuffixPatternMatch = Boolean.valueOf(pathMatchingElement.getAttribute("registered-suffixes-only")); + boolean useRegisteredSuffixPatternMatch = Boolean.parseBoolean(pathMatchingElement.getAttribute("registered-suffixes-only")); handlerMappingDef.getPropertyValues().add("useRegisteredSuffixPatternMatch", useRegisteredSuffixPatternMatch); + preferPathMatcher |= useRegisteredSuffixPatternMatch; } RuntimeBeanReference pathHelperRef = null; if (pathMatchingElement.hasAttribute("path-helper")) { pathHelperRef = new RuntimeBeanReference(pathMatchingElement.getAttribute("path-helper")); + preferPathMatcher = true; } pathHelperRef = MvcNamespaceUtils.registerUrlPathHelper(pathHelperRef, context, source); handlerMappingDef.getPropertyValues().add("urlPathHelper", pathHelperRef); @@ -428,10 +434,16 @@ class AnnotationDrivenBeanDefinitionParser implements BeanDefinitionParser { RuntimeBeanReference pathMatcherRef = null; if (pathMatchingElement.hasAttribute("path-matcher")) { pathMatcherRef = new RuntimeBeanReference(pathMatchingElement.getAttribute("path-matcher")); + preferPathMatcher = true; } pathMatcherRef = MvcNamespaceUtils.registerPathMatcher(pathMatcherRef, context, source); handlerMappingDef.getPropertyValues().add("pathMatcher", pathMatcherRef); + + if (preferPathMatcher) { + handlerMappingDef.getPropertyValues().add("patternParser", null); + } } + } private Properties getDefaultMediaTypes() { diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/PathMatchConfigurer.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/PathMatchConfigurer.java index 113d59cfb3..18cf3e4813 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/PathMatchConfigurer.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/PathMatchConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 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. @@ -23,7 +23,6 @@ import java.util.function.Predicate; import org.springframework.lang.Nullable; import org.springframework.util.AntPathMatcher; import org.springframework.util.PathMatcher; -import org.springframework.web.servlet.handler.AbstractHandlerMapping; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; import org.springframework.web.util.UrlPathHelper; import org.springframework.web.util.pattern.PathPattern; @@ -34,14 +33,19 @@ import org.springframework.web.util.pattern.PathPatternParser; *

* * @author Brian Clozel + * @author Rossen Stoyanchev * @since 4.0.3 */ public class PathMatchConfigurer { + private boolean preferPathMatcher = false; + @Nullable private PathPatternParser patternParser; @@ -74,17 +78,28 @@ public class PathMatchConfigurer { /** - * Enable use of parsed {@link PathPattern}s as described in - * {@link AbstractHandlerMapping#setPatternParser(PathPatternParser)}. - *

Note: This is mutually exclusive with use of - * {@link #setUrlPathHelper(UrlPathHelper)} and - * {@link #setPathMatcher(PathMatcher)}. - *

By default this is not enabled. + * Set the {@link PathPatternParser} to parse {@link PathPattern patterns} + * with for URL path matching. Parsed patterns provide a more modern and + * efficient alternative to String path matching via {@link AntPathMatcher}. + *

Note: This property is mutually exclusive with the + * following other, {@code AntPathMatcher} related properties: + *

+ *

By default, as of 6.0, a {@link PathPatternParser} with default + * settings is used, which enables parsed {@link PathPattern patterns}. + * Set this property to {@code null} to fall back on String path matching via + * {@link AntPathMatcher} instead, or alternatively, setting one of the above + * listed {@code AntPathMatcher} related properties has the same effect. * @param patternParser the parser to pre-parse patterns with * @since 5.3 */ - public PathMatchConfigurer setPatternParser(PathPatternParser patternParser) { + public PathMatchConfigurer setPatternParser(@Nullable PathPatternParser patternParser) { this.patternParser = patternParser; + this.preferPathMatcher = (patternParser == null); return this; } @@ -120,9 +135,11 @@ public class PathMatchConfigurer { /** * Whether to use suffix pattern match (".*") when matching patterns to * requests. If enabled a method mapped to "/users" also matches to "/users.*". + *

Note: This property is mutually exclusive with + * {@link #setPatternParser(PathPatternParser)}. If set, it enables use of + * String path matching, unless a {@code PathPatternParser} is also + * explicitly set in which case this property is ignored. *

By default this is set to {@code false}. - *

Note: This property is mutually exclusive with and - * ignored when {@link #setPatternParser(PathPatternParser)} is set. * @deprecated as of 5.2.4. See class-level note in * {@link RequestMappingHandlerMapping} on the deprecation of path extension * config options. As there is no replacement for this method, in 5.2.x it is @@ -132,6 +149,7 @@ public class PathMatchConfigurer { @Deprecated public PathMatchConfigurer setUseSuffixPatternMatch(Boolean suffixPatternMatch) { this.suffixPatternMatch = suffixPatternMatch; + this.preferPathMatcher |= suffixPatternMatch; return this; } @@ -141,9 +159,11 @@ public class PathMatchConfigurer { * {@link WebMvcConfigurer#configureContentNegotiation configure content * negotiation}. This is generally recommended to reduce ambiguity and to * avoid issues such as when a "." appears in the path for other reasons. + *

Note: This property is mutually exclusive with + * {@link #setPatternParser(PathPatternParser)}. If set, it enables use of + * String path matching, unless a {@code PathPatternParser} is also + * explicitly set in which case this property is ignored. *

By default this is set to "false". - *

Note: This property is mutually exclusive with and - * ignored when {@link #setPatternParser(PathPatternParser)} is set. * @deprecated as of 5.2.4. See class-level note in * {@link RequestMappingHandlerMapping} on the deprecation of path extension * config options. @@ -151,31 +171,54 @@ public class PathMatchConfigurer { @Deprecated public PathMatchConfigurer setUseRegisteredSuffixPatternMatch(Boolean registeredSuffixPatternMatch) { this.registeredSuffixPatternMatch = registeredSuffixPatternMatch; + this.preferPathMatcher |= registeredSuffixPatternMatch; return this; } /** * Set the UrlPathHelper to use to resolve the mapping path for the application. - *

Note: This property is mutually exclusive with and - * ignored when {@link #setPatternParser(PathPatternParser)} is set. + *

Note: This property is mutually exclusive with + * {@link #setPatternParser(PathPatternParser)}. If set, it enables use of + * String path matching, unless a {@code PathPatternParser} is also + * explicitly set in which case this property is ignored. + *

By default this is an instance of {@link UrlPathHelper} with default + * settings. */ public PathMatchConfigurer setUrlPathHelper(UrlPathHelper urlPathHelper) { this.urlPathHelper = urlPathHelper; + this.preferPathMatcher = true; return this; } /** * Set the PathMatcher to use for String pattern matching. - *

By default this is {@link AntPathMatcher}. - *

Note: This property is mutually exclusive with and - * ignored when {@link #setPatternParser(PathPatternParser)} is set. + *

Note: This property is mutually exclusive with + * {@link #setPatternParser(PathPatternParser)}. If set, it enables use of + * String path matching, unless a {@code PathPatternParser} is also + * explicitly set in which case this property is ignored. + *

By default this is an instance of {@link AntPathMatcher} with default + * settings. */ public PathMatchConfigurer setPathMatcher(PathMatcher pathMatcher) { this.pathMatcher = pathMatcher; + this.preferPathMatcher = true; return this; } + /** + * Whether to prefer {@link PathMatcher}. This is the case when either is true: + *

+ * @since 6.0 + */ + protected boolean preferPathMatcher() { + return (this.patternParser == null && this.preferPathMatcher); + } + /** * Return the {@link PathPatternParser} to use, if configured. * @since 5.3 diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java index d929f44e54..bd9c8897b1 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 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. @@ -313,18 +313,12 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv RequestMappingHandlerMapping mapping = createRequestMappingHandlerMapping(); mapping.setOrder(0); - mapping.setInterceptors(getInterceptors(conversionService, resourceUrlProvider)); mapping.setContentNegotiationManager(contentNegotiationManager); - mapping.setCorsConfigurations(getCorsConfigurations()); + + initHandlerMapping(mapping, conversionService, resourceUrlProvider); PathMatchConfigurer pathConfig = getPathMatchConfigurer(); - if (pathConfig.getPatternParser() != null) { - mapping.setPatternParser(pathConfig.getPatternParser()); - } - else { - mapping.setUrlPathHelper(pathConfig.getUrlPathHelperOrDefault()); - mapping.setPathMatcher(pathConfig.getPathMatcherOrDefault()); - + if (pathConfig.preferPathMatcher()) { Boolean useSuffixPatternMatch = pathConfig.isUseSuffixPatternMatch(); if (useSuffixPatternMatch != null) { mapping.setUseSuffixPatternMatch(useSuffixPatternMatch); @@ -334,10 +328,12 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv mapping.setUseRegisteredSuffixPatternMatch(useRegisteredSuffixPatternMatch); } } + Boolean useTrailingSlashMatch = pathConfig.isUseTrailingSlashMatch(); if (useTrailingSlashMatch != null) { mapping.setUseTrailingSlashMatch(useTrailingSlashMatch); } + if (pathConfig.getPathPrefixes() != null) { mapping.setPathPrefixes(pathConfig.getPathPrefixes()); } @@ -497,21 +493,29 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv ViewControllerRegistry registry = new ViewControllerRegistry(this.applicationContext); addViewControllers(registry); - AbstractHandlerMapping handlerMapping = registry.buildHandlerMapping(); - if (handlerMapping == null) { - return null; + AbstractHandlerMapping mapping = registry.buildHandlerMapping(); + initHandlerMapping(mapping, conversionService, resourceUrlProvider); + return mapping; + } + + private void initHandlerMapping( + @Nullable AbstractHandlerMapping mapping, FormattingConversionService conversionService, + ResourceUrlProvider resourceUrlProvider) { + + if (mapping == null) { + return; } PathMatchConfigurer pathConfig = getPathMatchConfigurer(); - if (pathConfig.getPatternParser() != null) { - handlerMapping.setPatternParser(pathConfig.getPatternParser()); + if (pathConfig.preferPathMatcher()) { + mapping.setPatternParser(null); + mapping.setUrlPathHelper(pathConfig.getUrlPathHelperOrDefault()); + mapping.setPathMatcher(pathConfig.getPathMatcherOrDefault()); } - else { - handlerMapping.setUrlPathHelper(pathConfig.getUrlPathHelperOrDefault()); - handlerMapping.setPathMatcher(pathConfig.getPathMatcherOrDefault()); + else if (pathConfig.getPatternParser() != null) { + mapping.setPatternParser(pathConfig.getPatternParser()); } - handlerMapping.setInterceptors(getInterceptors(conversionService, resourceUrlProvider)); - handlerMapping.setCorsConfigurations(getCorsConfigurations()); - return handlerMapping; + mapping.setInterceptors(getInterceptors(conversionService, resourceUrlProvider)); + mapping.setCorsConfigurations(getCorsConfigurations()); } /** @@ -532,18 +536,7 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv BeanNameUrlHandlerMapping mapping = new BeanNameUrlHandlerMapping(); mapping.setOrder(2); - - PathMatchConfigurer pathConfig = getPathMatchConfigurer(); - if (pathConfig.getPatternParser() != null) { - mapping.setPatternParser(pathConfig.getPatternParser()); - } - else { - mapping.setUrlPathHelper(pathConfig.getUrlPathHelperOrDefault()); - mapping.setPathMatcher(pathConfig.getPathMatcherOrDefault()); - } - - mapping.setInterceptors(getInterceptors(conversionService, resourceUrlProvider)); - mapping.setCorsConfigurations(getCorsConfigurations()); + initHandlerMapping(mapping, conversionService, resourceUrlProvider); return mapping; } @@ -599,20 +592,9 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv this.servletContext, contentNegotiationManager, pathConfig.getUrlPathHelper()); addResourceHandlers(registry); - AbstractHandlerMapping handlerMapping = registry.getHandlerMapping(); - if (handlerMapping == null) { - return null; - } - if (pathConfig.getPatternParser() != null) { - handlerMapping.setPatternParser(pathConfig.getPatternParser()); - } - else { - handlerMapping.setUrlPathHelper(pathConfig.getUrlPathHelperOrDefault()); - handlerMapping.setPathMatcher(pathConfig.getPathMatcherOrDefault()); - } - handlerMapping.setInterceptors(getInterceptors(conversionService, resourceUrlProvider)); - handlerMapping.setCorsConfigurations(getCorsConfigurations()); - return handlerMapping; + AbstractHandlerMapping mapping = registry.getHandlerMapping(); + initHandlerMapping(mapping, conversionService, resourceUrlProvider); + return mapping; } /** diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMapping.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMapping.java index 0f4f30ae32..a900e9f5f2 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMapping.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMapping.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 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. @@ -49,6 +49,7 @@ import org.springframework.web.cors.CorsProcessor; import org.springframework.web.cors.CorsUtils; import org.springframework.web.cors.DefaultCorsProcessor; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.servlet.DispatcherServlet; import org.springframework.web.servlet.HandlerExecutionChain; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.HandlerMapping; @@ -87,7 +88,7 @@ public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport private Object defaultHandler; @Nullable - private PathPatternParser patternParser; + private PathPatternParser patternParser = new PathPatternParser(); private UrlPathHelper urlPathHelper = new UrlPathHelper(); @@ -127,43 +128,45 @@ public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport } /** - * Enable use of pre-parsed {@link PathPattern}s as an alternative to - * String pattern matching with {@link AntPathMatcher}. The syntax is - * largely the same but the {@code PathPattern} syntax is more tailored for - * web applications, and its implementation is more efficient. - *

This property is mutually exclusive with the following others which - * are effectively ignored when this is set: + * Set the {@link PathPatternParser} to parse {@link PathPattern patterns} + * with for URL path matching. Parsed patterns provide a more modern and + * efficient alternative to String path matching via {@link AntPathMatcher}. + *

Note: This property is mutually exclusive with the + * below properties, all of which are not necessary for parsed patterns and + * are ignored when a {@code PathPatternParser} is available: *

- *

By default this is not set. + *

By default, as of 6.0, this is set to a {@link PathPatternParser} + * instance with default settings and therefore use of parsed patterns is + * enabled. Set this to {@code null} to switch to String path matching + * via {@link AntPathMatcher} instead. * @param patternParser the parser to use * @since 5.3 */ - public void setPatternParser(PathPatternParser patternParser) { + public void setPatternParser(@Nullable PathPatternParser patternParser) { this.patternParser = patternParser; } /** * Return the {@link #setPatternParser(PathPatternParser) configured} - * {@code PathPatternParser}, or {@code null}. + * {@code PathPatternParser}, or {@code null} otherwise which indicates that + * String pattern matching with {@link AntPathMatcher} is enabled instead. * @since 5.3 */ @Nullable @@ -570,7 +573,7 @@ public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport protected String initLookupPath(HttpServletRequest request) { if (usesPathPatterns()) { request.removeAttribute(UrlPathHelper.PATH_ATTRIBUTE); - RequestPath requestPath = ServletRequestPathUtils.getParsedRequestPath(request); + RequestPath requestPath = getRequestPath(request); String lookupPath = requestPath.pathWithinApplication().value(); return UrlPathHelper.defaultInstance.removeSemicolonContent(lookupPath); } @@ -579,6 +582,14 @@ public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport } } + private RequestPath getRequestPath(HttpServletRequest request) { + // Expect pre-parsed path with DispatcherServlet, + // but otherwise parse per handler lookup + cache for handling + return request.getAttribute(DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE) != null ? + ServletRequestPathUtils.getParsedRequestPath(request) : + ServletRequestPathUtils.parseAndCache(request); + } + /** * Build a {@link HandlerExecutionChain} for the given handler, including * applicable interceptors. diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMethodMapping.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMethodMapping.java index 39480d9147..5ebd32a6f4 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMethodMapping.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMethodMapping.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 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. @@ -102,7 +102,7 @@ public abstract class AbstractHandlerMethodMapping extends AbstractHandlerMap @Override - public void setPatternParser(PathPatternParser patternParser) { + public void setPatternParser(@Nullable PathPatternParser patternParser) { Assert.state(this.mappingRegistry.getRegistrations().isEmpty(), "PathPatternParser must be set before the initialization of " + "request mappings through InitializingBean#afterPropertiesSet."); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractUrlHandlerMapping.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractUrlHandlerMapping.java index a12aa46521..7fa02e8beb 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractUrlHandlerMapping.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractUrlHandlerMapping.java @@ -75,7 +75,7 @@ public abstract class AbstractUrlHandlerMapping extends AbstractHandlerMapping i @Override - public void setPatternParser(PathPatternParser patternParser) { + public void setPatternParser(@Nullable PathPatternParser patternParser) { Assert.state(this.handlerMap.isEmpty(), "PathPatternParser must be set before the initialization of " + "the handler map via ApplicationContextAware#setApplicationContext."); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java index 57fa565e1d..051ec87dbe 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java @@ -76,6 +76,8 @@ import org.springframework.web.util.pattern.PathPatternParser; public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMapping implements MatchableHandlerMapping, EmbeddedValueResolverAware { + private boolean defaultPatternParser = true; + private boolean useSuffixPatternMatch = false; private boolean useRegisteredSuffixPatternMatch = false; @@ -92,6 +94,14 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi private RequestMappingInfo.BuilderConfiguration config = new RequestMappingInfo.BuilderConfiguration(); + @Override + public void setPatternParser(@Nullable PathPatternParser patternParser) { + if (patternParser != null) { + this.defaultPatternParser = false; + } + super.setPatternParser(patternParser); + } + /** * Whether to use suffix pattern match (".*") when matching patterns to * requests. If enabled a method mapped to "/users" also matches to "/users.*". @@ -191,6 +201,12 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi this.config.setTrailingSlashMatch(useTrailingSlashMatch()); this.config.setContentNegotiationManager(getContentNegotiationManager()); + if (getPatternParser() != null && this.defaultPatternParser && + (this.useSuffixPatternMatch || this.useRegisteredSuffixPatternMatch)) { + + setPatternParser(null); + } + if (getPatternParser() != null) { this.config.setPatternParser(getPatternParser()); Assert.isTrue(!this.useSuffixPatternMatch && !this.useRegisteredSuffixPatternMatch, diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/MvcNamespaceTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/MvcNamespaceTests.java index 185c7fe02c..d908fdb7f1 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/MvcNamespaceTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/MvcNamespaceTests.java @@ -37,6 +37,7 @@ import com.fasterxml.jackson.databind.MapperFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.dataformat.xml.XmlMapper; import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.http.MappingMatch; import jakarta.validation.constraints.NotNull; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -137,6 +138,7 @@ import org.springframework.web.servlet.view.groovy.GroovyMarkupConfigurer; import org.springframework.web.servlet.view.groovy.GroovyMarkupViewResolver; import org.springframework.web.servlet.view.script.ScriptTemplateConfigurer; import org.springframework.web.servlet.view.script.ScriptTemplateViewResolver; +import org.springframework.web.testfixture.servlet.MockHttpServletMapping; import org.springframework.web.testfixture.servlet.MockHttpServletRequest; import org.springframework.web.testfixture.servlet.MockHttpServletResponse; import org.springframework.web.testfixture.servlet.MockRequestDispatcher; @@ -688,6 +690,7 @@ public class MvcNamespaceTests { request.setRequestURI("/myapp/app/"); request.setContextPath("/myapp"); request.setServletPath("/app/"); + request.setHttpServletMapping(new MockHttpServletMapping("", "", "", MappingMatch.PATH)); chain = mapping2.getHandler(request); assertThat(chain.getInterceptorList().size()).isEqualTo(4); assertThat(chain.getInterceptorList().get(1) instanceof ConversionServiceExposingInterceptor).isTrue(); diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/DelegatingWebMvcConfigurationIntegrationTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/DelegatingWebMvcConfigurationIntegrationTests.java index ffc38054e3..f48a0242fa 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/DelegatingWebMvcConfigurationIntegrationTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/DelegatingWebMvcConfigurationIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2022 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. @@ -181,6 +181,7 @@ public class DelegatingWebMvcConfigurationIntegrationTests { this.context = webContext; } + @Configuration static class ViewControllerConfiguration implements WebMvcConfigurer { @@ -188,8 +189,16 @@ public class DelegatingWebMvcConfigurationIntegrationTests { public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController("/test"); } + + @Override + public void configurePathMatch(PathMatchConfigurer configurer) { + // tests need to check the "mvcPathMatcher" and "mvcUrlPathHelper" instances + configurer.setPatternParser(null); + } + } + @Configuration static class ResourceHandlerConfiguration implements WebMvcConfigurer { @@ -197,5 +206,13 @@ public class DelegatingWebMvcConfigurationIntegrationTests { public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/resources/**"); } + + @Override + public void configurePathMatch(PathMatchConfigurer configurer) { + // tests need to check the "mvcPathMatcher" and "mvcUrlPathHelper" instances + configurer.setPatternParser(null); + } + } + } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/BeanNameUrlHandlerMappingTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/BeanNameUrlHandlerMappingTests.java index 5e166b87f3..91f40c2740 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/BeanNameUrlHandlerMappingTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/BeanNameUrlHandlerMappingTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2022 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. @@ -37,8 +37,6 @@ import static org.assertj.core.api.Assertions.assertThatIllegalStateException; */ public class BeanNameUrlHandlerMappingTests { - public static final String CONF = "/org/springframework/web/servlet/handler/map1.xml"; - private ConfigurableWebApplicationContext wac; @@ -47,7 +45,7 @@ public class BeanNameUrlHandlerMappingTests { MockServletContext sc = new MockServletContext(""); wac = new XmlWebApplicationContext(); wac.setServletContext(sc); - wac.setConfigLocations(new String[] {CONF}); + wac.setConfigLocations("/org/springframework/web/servlet/handler/map1.xml"); wac.refresh(); } @@ -55,7 +53,7 @@ public class BeanNameUrlHandlerMappingTests { public void requestsWithoutHandlers() throws Exception { HandlerMapping hm = (HandlerMapping) wac.getBean("handlerMapping"); - MockHttpServletRequest req = new MockHttpServletRequest("GET", "/mypath/nonsense.html"); + MockHttpServletRequest req = new MockHttpServletRequest("GET", "/myapp/mypath/nonsense.html"); req.setContextPath("/myapp"); Object h = hm.getHandler(req); assertThat(h == null).as("Handler is null").isTrue(); @@ -121,6 +119,7 @@ public class BeanNameUrlHandlerMappingTests { @Test public void requestsWithFullPaths() throws Exception { BeanNameUrlHandlerMapping hm = new BeanNameUrlHandlerMapping(); + hm.setPatternParser(null); // the test targets AntPathPatcher-specific feature hm.setAlwaysUseFullPath(true); hm.setApplicationContext(wac); Object bean = wac.getBean("godCtrl"); diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/AbstractServletHandlerMethodTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/AbstractServletHandlerMethodTests.java index 3d99f5a4f1..4c260f3a8a 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/AbstractServletHandlerMethodTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/AbstractServletHandlerMethodTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2022 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. @@ -29,7 +29,6 @@ import org.springframework.web.servlet.DispatcherServlet; import org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver; import org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver; import org.springframework.web.testfixture.servlet.MockServletConfig; -import org.springframework.web.util.pattern.PathPatternParser; import static org.assertj.core.api.Assertions.assertThat; @@ -97,9 +96,8 @@ public abstract class AbstractServletHandlerMethodTests { } BeanDefinition mappingDef = wac.getBeanDefinition("handlerMapping"); - if (usePathPatterns && !mappingDef.hasAttribute("patternParser")) { - BeanDefinition parserDef = register("parser", PathPatternParser.class, wac); - mappingDef.getPropertyValues().add("patternParser", parserDef); + if (!usePathPatterns) { + mappingDef.getPropertyValues().add("patternParser", null); } register("handlerAdapter", RequestMappingHandlerAdapter.class, wac); diff --git a/spring-webmvc/src/test/resources/org/springframework/web/servlet/handler/map3.xml b/spring-webmvc/src/test/resources/org/springframework/web/servlet/handler/map3.xml index 2d709e4c36..ff8552c57c 100644 --- a/spring-webmvc/src/test/resources/org/springframework/web/servlet/handler/map3.xml +++ b/spring-webmvc/src/test/resources/org/springframework/web/servlet/handler/map3.xml @@ -11,6 +11,7 @@ + true diff --git a/src/docs/asciidoc/web/webmvc.adoc b/src/docs/asciidoc/web/webmvc.adoc index 65b240bccc..b4c32c0e55 100644 --- a/src/docs/asciidoc/web/webmvc.adoc +++ b/src/docs/asciidoc/web/webmvc.adoc @@ -580,8 +580,8 @@ initialization parameters (`init-param` elements) to the Servlet declaration in The Servlet API exposes the full request path as `requestURI` and further sub-divides it into `contextPath`, `servletPath`, and `pathInfo` whose values vary depending on how a Servlet is mapped. From these inputs, Spring MVC needs to determine the lookup path to -use for handler mapping, which is the path within the mapping of the `DispatcherServlet` -itself, excluding the `contextPath` and any `servletMapping` prefix, if present. +use for mapping handlers, which should exclude the `contextPath` and any `servletMapping` +prefix, if applicable. The `servletPath` and `pathInfo` are decoded and that makes them impossible to compare directly to the full `requestURI` in order to derive the lookupPath and that makes it @@ -611,15 +611,17 @@ encoded path which may not always work well. Furthermore, sometimes the `DispatcherServlet` needs to share the URL space with another Servlet and may need to be mapped by prefix. -The above issues can be addressed more comprehensively by switching from `PathMatcher` to -the parsed `PathPattern` available in 5.3 or higher, see -<>. Unlike `AntPathMatcher` which needs -either the lookup path decoded or the controller mapping encoded, a parsed `PathPattern` -matches to a parsed representation of the path called `RequestPath`, one path segment -at a time. This allows decoding and sanitizing path segment values individually without -the risk of altering the structure of the path. Parsed `PathPattern` also supports -the use of `servletPath` prefix mapping as long as the prefix is kept simple and does -not have any characters that need to be encoded. +The above issues are addressed when using `PathPatternParser` and parsed patterns, as +an alternative to String path matching with `AntPathMatcher`. The `PathPatternParser` has +been available for use in Spring MVC from version 5.3, and is enabled by default from +version 6.0. Unlike `AntPathMatcher` which needs either the lookup path decoded or the +controller mapping encoded, a parsed `PathPattern` matches to a parsed representation +of the path called `RequestPath`, one path segment at a time. This allows decoding and +sanitizing path segment values individually without the risk of altering the structure +of the path. Parsed `PathPattern` also supports the use of `servletPath` prefix mapping +as long as a Servlet path mapping is used and the prefix is kept simple, i.e. it has no +encoded characters. For pattern syntax details and comparison, see +<>. @@ -1617,11 +1619,11 @@ filesystem, and other locations. It is less efficient and the String path input challenge for dealing effectively with encoding and other issues with URLs. `PathPattern` is the recommended solution for web applications and it is the only choice in -Spring WebFlux. Prior to version 5.3, `AntPathMatcher` was the only choice in Spring MVC -and continues to be the default. However `PathPattern` can be enabled in the -<>. +Spring WebFlux. It was enabled for use in Spring MVC from version 5.3 and is enabled by +default from version 6.0. See <> for +customizations of path matching options. -`PathPattern` supports the same pattern syntax as `AntPathMatcher`. In addition it also +`PathPattern` supports the same pattern syntax as `AntPathMatcher`. In addition, it also supports the capturing pattern, e.g. `+{*spring}+`, for matching 0 or more path segments at the end of a path. `PathPattern` also restricts the use of `+**+` for matching multiple path segments such that it's only allowed at the end of a pattern. This eliminates many @@ -5997,9 +5999,7 @@ The following example shows how to customize path matching in Java configuration @Override public void configurePathMatch(PathMatchConfigurer configurer) { - configurer - .setPatternParser(new PathPatternParser()) - .addPathPrefix("/api", HandlerTypePredicate.forAnnotation(RestController.class)); + configurer.addPathPrefix("/api", HandlerTypePredicate.forAnnotation(RestController.class)); } private PathPatternParser patternParser() { @@ -6015,9 +6015,7 @@ The following example shows how to customize path matching in Java configuration class WebConfig : WebMvcConfigurer { override fun configurePathMatch(configurer: PathMatchConfigurer) { - configurer - .setPatternParser(patternParser) - .addPathPrefix("/api", HandlerTypePredicate.forAnnotation(RestController::class.java)) + configurer.addPathPrefix("/api", HandlerTypePredicate.forAnnotation(RestController::class.java)) } fun patternParser(): PathPatternParser { @@ -6026,7 +6024,7 @@ The following example shows how to customize path matching in Java configuration } ---- -The following example shows how to achieve the same configuration in XML: +The following example shows how to customize path matching in XML configuration: [source,xml,indent=0,subs="verbatim,quotes"] ----