diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/MvcUriComponentsBuilder.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/MvcUriComponentsBuilder.java index eaa3085b62..253905b5cb 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/MvcUriComponentsBuilder.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/MvcUriComponentsBuilder.java @@ -174,8 +174,15 @@ public class MvcUriComponentsBuilder { Class controllerType) { builder = getBaseUrlToUse(builder); + + // Externally configured prefix via PathConfigurer.. + String prefix = getPathPrefix(controllerType); + builder.path(prefix); + String mapping = getClassMapping(controllerType); - return builder.path(mapping); + builder.path(mapping); + + return builder; } /** @@ -526,15 +533,21 @@ public class MvcUriComponentsBuilder { } - private static UriComponentsBuilder fromMethodInternal(@Nullable UriComponentsBuilder baseUrl, + private static UriComponentsBuilder fromMethodInternal(@Nullable UriComponentsBuilder builder, Class controllerType, Method method, Object... args) { - baseUrl = getBaseUrlToUse(baseUrl); + builder = getBaseUrlToUse(builder); + + // Externally configured prefix via PathConfigurer.. + String prefix = getPathPrefix(controllerType); + builder.path(prefix); + String typePath = getClassMapping(controllerType); String methodPath = getMethodMapping(method); String path = pathMatcher.combine(typePath, methodPath); - baseUrl.path(path); - UriComponents uriComponents = applyContributors(baseUrl, method, args); + builder.path(path); + + UriComponents uriComponents = applyContributors(builder, method, args); return UriComponentsBuilder.newInstance().uriComponents(uriComponents); } @@ -544,6 +557,22 @@ public class MvcUriComponentsBuilder { baseUrl.cloneBuilder(); } + private static String getPathPrefix(Class controllerType) { + WebApplicationContext wac = getWebApplicationContext(); + if (wac != null) { + Map map = wac.getBeansOfType(RequestMappingHandlerMapping.class); + for (RequestMappingHandlerMapping mapping : map.values()) { + if (mapping.isHandler(controllerType)) { + String prefix = mapping.getPathPrefix(controllerType); + if (prefix != null) { + return prefix; + } + } + } + } + return ""; + } + private static String getClassMapping(Class controllerType) { Assert.notNull(controllerType, "'controllerType' must not be null"); RequestMapping mapping = AnnotatedElementUtils.findMergedAnnotation(controllerType, RequestMapping.class); 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 48007022b8..4d5f4d03ee 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 @@ -32,7 +32,6 @@ import org.springframework.lang.Nullable; import org.springframework.stereotype.Controller; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; -import org.springframework.util.StringUtils; import org.springframework.util.StringValueResolver; import org.springframework.web.accept.ContentNegotiationManager; import org.springframework.web.bind.annotation.CrossOrigin; @@ -67,7 +66,7 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi private boolean useTrailingSlashMatch = true; - private final Map>> pathPrefixes = new LinkedHashMap<>(); + private Map>> pathPrefixes = new LinkedHashMap<>(); private ContentNegotiationManager contentNegotiationManager = new ContentNegotiationManager(); @@ -120,10 +119,7 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi * @since 5.1 */ public void setPathPrefixes(Map>> prefixes) { - this.pathPrefixes.clear(); - prefixes.entrySet().stream() - .filter(entry -> StringUtils.hasText(entry.getKey())) - .forEach(entry -> this.pathPrefixes.put(entry.getKey(), entry.getValue())); + this.pathPrefixes = Collections.unmodifiableMap(new LinkedHashMap<>(prefixes)); } /** @@ -180,7 +176,7 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi * @since 5.1 */ public Map>> getPathPrefixes() { - return Collections.unmodifiableMap(this.pathPrefixes); + return this.pathPrefixes; } /** @@ -227,20 +223,28 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi if (typeInfo != null) { info = typeInfo.combine(info); } - for (Map.Entry>> entry : this.pathPrefixes.entrySet()) { - if (entry.getValue().test(handlerType)) { - String prefix = entry.getKey(); - if (this.embeddedValueResolver != null) { - prefix = this.embeddedValueResolver.resolveStringValue(prefix); - } - info = RequestMappingInfo.paths(prefix).build().combine(info); - break; - } + String prefix = getPathPrefix(handlerType); + if (prefix != null) { + info = RequestMappingInfo.paths(prefix).build().combine(info); } } return info; } + @Nullable + String getPathPrefix(Class handlerType) { + for (Map.Entry>> entry : this.pathPrefixes.entrySet()) { + if (entry.getValue().test(handlerType)) { + String prefix = entry.getKey(); + if (this.embeddedValueResolver != null) { + prefix = this.embeddedValueResolver.resolveStringValue(prefix); + } + return prefix; + } + } + return null; + } + /** * Delegates to {@link #createRequestMappingInfo(RequestMapping, RequestCondition)}, * supplying the appropriate custom {@link RequestCondition} depending on whether diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/MvcUriComponentsBuilderTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/MvcUriComponentsBuilderTests.java index 205aeca886..f5bd47339c 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/MvcUriComponentsBuilderTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/MvcUriComponentsBuilderTests.java @@ -56,6 +56,7 @@ import org.springframework.web.filter.ForwardedHeaderFilter; import org.springframework.web.servlet.DispatcherServlet; import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.config.annotation.EnableWebMvc; +import org.springframework.web.servlet.config.annotation.PathMatchConfigurer; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import org.springframework.web.util.UriComponents; import org.springframework.web.util.UriComponentsBuilder; @@ -378,12 +379,9 @@ public class MvcUriComponentsBuilderTests { @Test public void fromMappingNamePlain() { - AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext(); - context.setServletContext(new MockServletContext()); - context.register(WebConfig.class); - context.refresh(); - this.request.setAttribute(DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE, context); + initWebApplicationContext(WebConfig.class); + this.request.setServerName("example.org"); this.request.setServerPort(9999); this.request.setContextPath("/base"); @@ -395,12 +393,8 @@ public class MvcUriComponentsBuilderTests { @Test public void fromMappingNameWithCustomBaseUrl() { - AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext(); - context.setServletContext(new MockServletContext()); - context.register(WebConfig.class); - context.refresh(); - this.request.setAttribute(DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE, context); + initWebApplicationContext(WebConfig.class); UriComponentsBuilder baseUrl = UriComponentsBuilder.fromUriString("http://example.org:9999/base"); MvcUriComponentsBuilder mvcBuilder = relativeTo(baseUrl); @@ -408,6 +402,41 @@ public class MvcUriComponentsBuilderTests { assertEquals("http://example.org:9999/base/people/123/addresses/DE", url); } + @Test + public void fromControllerWithPrefix() { + + initWebApplicationContext(PathPrefixWebConfig.class); + + this.request.setServerName("example.org"); + this.request.setServerPort(9999); + this.request.setContextPath("/base"); + + assertEquals("http://example.org:9999/base/api/people/123/addresses", + fromController(PersonsAddressesController.class).buildAndExpand("123").toString()); + } + + @Test + public void fromMethodWithPrefix() { + + initWebApplicationContext(PathPrefixWebConfig.class); + + this.request.setServerName("example.org"); + this.request.setServerPort(9999); + this.request.setContextPath("/base"); + + assertEquals("http://example.org:9999/base/api/people/123/addresses/DE", + fromMethodCall(on(PersonsAddressesController.class).getAddressesForCountry("DE")) + .buildAndExpand("123").toString()); + } + + private void initWebApplicationContext(Class configClass) { + AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext(); + context.setServletContext(new MockServletContext()); + context.register(configClass); + context.refresh(); + this.request.setAttribute(DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE, context); + } + static class Person { @@ -555,6 +584,21 @@ public class MvcUriComponentsBuilderTests { } + @EnableWebMvc + static class PathPrefixWebConfig implements WebMvcConfigurer { + + @Override + public void configurePathMatch(PathMatchConfigurer configurer) { + configurer.addPathPrefix("/api", PersonsAddressesController.class::equals); + } + + @Bean + public PersonsAddressesController controller() { + return new PersonsAddressesController(); + } + } + + @Controller @RequestMapping("/hotels/{hotel}") static class BookingControllerWithModelAndView {