Improve PathMatcher/PatternParser XML configuration

Prior to this commit, the MVC namespace for the XML Spring configuration
model would use the `PathMatcher` bean instance when provided like this:

```
<bean id="pathMatcher" class="org.springframework.util.AntPathMatcher"/>
<mvc:annotation-driven>
  <mvc:path-matching path-matcher="pathMatcher"/>
</mvc:annotation-driven>
<mvc:resources mapping="/resources/**" location="classpath:/static/"/>
```

With this configuration, the handler mapping for annotated controller
would use the given `AntPathMatcher` instance but the handler mapping
for resources would still use the default, which is `PathPatternParser`
since 6.0.

This commit ensures that when a custom `path-matcher` is defined, it's
consistently used for all MVC handler mappings as an alias to the
well-known bean name. This allows to use `AntPathMatcher` consistently
while working on a migration path to `PathPatternParser`

This commit also adds a new XML attribute to the path matching
configuration that makes it possible to use a custom `PathPatternParser`
instance:

```
<bean id="patternParser" class="org.springframework.web.util.pattern.PathPatternParser"/>
<mvc:annotation-driven>
  <mvc:path-matching pattern-parser="patternParser"/>
</mvc:annotation-driven>
```

Closes gh-34064
This commit is contained in:
Brian Clozel
2024-12-17 10:13:56 +01:00
parent b0fcde9aa3
commit 1a34b0dd87
9 changed files with 176 additions and 21 deletions

View File

@@ -407,16 +407,13 @@ class AnnotationDrivenBeanDefinitionParser implements BeanDefinitionParser {
RootBeanDefinition handlerMappingDef, Element element, ParserContext context) {
Element pathMatchingElement = DomUtils.getChildElementByTagName(element, "path-matching");
Object source = context.extractSource(element);
if (pathMatchingElement != null) {
Object source = context.extractSource(element);
if (pathMatchingElement.hasAttribute("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);
@@ -441,12 +438,20 @@ class AnnotationDrivenBeanDefinitionParser implements BeanDefinitionParser {
pathMatcherRef = new RuntimeBeanReference(pathMatchingElement.getAttribute("path-matcher"));
preferPathMatcher = true;
}
pathMatcherRef = MvcNamespaceUtils.registerPathMatcher(pathMatcherRef, context, source);
handlerMappingDef.getPropertyValues().add("pathMatcher", pathMatcherRef);
if (preferPathMatcher) {
pathMatcherRef = MvcNamespaceUtils.registerPathMatcher(pathMatcherRef, context, source);
handlerMappingDef.getPropertyValues().add("pathMatcher", pathMatcherRef);
handlerMappingDef.getPropertyValues().add("patternParser", null);
}
else if (pathMatchingElement.hasAttribute("pattern-parser")) {
RuntimeBeanReference patternParserRef = new RuntimeBeanReference(pathMatchingElement.getAttribute("pattern-parser"));
patternParserRef = MvcNamespaceUtils.registerPatternParser(patternParserRef, context, source);
handlerMappingDef.getPropertyValues().add("patternParser", patternParserRef);
}
}
else {
RuntimeBeanReference pathMatcherRef = MvcNamespaceUtils.registerPathMatcher(null, context, source);
handlerMappingDef.getPropertyValues().add("pathMatcher", pathMatcherRef);
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 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.
@@ -28,9 +28,11 @@ import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.beans.factory.xml.ParserContext;
import org.springframework.lang.Nullable;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.Assert;
import org.springframework.util.PathMatcher;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.handler.AbstractHandlerMapping;
import org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping;
import org.springframework.web.servlet.handler.HandlerMappingIntrospector;
import org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver;
@@ -39,6 +41,7 @@ import org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter;
import org.springframework.web.servlet.support.SessionFlashMapManager;
import org.springframework.web.servlet.view.DefaultRequestToViewNameTranslator;
import org.springframework.web.util.UrlPathHelper;
import org.springframework.web.util.pattern.PathPatternParser;
/**
* Convenience methods for use in MVC namespace BeanDefinitionParsers.
@@ -64,6 +67,8 @@ public abstract class MvcNamespaceUtils {
private static final String PATH_MATCHER_BEAN_NAME = "mvcPathMatcher";
private static final String PATTERN_PARSER_BEAN_NAME = "mvcPatternParser";
private static final String CORS_CONFIGURATION_BEAN_NAME = "mvcCorsConfigurations";
private static final String HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME = "mvcHandlerMappingIntrospector";
@@ -105,6 +110,18 @@ public abstract class MvcNamespaceUtils {
return new RuntimeBeanReference(URL_PATH_HELPER_BEAN_NAME);
}
/**
* Return the {@link PathMatcher} bean definition if it has been registered
* in the context as an alias with its well-known name, or {@code null}.
*/
@Nullable
static RuntimeBeanReference getCustomPathMatcher(ParserContext context) {
if(context.getRegistry().isAlias(PATH_MATCHER_BEAN_NAME)) {
return new RuntimeBeanReference(PATH_MATCHER_BEAN_NAME);
}
return null;
}
/**
* Adds an alias to an existing well-known name or registers a new instance of a {@link PathMatcher}
* under that well-known name, unless already registered.
@@ -117,6 +134,9 @@ public abstract class MvcNamespaceUtils {
if (context.getRegistry().isAlias(PATH_MATCHER_BEAN_NAME)) {
context.getRegistry().removeAlias(PATH_MATCHER_BEAN_NAME);
}
if (context.getRegistry().containsBeanDefinition(PATH_MATCHER_BEAN_NAME)) {
context.getRegistry().removeBeanDefinition(PATH_MATCHER_BEAN_NAME);
}
context.getRegistry().registerAlias(pathMatcherRef.getBeanName(), PATH_MATCHER_BEAN_NAME);
}
else if (!context.getRegistry().isAlias(PATH_MATCHER_BEAN_NAME) &&
@@ -130,6 +150,60 @@ public abstract class MvcNamespaceUtils {
return new RuntimeBeanReference(PATH_MATCHER_BEAN_NAME);
}
/**
* Return the {@link PathPatternParser} bean definition if it has been registered
* in the context as an alias with its well-known name, or {@code null}.
*/
@Nullable
static RuntimeBeanReference getCustomPatternParser(ParserContext context) {
if (context.getRegistry().isAlias(PATTERN_PARSER_BEAN_NAME)) {
return new RuntimeBeanReference(PATTERN_PARSER_BEAN_NAME);
}
return null;
}
/**
* Adds an alias to an existing well-known name or registers a new instance of a {@link PathPatternParser}
* under that well-known name, unless already registered.
* @return a RuntimeBeanReference to this {@link PathPatternParser} instance
*/
public static RuntimeBeanReference registerPatternParser(@Nullable RuntimeBeanReference patternParserRef,
ParserContext context, @Nullable Object source) {
if (patternParserRef != null) {
if (context.getRegistry().isAlias(PATTERN_PARSER_BEAN_NAME)) {
context.getRegistry().removeAlias(PATTERN_PARSER_BEAN_NAME);
}
context.getRegistry().registerAlias(patternParserRef.getBeanName(), PATTERN_PARSER_BEAN_NAME);
}
else if (!context.getRegistry().isAlias(PATTERN_PARSER_BEAN_NAME) &&
!context.getRegistry().containsBeanDefinition(PATTERN_PARSER_BEAN_NAME)) {
RootBeanDefinition pathMatcherDef = new RootBeanDefinition(PathPatternParser.class);
pathMatcherDef.setSource(source);
pathMatcherDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
context.getRegistry().registerBeanDefinition(PATTERN_PARSER_BEAN_NAME, pathMatcherDef);
context.registerComponent(new BeanComponentDefinition(pathMatcherDef, PATTERN_PARSER_BEAN_NAME));
}
return new RuntimeBeanReference(PATTERN_PARSER_BEAN_NAME);
}
static void configurePathMatching(RootBeanDefinition handlerMappingDef, ParserContext context, @Nullable Object source) {
Assert.isTrue(AbstractHandlerMapping.class.isAssignableFrom(handlerMappingDef.getBeanClass()),
() -> "Handler mapping type [" + handlerMappingDef.getTargetType() + "] not supported");
RuntimeBeanReference customPathMatcherRef = MvcNamespaceUtils.getCustomPathMatcher(context);
RuntimeBeanReference customPatternParserRef = MvcNamespaceUtils.getCustomPatternParser(context);
if (customPathMatcherRef != null) {
handlerMappingDef.getPropertyValues().add("pathMatcher", customPathMatcherRef)
.add("patternParser", null);
}
else if (customPatternParserRef != null) {
handlerMappingDef.getPropertyValues().add("patternParser", customPatternParserRef);
}
else {
handlerMappingDef.getPropertyValues().add("pathMatcher",
MvcNamespaceUtils.registerPathMatcher(null, context, source));
}
}
/**
* Registers an {@link HttpRequestHandlerAdapter} under a well-known
* name unless already registered.
@@ -142,6 +216,7 @@ public abstract class MvcNamespaceUtils {
mappingDef.getPropertyValues().add("order", 2); // consistent with WebMvcConfigurationSupport
RuntimeBeanReference corsRef = MvcNamespaceUtils.registerCorsConfigurations(null, context, source);
mappingDef.getPropertyValues().add("corsConfigurations", corsRef);
configurePathMatching(mappingDef, context, source);
context.getRegistry().registerBeanDefinition(BEAN_NAME_URL_HANDLER_MAPPING_BEAN_NAME, mappingDef);
context.registerComponent(new BeanComponentDefinition(mappingDef, BEAN_NAME_URL_HANDLER_MAPPING_BEAN_NAME));
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2024 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.
@@ -92,7 +92,6 @@ class ResourcesBeanDefinitionParser implements BeanDefinitionParser {
registerUrlProvider(context, source);
RuntimeBeanReference pathMatcherRef = MvcNamespaceUtils.registerPathMatcher(null, context, source);
RuntimeBeanReference pathHelperRef = MvcNamespaceUtils.registerUrlPathHelper(null, context, source);
String resourceHandlerName = registerResourceHandler(context, element, pathHelperRef, source);
@@ -111,8 +110,8 @@ class ResourcesBeanDefinitionParser implements BeanDefinitionParser {
RootBeanDefinition handlerMappingDef = new RootBeanDefinition(SimpleUrlHandlerMapping.class);
handlerMappingDef.setSource(source);
handlerMappingDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
handlerMappingDef.getPropertyValues().add("urlMap", urlMap);
handlerMappingDef.getPropertyValues().add("pathMatcher", pathMatcherRef).add("urlPathHelper", pathHelperRef);
handlerMappingDef.getPropertyValues().add("urlMap", urlMap).add("urlPathHelper", pathHelperRef);
MvcNamespaceUtils.configurePathMatching(handlerMappingDef, context, source);
String orderValue = element.getAttribute("order");
// Use a default of near-lowest precedence, still allowing for even lower precedence in other mappings

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2024 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.
@@ -123,7 +123,7 @@ class ViewControllerBeanDefinitionParser implements BeanDefinitionParser {
beanDef.setSource(source);
beanDef.getPropertyValues().add("order", "1");
beanDef.getPropertyValues().add("pathMatcher", MvcNamespaceUtils.registerPathMatcher(null, context, source));
MvcNamespaceUtils.configurePathMatching(beanDef, context, source);
beanDef.getPropertyValues().add("urlPathHelper", MvcNamespaceUtils.registerUrlPathHelper(null, context, source));
RuntimeBeanReference corsConfigurationsRef = MvcNamespaceUtils.registerCorsConfigurations(null, context, source);
beanDef.getPropertyValues().add("corsConfigurations", corsConfigurationsRef);

View File

@@ -89,7 +89,14 @@
<xsd:annotation>
<xsd:documentation><![CDATA[
The bean name of the PathMatcher implementation to use for matching URL paths against registered URL patterns.
Default is AntPathMatcher.
If no bean is provided, the PathPatternParser will be used instead.
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="pattern-parser" type="xsd:string">
<xsd:annotation>
<xsd:documentation><![CDATA[
The bean name of the PathPatternParser instance to use for matching URL paths against registered URL patterns.
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>