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

@@ -89,6 +89,7 @@ class AnnotationDrivenBeanDefinitionParserTests {
assertThat(hm.useRegisteredSuffixPatternMatch()).isTrue();
assertThat(hm.getUrlPathHelper()).isInstanceOf(TestPathHelper.class);
assertThat(hm.getPathMatcher()).isInstanceOf(TestPathMatcher.class);
assertThat(hm.getPatternParser()).isNull();
List<String> fileExtensions = hm.getContentNegotiationManager().getAllFileExtensions();
assertThat(fileExtensions).containsExactly("xml");
}

View File

@@ -64,6 +64,7 @@ import org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConve
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.MockHttpServletResponse;
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 @@ public class MvcNamespaceTests {
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 @@ public class MvcNamespaceTests {
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 @@ public class MvcNamespaceTests {
.isInstanceOf(NoResourceFoundException.class);
}
@Test
void testUseDeprecatedPathMatcher() throws Exception {
loadBeanDefinitions("mvc-config-deprecated-path-matcher.xml");
Map<String, AbstractHandlerMapping> 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<String, AbstractHandlerMapping> 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");
@@ -601,6 +632,9 @@ public class MvcNamespaceTests {
assertThat(beanNameMapping).isNotNull();
assertThat(beanNameMapping.getOrder()).isEqualTo(2);
assertThat(beanNameMapping.getPathMatcher()).isNotNull();
assertThat(beanNameMapping.getPatternParser()).isNotNull();
MockHttpServletRequest request = new MockHttpServletRequest();
request.setMethod("GET");
@@ -896,11 +930,11 @@ public class MvcNamespaceTests {
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
@@ -910,7 +944,7 @@ public class MvcNamespaceTests {
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<String, CorsConfiguration> configs = ((UrlBasedCorsConfigurationSource) accessor
@@ -935,7 +969,7 @@ public class MvcNamespaceTests {
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<String, CorsConfiguration> configs = ((UrlBasedCorsConfigurationSource) accessor

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd">
<bean id="patternParser" class="org.springframework.web.util.pattern.PathPatternParser"/>
<mvc:annotation-driven>
<mvc:path-matching pattern-parser="patternParser"/>
</mvc:annotation-driven>
<mvc:view-controller path="/foo"/>
<mvc:resources mapping="/resources/**" location="/, classpath:/META-INF/"/>
</beans>

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd">
<bean id="pathMatcher" class="org.springframework.util.AntPathMatcher"/>
<mvc:annotation-driven>
<mvc:path-matching path-matcher="pathMatcher"/>
</mvc:annotation-driven>
<mvc:view-controller path="/foo"/>
<mvc:resources mapping="/resources/**" location="/, classpath:/META-INF/"/>
</beans>