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 b48e06b26241..1ec70aa2947e 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
@@ -401,16 +401,13 @@ private void configurePathMatchingProperties(
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);
@@ -435,12 +432,20 @@ private void configurePathMatchingProperties(
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);
}
}
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/MvcNamespaceUtils.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/MvcNamespaceUtils.java
index 9e7d7d681aee..e68be743bd88 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/MvcNamespaceUtils.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/MvcNamespaceUtils.java
@@ -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.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.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 @@ else if (!context.getRegistry().isAlias(URL_PATH_HELPER_BEAN_NAME) &&
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 static RuntimeBeanReference registerPathMatcher(@Nullable RuntimeBeanRefe
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 @@ else if (!context.getRegistry().isAlias(PATH_MATCHER_BEAN_NAME) &&
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 @@ private static void registerBeanNameUrlHandlerMapping(ParserContext context, @Nu
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));
}
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/ResourcesBeanDefinitionParser.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/ResourcesBeanDefinitionParser.java
index 8f85ba6efe49..62338d80520a 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/ResourcesBeanDefinitionParser.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/ResourcesBeanDefinitionParser.java
@@ -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 @@ public BeanDefinition parse(Element element, ParserContext context) {
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 @@ public BeanDefinition parse(Element element, ParserContext context) {
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
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/ViewControllerBeanDefinitionParser.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/ViewControllerBeanDefinitionParser.java
index 8a39ed1a2832..5964af08ad11 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/ViewControllerBeanDefinitionParser.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/ViewControllerBeanDefinitionParser.java
@@ -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 @@ private BeanDefinition registerHandlerMapping(ParserContext context, @Nullable O
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);
diff --git a/spring-webmvc/src/main/resources/org/springframework/web/servlet/config/spring-mvc.xsd b/spring-webmvc/src/main/resources/org/springframework/web/servlet/config/spring-mvc.xsd
index 6a674fcf1794..1131f9bc90bd 100644
--- a/spring-webmvc/src/main/resources/org/springframework/web/servlet/config/spring-mvc.xsd
+++ b/spring-webmvc/src/main/resources/org/springframework/web/servlet/config/spring-mvc.xsd
@@ -89,7 +89,14 @@
+
+
+
+
+
diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/AnnotationDrivenBeanDefinitionParserTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/AnnotationDrivenBeanDefinitionParserTests.java
index 42de3720f386..4325cb39633c 100644
--- a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/AnnotationDrivenBeanDefinitionParserTests.java
+++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/AnnotationDrivenBeanDefinitionParserTests.java
@@ -89,6 +89,7 @@ public void testPathMatchingConfiguration() {
assertThat(hm.useRegisteredSuffixPatternMatch()).isTrue();
assertThat(hm.getUrlPathHelper()).isInstanceOf(TestPathHelper.class);
assertThat(hm.getPathMatcher()).isInstanceOf(TestPathMatcher.class);
+ assertThat(hm.getPatternParser()).isNull();
List fileExtensions = hm.getContentNegotiationManager().getAllFileExtensions();
assertThat(fileExtensions).containsExactly("xml");
}
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 eb9cba86ba5a..011b4808c96e 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
@@ -64,6 +64,7 @@
import org.springframework.lang.Nullable;
import org.springframework.scheduling.concurrent.ConcurrentTaskExecutor;
import org.springframework.stereotype.Controller;
+import org.springframework.util.AntPathMatcher;
import org.springframework.util.PathMatcher;
import org.springframework.validation.BindingResult;
import org.springframework.validation.Errors;
@@ -145,6 +146,7 @@
import org.springframework.web.testfixture.servlet.MockRequestDispatcher;
import org.springframework.web.testfixture.servlet.MockServletContext;
import org.springframework.web.util.UrlPathHelper;
+import org.springframework.web.util.pattern.PathPatternParser;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
@@ -202,6 +204,8 @@ void testDefaultConfig() throws Exception {
assertThat(mapping).isNotNull();
assertThat(mapping.getOrder()).isEqualTo(0);
assertThat(mapping.getUrlPathHelper().shouldRemoveSemicolonContent()).isTrue();
+ assertThat(mapping.getPathMatcher()).isEqualTo(appContext.getBean("mvcPathMatcher"));
+ assertThat(mapping.getPatternParser()).isNotNull();
mapping.setDefaultHandler(handlerMethod);
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo.json");
@@ -392,6 +396,8 @@ void testResources() throws Exception {
SimpleUrlHandlerMapping resourceMapping = appContext.getBean(SimpleUrlHandlerMapping.class);
assertThat(resourceMapping).isNotNull();
assertThat(resourceMapping.getOrder()).isEqualTo(Ordered.LOWEST_PRECEDENCE - 1);
+ assertThat(resourceMapping.getPathMatcher()).isNotNull();
+ assertThat(resourceMapping.getPatternParser()).isNotNull();
BeanNameUrlHandlerMapping beanNameMapping = appContext.getBean(BeanNameUrlHandlerMapping.class);
assertThat(beanNameMapping).isNotNull();
@@ -423,6 +429,31 @@ void testResources() throws Exception {
.isInstanceOf(NoResourceFoundException.class);
}
+ @Test
+ void testUseDeprecatedPathMatcher() throws Exception {
+ loadBeanDefinitions("mvc-config-deprecated-path-matcher.xml");
+ Map handlerMappings = appContext.getBeansOfType(AbstractHandlerMapping.class);
+ AntPathMatcher mvcPathMatcher = appContext.getBean("pathMatcher", AntPathMatcher.class);
+ assertThat(handlerMappings).hasSize(4);
+ handlerMappings.forEach((name, hm) -> {
+ assertThat(hm.getPathMatcher()).as("path matcher for %s", name).isEqualTo(mvcPathMatcher);
+ assertThat(hm.getPatternParser()).as("pattern parser for %s", name).isNull();
+ });
+ }
+
+ @Test
+ void testUsePathPatternParser() throws Exception {
+ loadBeanDefinitions("mvc-config-custom-pattern-parser.xml");
+
+ PathPatternParser patternParser = appContext.getBean("patternParser", PathPatternParser.class);
+ Map handlerMappings = appContext.getBeansOfType(AbstractHandlerMapping.class);
+ assertThat(handlerMappings).hasSize(4);
+ handlerMappings.forEach((name, hm) -> {
+ assertThat(hm.getPathMatcher()).as("path matcher for %s", name).isNotNull();
+ assertThat(hm.getPatternParser()).as("pattern parser for %s", name).isEqualTo(patternParser);
+ });
+ }
+
@Test
void testResourcesWithOptionalAttributes() {
loadBeanDefinitions("mvc-config-resources-optional-attrs.xml");
@@ -600,6 +631,9 @@ void testViewControllers() throws Exception {
assertThat(beanNameMapping).isNotNull();
assertThat(beanNameMapping.getOrder()).isEqualTo(2);
+ assertThat(beanNameMapping.getPathMatcher()).isNotNull();
+ assertThat(beanNameMapping.getPatternParser()).isNotNull();
+
MockHttpServletRequest request = new MockHttpServletRequest();
request.setMethod("GET");
@@ -895,11 +929,11 @@ void testPathMatchingHandlerMappings() {
assertThat(viewController.getUrlPathHelper().getClass()).isEqualTo(TestPathHelper.class);
assertThat(viewController.getPathMatcher().getClass()).isEqualTo(TestPathMatcher.class);
- for (SimpleUrlHandlerMapping handlerMapping : appContext.getBeansOfType(SimpleUrlHandlerMapping.class).values()) {
+ appContext.getBeansOfType(SimpleUrlHandlerMapping.class).forEach((name, handlerMapping) -> {
assertThat(handlerMapping).isNotNull();
- assertThat(handlerMapping.getUrlPathHelper().getClass()).isEqualTo(TestPathHelper.class);
- assertThat(handlerMapping.getPathMatcher().getClass()).isEqualTo(TestPathMatcher.class);
- }
+ assertThat(handlerMapping.getUrlPathHelper().getClass()).as("path helper for %s", name).isEqualTo(TestPathHelper.class);
+ assertThat(handlerMapping.getPathMatcher().getClass()).as("path matcher for %s", name).isEqualTo(TestPathMatcher.class);
+ });
}
@Test
@@ -909,7 +943,7 @@ void testCorsMinimal() {
String[] beanNames = appContext.getBeanNamesForType(AbstractHandlerMapping.class);
assertThat(beanNames).hasSize(2);
for (String beanName : beanNames) {
- AbstractHandlerMapping handlerMapping = (AbstractHandlerMapping)appContext.getBean(beanName);
+ AbstractHandlerMapping handlerMapping = (AbstractHandlerMapping) appContext.getBean(beanName);
assertThat(handlerMapping).isNotNull();
DirectFieldAccessor accessor = new DirectFieldAccessor(handlerMapping);
Map configs = ((UrlBasedCorsConfigurationSource) accessor
@@ -934,7 +968,7 @@ void testCors() {
String[] beanNames = appContext.getBeanNamesForType(AbstractHandlerMapping.class);
assertThat(beanNames).hasSize(2);
for (String beanName : beanNames) {
- AbstractHandlerMapping handlerMapping = (AbstractHandlerMapping)appContext.getBean(beanName);
+ AbstractHandlerMapping handlerMapping = (AbstractHandlerMapping) appContext.getBean(beanName);
assertThat(handlerMapping).isNotNull();
DirectFieldAccessor accessor = new DirectFieldAccessor(handlerMapping);
Map configs = ((UrlBasedCorsConfigurationSource) accessor
diff --git a/spring-webmvc/src/test/resources/org/springframework/web/servlet/config/mvc-config-custom-pattern-parser.xml b/spring-webmvc/src/test/resources/org/springframework/web/servlet/config/mvc-config-custom-pattern-parser.xml
new file mode 100644
index 000000000000..4e3e5bec51d9
--- /dev/null
+++ b/spring-webmvc/src/test/resources/org/springframework/web/servlet/config/mvc-config-custom-pattern-parser.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/spring-webmvc/src/test/resources/org/springframework/web/servlet/config/mvc-config-deprecated-path-matcher.xml b/spring-webmvc/src/test/resources/org/springframework/web/servlet/config/mvc-config-deprecated-path-matcher.xml
new file mode 100644
index 000000000000..78afbe8cb6f8
--- /dev/null
+++ b/spring-webmvc/src/test/resources/org/springframework/web/servlet/config/mvc-config-deprecated-path-matcher.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+