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 @@ + + + + + + + + + + + + +