Path prefixes for groups of controllers

Issue: SPR-16336
This commit is contained in:
Rossen Stoyanchev 2018-06-07 18:16:18 -04:00
parent 31159a8506
commit 19dc981685
12 changed files with 313 additions and 33 deletions

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2017 the original author or authors. * Copyright 2002-2018 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -16,7 +16,11 @@
package org.springframework.web.reactive.config; package org.springframework.web.reactive.config;
import java.util.LinkedHashMap;
import java.util.Map;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
import org.springframework.web.method.HandlerTypePredicate;
/** /**
* Assist with configuring {@code HandlerMapping}'s with path matching options. * Assist with configuring {@code HandlerMapping}'s with path matching options.
@ -34,6 +38,9 @@ public class PathMatchConfigurer {
@Nullable @Nullable
private Boolean caseSensitiveMatch; private Boolean caseSensitiveMatch;
@Nullable
private Map<String, HandlerTypePredicate> pathPrefixes;
/** /**
* Whether to match to URLs irrespective of their case. * Whether to match to URLs irrespective of their case.
@ -55,6 +62,22 @@ public class PathMatchConfigurer {
return this; return this;
} }
/**
* Configure a path prefix to apply to matching controller methods.
* <p>Prefixes are used to enrich the mappings of every {@code @RequestMapping}
* method whose controller type is matched by the corresponding
* {@link HandlerTypePredicate}. The prefix for the first matching predicate
* is used.
* @param prefix the path prefix to apply
* @param predicate a predicate for matching controller types
* @since 5.1
*/
public PathMatchConfigurer addPathPrefix(String prefix, HandlerTypePredicate predicate) {
this.pathPrefixes = this.pathPrefixes == null ? new LinkedHashMap<>() : this.pathPrefixes;
this.pathPrefixes.put(prefix, predicate);
return this;
}
@Nullable @Nullable
protected Boolean isUseTrailingSlashMatch() { protected Boolean isUseTrailingSlashMatch() {
return this.trailingSlashMatch; return this.trailingSlashMatch;
@ -65,4 +88,8 @@ public class PathMatchConfigurer {
return this.caseSensitiveMatch; return this.caseSensitiveMatch;
} }
@Nullable
protected Map<String, HandlerTypePredicate> getPathPrefixes() {
return this.pathPrefixes;
}
} }

View File

@ -44,6 +44,7 @@ import org.springframework.validation.Validator;
import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.support.ConfigurableWebBindingInitializer; import org.springframework.web.bind.support.ConfigurableWebBindingInitializer;
import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.method.HandlerTypePredicate;
import org.springframework.web.reactive.DispatcherHandler; import org.springframework.web.reactive.DispatcherHandler;
import org.springframework.web.reactive.HandlerMapping; import org.springframework.web.reactive.HandlerMapping;
import org.springframework.web.reactive.accept.RequestedContentTypeResolver; import org.springframework.web.reactive.accept.RequestedContentTypeResolver;
@ -119,14 +120,22 @@ public class WebFluxConfigurationSupport implements ApplicationContextAware {
mapping.setCorsConfigurations(getCorsConfigurations()); mapping.setCorsConfigurations(getCorsConfigurations());
PathMatchConfigurer configurer = getPathMatchConfigurer(); PathMatchConfigurer configurer = getPathMatchConfigurer();
Boolean useTrailingSlashMatch = configurer.isUseTrailingSlashMatch(); Boolean useTrailingSlashMatch = configurer.isUseTrailingSlashMatch();
Boolean useCaseSensitiveMatch = configurer.isUseCaseSensitiveMatch();
if (useTrailingSlashMatch != null) { if (useTrailingSlashMatch != null) {
mapping.setUseTrailingSlashMatch(useTrailingSlashMatch); mapping.setUseTrailingSlashMatch(useTrailingSlashMatch);
} }
Boolean useCaseSensitiveMatch = configurer.isUseCaseSensitiveMatch();
if (useCaseSensitiveMatch != null) { if (useCaseSensitiveMatch != null) {
mapping.setUseCaseSensitiveMatch(useCaseSensitiveMatch); mapping.setUseCaseSensitiveMatch(useCaseSensitiveMatch);
} }
Map<String, HandlerTypePredicate> pathPrefixes = configurer.getPathPrefixes();
if (pathPrefixes != null) {
mapping.setPathPrefixes(pathPrefixes);
}
return mapping; return mapping;
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2017 the original author or authors. * Copyright 2002-2018 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -18,6 +18,9 @@ package org.springframework.web.reactive.result.method.annotation;
import java.lang.reflect.AnnotatedElement; import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import org.springframework.context.EmbeddedValueResolverAware; import org.springframework.context.EmbeddedValueResolverAware;
import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.core.annotation.AnnotatedElementUtils;
@ -25,11 +28,13 @@ import org.springframework.lang.Nullable;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils; import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.util.StringValueResolver; import org.springframework.util.StringValueResolver;
import org.springframework.web.bind.annotation.CrossOrigin; import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.method.HandlerTypePredicate;
import org.springframework.web.method.HandlerMethod; import org.springframework.web.method.HandlerMethod;
import org.springframework.web.reactive.accept.RequestedContentTypeResolver; import org.springframework.web.reactive.accept.RequestedContentTypeResolver;
import org.springframework.web.reactive.accept.RequestedContentTypeResolverBuilder; import org.springframework.web.reactive.accept.RequestedContentTypeResolverBuilder;
@ -48,6 +53,8 @@ import org.springframework.web.reactive.result.method.RequestMappingInfoHandlerM
public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMapping public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMapping
implements EmbeddedValueResolverAware { implements EmbeddedValueResolverAware {
private final Map<String, HandlerTypePredicate> pathPrefixes = new LinkedHashMap<>();
private RequestedContentTypeResolver contentTypeResolver = new RequestedContentTypeResolverBuilder().build(); private RequestedContentTypeResolver contentTypeResolver = new RequestedContentTypeResolverBuilder().build();
@Nullable @Nullable
@ -56,6 +63,22 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi
private RequestMappingInfo.BuilderConfiguration config = new RequestMappingInfo.BuilderConfiguration(); private RequestMappingInfo.BuilderConfiguration config = new RequestMappingInfo.BuilderConfiguration();
/**
* Configure path prefixes to apply to controller methods.
* <p>Prefixes are used to enrich the mappings of every {@code @RequestMapping}
* method whose controller type is matched by the corresponding
* {@link HandlerTypePredicate} in the map. The prefix for the first matching
* predicate is used, assuming the input map has predictable order.
* @param prefixes a map with path prefixes as key
* @since 5.1
*/
public void setPathPrefixes(Map<String, HandlerTypePredicate> prefixes) {
this.pathPrefixes.clear();
prefixes.entrySet().stream()
.filter(entry -> StringUtils.hasText(entry.getKey()))
.forEach(entry -> this.pathPrefixes.put(entry.getKey(), entry.getValue()));
}
/** /**
* Set the {@link RequestedContentTypeResolver} to use to determine requested media types. * Set the {@link RequestedContentTypeResolver} to use to determine requested media types.
* If not set, the default constructor is used. * If not set, the default constructor is used.
@ -80,6 +103,14 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi
} }
/**
* The configured path prefixes as a read-only, possibly empty map.
* @since 5.1
*/
public Map<String, HandlerTypePredicate> getPathPrefixes() {
return Collections.unmodifiableMap(this.pathPrefixes);
}
/** /**
* Return the configured {@link RequestedContentTypeResolver}. * Return the configured {@link RequestedContentTypeResolver}.
*/ */
@ -113,6 +144,16 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi
if (typeInfo != null) { if (typeInfo != null) {
info = typeInfo.combine(info); info = typeInfo.combine(info);
} }
for (Map.Entry<String, HandlerTypePredicate> 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;
}
}
} }
return info; return info;
} }

View File

@ -18,8 +18,10 @@ package org.springframework.web.reactive.config;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.security.Principal;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map;
import javax.xml.bind.annotation.XmlRootElement; import javax.xml.bind.annotation.XmlRootElement;
import org.junit.Test; import org.junit.Test;
@ -47,11 +49,17 @@ import org.springframework.util.MimeTypeUtils;
import org.springframework.util.MultiValueMap; import org.springframework.util.MultiValueMap;
import org.springframework.util.ReflectionUtils; import org.springframework.util.ReflectionUtils;
import org.springframework.validation.Validator; import org.springframework.validation.Validator;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.support.WebBindingInitializer; import org.springframework.web.bind.support.WebBindingInitializer;
import org.springframework.web.bind.support.WebExchangeDataBinder; import org.springframework.web.bind.support.WebExchangeDataBinder;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.method.HandlerTypePredicate;
import org.springframework.web.reactive.accept.RequestedContentTypeResolver; import org.springframework.web.reactive.accept.RequestedContentTypeResolver;
import org.springframework.web.reactive.handler.AbstractUrlHandlerMapping; import org.springframework.web.reactive.handler.AbstractUrlHandlerMapping;
import org.springframework.web.reactive.handler.SimpleUrlHandlerMapping; import org.springframework.web.reactive.handler.SimpleUrlHandlerMapping;
import org.springframework.web.reactive.result.method.RequestMappingInfo;
import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerAdapter; import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerAdapter;
import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerMapping; import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerMapping;
import org.springframework.web.reactive.result.method.annotation.ResponseBodyResultHandler; import org.springframework.web.reactive.result.method.annotation.ResponseBodyResultHandler;
@ -67,6 +75,7 @@ import org.springframework.web.server.WebHandler;
import org.springframework.web.util.pattern.PathPatternParser; import org.springframework.web.util.pattern.PathPatternParser;
import static org.junit.Assert.*; import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
import static org.springframework.core.ResolvableType.forClass; import static org.springframework.core.ResolvableType.forClass;
import static org.springframework.core.ResolvableType.forClassWithGenerics; import static org.springframework.core.ResolvableType.forClassWithGenerics;
import static org.springframework.http.MediaType.APPLICATION_FORM_URLENCODED; import static org.springframework.http.MediaType.APPLICATION_FORM_URLENCODED;
@ -110,7 +119,7 @@ public class WebFluxConfigurationSupportTests {
} }
@Test @Test
public void customPathMatchConfig() throws Exception { public void customPathMatchConfig() {
ApplicationContext context = loadConfig(CustomPatchMatchConfig.class); ApplicationContext context = loadConfig(CustomPatchMatchConfig.class);
final Field field = ReflectionUtils.findField(PathPatternParser.class, "matchOptionalTrailingSeparator"); final Field field = ReflectionUtils.findField(PathPatternParser.class, "matchOptionalTrailingSeparator");
ReflectionUtils.makeAccessible(field); ReflectionUtils.makeAccessible(field);
@ -123,6 +132,11 @@ public class WebFluxConfigurationSupportTests {
assertNotNull(patternParser); assertNotNull(patternParser);
boolean matchOptionalTrailingSlash = (boolean) ReflectionUtils.getField(field, patternParser); boolean matchOptionalTrailingSlash = (boolean) ReflectionUtils.getField(field, patternParser);
assertFalse(matchOptionalTrailingSlash); assertFalse(matchOptionalTrailingSlash);
Map<RequestMappingInfo, HandlerMethod> map = mapping.getHandlerMethods();
assertEquals(1, map.size());
assertEquals(Collections.singleton(new PathPatternParser().parse("/api/user/{id}")),
map.keySet().iterator().next().getPatternsCondition().getPatterns());
} }
@Test @Test
@ -295,6 +309,23 @@ public class WebFluxConfigurationSupportTests {
@Override @Override
public void configurePathMatching(PathMatchConfigurer configurer) { public void configurePathMatching(PathMatchConfigurer configurer) {
configurer.setUseTrailingSlashMatch(false); configurer.setUseTrailingSlashMatch(false);
configurer.addPathPrefix("/api", HandlerTypePredicate.forAnnotation(RestController.class));
}
@Bean
UserController userController() {
return new UserController();
}
}
@RestController
@RequestMapping("/user")
static class UserController {
@GetMapping("/{id}")
public Principal getUser() {
return mock(Principal.class);
} }
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2017 the original author or authors. * Copyright 2002-2018 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -21,7 +21,9 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target; import java.lang.annotation.Target;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.security.Principal;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.Set; import java.util.Set;
import org.junit.Before; import org.junit.Before;
@ -37,13 +39,17 @@ import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.support.StaticWebApplicationContext; import org.springframework.web.context.support.StaticWebApplicationContext;
import org.springframework.web.method.HandlerTypePredicate;
import org.springframework.web.reactive.result.method.RequestMappingInfo; import org.springframework.web.reactive.result.method.RequestMappingInfo;
import org.springframework.web.util.pattern.PathPattern; import org.springframework.web.util.pattern.PathPattern;
import org.springframework.web.util.pattern.PathPatternParser;
import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotNull;
import static org.mockito.Mockito.*;
/** /**
* Unit tests for {@link RequestMappingHandlerMapping}. * Unit tests for {@link RequestMappingHandlerMapping}.
@ -64,9 +70,7 @@ public class RequestMappingHandlerMappingTests {
@Test @Test
public void resolveEmbeddedValuesInPatterns() { public void resolveEmbeddedValuesInPatterns() {
this.handlerMapping.setEmbeddedValueResolver( this.handlerMapping.setEmbeddedValueResolver(value -> "/${pattern}/bar".equals(value) ? "/foo/bar" : value);
value -> "/${pattern}/bar".equals(value) ? "/foo/bar" : value
);
String[] patterns = new String[] { "/foo", "/${pattern}/bar" }; String[] patterns = new String[] { "/foo", "/${pattern}/bar" };
String[] result = this.handlerMapping.resolveEmbeddedValuesInPatterns(patterns); String[] result = this.handlerMapping.resolveEmbeddedValuesInPatterns(patterns);
@ -74,6 +78,20 @@ public class RequestMappingHandlerMappingTests {
assertArrayEquals(new String[] { "/foo", "/foo/bar" }, result); assertArrayEquals(new String[] { "/foo", "/foo/bar" }, result);
} }
@Test
public void pathPrefix() throws NoSuchMethodException {
this.handlerMapping.setEmbeddedValueResolver(value -> "/${prefix}".equals(value) ? "/api" : value);
this.handlerMapping.setPathPrefixes(Collections.singletonMap(
"/${prefix}", HandlerTypePredicate.forAnnotation(RestController.class)));
Method method = UserController.class.getMethod("getUser");
RequestMappingInfo info = this.handlerMapping.getMappingForMethod(method, UserController.class);
assertNotNull(info);
assertEquals(Collections.singleton(new PathPatternParser().parse("/api/user/{id}")),
info.getPatternsCondition().getPatterns());
}
@Test @Test
public void resolveRequestMappingViaComposedAnnotation() throws Exception { public void resolveRequestMappingViaComposedAnnotation() throws Exception {
RequestMappingInfo info = assertComposedAnnotationMapping("postJson", "/postJson", RequestMethod.POST); RequestMappingInfo info = assertComposedAnnotationMapping("postJson", "/postJson", RequestMethod.POST);
@ -191,4 +209,15 @@ public class RequestMappingHandlerMappingTests {
String[] value() default {}; String[] value() default {};
} }
@RestController
@RequestMapping("/user")
static class UserController {
@GetMapping("/{id}")
public Principal getUser() {
return mock(Principal.class);
}
}
} }

View File

@ -16,8 +16,12 @@
package org.springframework.web.servlet.config.annotation; package org.springframework.web.servlet.config.annotation;
import java.util.LinkedHashMap;
import java.util.Map;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
import org.springframework.util.PathMatcher; import org.springframework.util.PathMatcher;
import org.springframework.web.method.HandlerTypePredicate;
import org.springframework.web.util.UrlPathHelper; import org.springframework.web.util.UrlPathHelper;
/** /**
@ -53,6 +57,9 @@ public class PathMatchConfigurer {
@Nullable @Nullable
private PathMatcher pathMatcher; private PathMatcher pathMatcher;
@Nullable
private Map<String, HandlerTypePredicate> pathPrefixes;
/** /**
* Whether to use suffix pattern match (".*") when matching patterns to * Whether to use suffix pattern match (".*") when matching patterns to
@ -110,6 +117,22 @@ public class PathMatchConfigurer {
return this; return this;
} }
/**
* Configure a path prefix to apply to matching controller methods.
* <p>Prefixes are used to enrich the mappings of every {@code @RequestMapping}
* method whose controller type is matched by the corresponding
* {@link HandlerTypePredicate}. The prefix for the first matching predicate
* is used.
* @param prefix the prefix to apply
* @param predicate a predicate for matching controller types
* @since 5.1
*/
public PathMatchConfigurer addPathPrefix(String prefix, HandlerTypePredicate predicate) {
this.pathPrefixes = this.pathPrefixes == null ? new LinkedHashMap<>() : this.pathPrefixes;
this.pathPrefixes.put(prefix, predicate);
return this;
}
@Nullable @Nullable
public Boolean isUseSuffixPatternMatch() { public Boolean isUseSuffixPatternMatch() {
@ -136,4 +159,8 @@ public class PathMatchConfigurer {
return this.pathMatcher; return this.pathMatcher;
} }
@Nullable
protected Map<String, HandlerTypePredicate> getPathPrefixes() {
return this.pathPrefixes;
}
} }

View File

@ -70,6 +70,7 @@ import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.support.ConfigurableWebBindingInitializer; import org.springframework.web.bind.support.ConfigurableWebBindingInitializer;
import org.springframework.web.context.ServletContextAware; import org.springframework.web.context.ServletContextAware;
import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.method.HandlerTypePredicate;
import org.springframework.web.method.support.CompositeUriComponentsContributor; import org.springframework.web.method.support.CompositeUriComponentsContributor;
import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.HandlerMethodReturnValueHandler; import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
@ -284,15 +285,18 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv
mapping.setCorsConfigurations(getCorsConfigurations()); mapping.setCorsConfigurations(getCorsConfigurations());
PathMatchConfigurer configurer = getPathMatchConfigurer(); PathMatchConfigurer configurer = getPathMatchConfigurer();
Boolean useSuffixPatternMatch = configurer.isUseSuffixPatternMatch(); Boolean useSuffixPatternMatch = configurer.isUseSuffixPatternMatch();
Boolean useRegisteredSuffixPatternMatch = configurer.isUseRegisteredSuffixPatternMatch();
Boolean useTrailingSlashMatch = configurer.isUseTrailingSlashMatch();
if (useSuffixPatternMatch != null) { if (useSuffixPatternMatch != null) {
mapping.setUseSuffixPatternMatch(useSuffixPatternMatch); mapping.setUseSuffixPatternMatch(useSuffixPatternMatch);
} }
Boolean useRegisteredSuffixPatternMatch = configurer.isUseRegisteredSuffixPatternMatch();
if (useRegisteredSuffixPatternMatch != null) { if (useRegisteredSuffixPatternMatch != null) {
mapping.setUseRegisteredSuffixPatternMatch(useRegisteredSuffixPatternMatch); mapping.setUseRegisteredSuffixPatternMatch(useRegisteredSuffixPatternMatch);
} }
Boolean useTrailingSlashMatch = configurer.isUseTrailingSlashMatch();
if (useTrailingSlashMatch != null) { if (useTrailingSlashMatch != null) {
mapping.setUseTrailingSlashMatch(useTrailingSlashMatch); mapping.setUseTrailingSlashMatch(useTrailingSlashMatch);
} }
@ -307,6 +311,11 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv
mapping.setPathMatcher(pathMatcher); mapping.setPathMatcher(pathMatcher);
} }
Map<String, HandlerTypePredicate> pathPrefixes = configurer.getPathPrefixes();
if (pathPrefixes != null) {
mapping.setPathPrefixes(pathPrefixes);
}
return mapping; return mapping;
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2017 the original author or authors. * Copyright 2002-2018 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -18,7 +18,10 @@ package org.springframework.web.servlet.mvc.method.annotation;
import java.lang.reflect.AnnotatedElement; import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Set; import java.util.Set;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
@ -28,12 +31,14 @@ import org.springframework.lang.Nullable;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils; import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.util.StringValueResolver; import org.springframework.util.StringValueResolver;
import org.springframework.web.accept.ContentNegotiationManager; import org.springframework.web.accept.ContentNegotiationManager;
import org.springframework.web.bind.annotation.CrossOrigin; import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.method.HandlerTypePredicate;
import org.springframework.web.method.HandlerMethod; import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.handler.MatchableHandlerMapping; import org.springframework.web.servlet.handler.MatchableHandlerMapping;
import org.springframework.web.servlet.handler.RequestMatchResult; import org.springframework.web.servlet.handler.RequestMatchResult;
@ -62,6 +67,8 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi
private boolean useTrailingSlashMatch = true; private boolean useTrailingSlashMatch = true;
private final Map<String, HandlerTypePredicate> pathPrefixes = new LinkedHashMap<>();
private ContentNegotiationManager contentNegotiationManager = new ContentNegotiationManager(); private ContentNegotiationManager contentNegotiationManager = new ContentNegotiationManager();
@Nullable @Nullable
@ -102,6 +109,22 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi
this.useTrailingSlashMatch = useTrailingSlashMatch; this.useTrailingSlashMatch = useTrailingSlashMatch;
} }
/**
* Configure path prefixes to apply to controller methods.
* <p>Prefixes are used to enrich the mappings of every {@code @RequestMapping}
* method whose controller type is matched by the corresponding
* {@link HandlerTypePredicate} in the map. The prefix for the first matching
* predicate is used, assuming the input map has predictable order.
* @param prefixes a map with path prefixes as key
* @since 5.1
*/
public void setPathPrefixes(Map<String, HandlerTypePredicate> prefixes) {
this.pathPrefixes.clear();
prefixes.entrySet().stream()
.filter(entry -> StringUtils.hasText(entry.getKey()))
.forEach(entry -> this.pathPrefixes.put(entry.getKey(), entry.getValue()));
}
/** /**
* Set the {@link ContentNegotiationManager} to use to determine requested media types. * Set the {@link ContentNegotiationManager} to use to determine requested media types.
* If not set, the default constructor is used. * If not set, the default constructor is used.
@ -151,6 +174,14 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi
return this.useTrailingSlashMatch; return this.useTrailingSlashMatch;
} }
/**
* The configured path prefixes as a read-only, possibly empty map.
* @since 5.1
*/
public Map<String, HandlerTypePredicate> getPathPrefixes() {
return Collections.unmodifiableMap(this.pathPrefixes);
}
/** /**
* Return the configured {@link ContentNegotiationManager}. * Return the configured {@link ContentNegotiationManager}.
*/ */
@ -195,6 +226,16 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi
if (typeInfo != null) { if (typeInfo != null) {
info = typeInfo.combine(info); info = typeInfo.combine(info);
} }
for (Map.Entry<String, HandlerTypePredicate> 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;
}
}
} }
return info; return info;
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2016 the original author or authors. * Copyright 2002-2018 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -16,6 +16,7 @@
package org.springframework.web.servlet.config.annotation; package org.springframework.web.servlet.config.annotation;
import java.security.Principal;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
@ -48,7 +49,9 @@ import org.springframework.validation.Errors;
import org.springframework.validation.MessageCodesResolver; import org.springframework.validation.MessageCodesResolver;
import org.springframework.validation.Validator; import org.springframework.validation.Validator;
import org.springframework.web.accept.ContentNegotiationManager; import org.springframework.web.accept.ContentNegotiationManager;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.support.ConfigurableWebBindingInitializer; import org.springframework.web.bind.support.ConfigurableWebBindingInitializer;
import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.ServletWebRequest; import org.springframework.web.context.request.ServletWebRequest;
@ -56,6 +59,8 @@ import org.springframework.web.context.request.async.CallableProcessingIntercept
import org.springframework.web.context.request.async.DeferredResultProcessingInterceptor; import org.springframework.web.context.request.async.DeferredResultProcessingInterceptor;
import org.springframework.web.context.support.StaticWebApplicationContext; import org.springframework.web.context.support.StaticWebApplicationContext;
import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.method.HandlerTypePredicate;
import org.springframework.web.method.annotation.ModelAttributeMethodProcessor; import org.springframework.web.method.annotation.ModelAttributeMethodProcessor;
import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.HandlerMethodReturnValueHandler; import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
@ -70,6 +75,7 @@ import org.springframework.web.servlet.handler.SimpleMappingExceptionResolver;
import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping; import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping;
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor; import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;
import org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver; import org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import org.springframework.web.servlet.resource.ResourceHttpRequestHandler; import org.springframework.web.servlet.resource.ResourceHttpRequestHandler;
@ -86,6 +92,7 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertSame; import static org.junit.Assert.assertSame;
import static org.mockito.Mockito.*;
import static org.springframework.http.MediaType.APPLICATION_ATOM_XML; import static org.springframework.http.MediaType.APPLICATION_ATOM_XML;
import static org.springframework.http.MediaType.APPLICATION_JSON; import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.http.MediaType.APPLICATION_XML; import static org.springframework.http.MediaType.APPLICATION_XML;
@ -114,6 +121,7 @@ public class WebMvcConfigurationSupportExtensionTests {
this.context = new StaticWebApplicationContext(); this.context = new StaticWebApplicationContext();
this.context.setServletContext(new MockServletContext(new FileSystemResourceLoader())); this.context.setServletContext(new MockServletContext(new FileSystemResourceLoader()));
this.context.registerSingleton("controller", TestController.class); this.context.registerSingleton("controller", TestController.class);
this.context.registerSingleton("userController", UserController.class);
this.config = new TestWebMvcConfigurationSupport(); this.config = new TestWebMvcConfigurationSupport();
this.config.setApplicationContext(this.context); this.config.setApplicationContext(this.context);
@ -135,6 +143,15 @@ public class WebMvcConfigurationSupportExtensionTests {
assertEquals(ConversionServiceExposingInterceptor.class, chain.getInterceptors()[1].getClass()); assertEquals(ConversionServiceExposingInterceptor.class, chain.getInterceptors()[1].getClass());
assertEquals(ResourceUrlProviderExposingInterceptor.class, chain.getInterceptors()[2].getClass()); assertEquals(ResourceUrlProviderExposingInterceptor.class, chain.getInterceptors()[2].getClass());
Map<RequestMappingInfo, HandlerMethod> map = rmHandlerMapping.getHandlerMethods();
assertEquals(2, map.size());
RequestMappingInfo info = map.entrySet().stream()
.filter(entry -> entry.getValue().getBeanType().equals(UserController.class))
.findFirst()
.orElseThrow(() -> new AssertionError("UserController bean not found"))
.getKey();
assertEquals(Collections.singleton("/api/user/{id}"), info.getPatternsCondition().getPatterns());
AbstractHandlerMapping handlerMapping = (AbstractHandlerMapping) this.config.viewControllerHandlerMapping(); AbstractHandlerMapping handlerMapping = (AbstractHandlerMapping) this.config.viewControllerHandlerMapping();
handlerMapping.setApplicationContext(this.context); handlerMapping.setApplicationContext(this.context);
assertNotNull(handlerMapping); assertNotNull(handlerMapping);
@ -402,6 +419,7 @@ public class WebMvcConfigurationSupportExtensionTests {
public void configurePathMatch(PathMatchConfigurer configurer) { public void configurePathMatch(PathMatchConfigurer configurer) {
configurer.setPathMatcher(new TestPathMatcher()); configurer.setPathMatcher(new TestPathMatcher());
configurer.setUrlPathHelper(new TestPathHelper()); configurer.setUrlPathHelper(new TestPathHelper());
configurer.addPathPrefix("/api", HandlerTypePredicate.forAnnotation(RestController.class));
} }
@Override @Override
@ -453,4 +471,16 @@ public class WebMvcConfigurationSupportExtensionTests {
private class TestPathHelper extends UrlPathHelper {} private class TestPathHelper extends UrlPathHelper {}
private class TestPathMatcher extends AntPathMatcher {} private class TestPathMatcher extends AntPathMatcher {}
@RestController
@RequestMapping("/user")
static class UserController {
@GetMapping("/{id}")
public Principal getUser() {
return mock(Principal.class);
}
}
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2016 the original author or authors. * Copyright 2002-2018 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -21,6 +21,7 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target; import java.lang.annotation.Target;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.security.Principal;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
@ -42,14 +43,13 @@ import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.support.StaticWebApplicationContext; import org.springframework.web.context.support.StaticWebApplicationContext;
import org.springframework.web.method.HandlerTypePredicate;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo; import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.*;
import static org.junit.Assert.assertEquals; import static org.mockito.Mockito.*;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
/** /**
* Tests for {@link RequestMappingHandlerMapping}. * Tests for {@link RequestMappingHandlerMapping}.
@ -140,6 +140,19 @@ public class RequestMappingHandlerMappingTests {
assertArrayEquals(new String[] { "/foo", "/foo/bar" }, result); assertArrayEquals(new String[] { "/foo", "/foo/bar" }, result);
} }
@Test
public void pathPrefix() throws NoSuchMethodException {
this.handlerMapping.setEmbeddedValueResolver(value -> "/${prefix}".equals(value) ? "/api" : value);
this.handlerMapping.setPathPrefixes(Collections.singletonMap(
"/${prefix}", HandlerTypePredicate.forAnnotation(RestController.class)));
Method method = UserController.class.getMethod("getUser");
RequestMappingInfo info = this.handlerMapping.getMappingForMethod(method, UserController.class);
assertNotNull(info);
assertEquals(Collections.singleton("/api/user/{id}"), info.getPatternsCondition().getPatterns());
}
@Test @Test
public void resolveRequestMappingViaComposedAnnotation() throws Exception { public void resolveRequestMappingViaComposedAnnotation() throws Exception {
RequestMappingInfo info = assertComposedAnnotationMapping("postJson", "/postJson", RequestMethod.POST); RequestMappingInfo info = assertComposedAnnotationMapping("postJson", "/postJson", RequestMethod.POST);
@ -245,6 +258,7 @@ public class RequestMappingHandlerMappingTests {
} }
@RequestMapping(method = RequestMethod.POST, @RequestMapping(method = RequestMethod.POST,
produces = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE,
consumes = MediaType.APPLICATION_JSON_VALUE) consumes = MediaType.APPLICATION_JSON_VALUE)
@ -256,4 +270,15 @@ public class RequestMappingHandlerMappingTests {
String[] value() default {}; String[] value() default {};
} }
@RestController
@RequestMapping("/user")
static class UserController {
@GetMapping("/{id}")
public Principal getUser() {
return mock(Principal.class);
}
}
} }

View File

@ -2929,16 +2929,8 @@ match to incoming URLs without versions -- e.g. `"/jquery/jquery.min.js"` to
=== Path Matching === Path Matching
[.small]#<<web.adoc#mvc-config-path-matching,Same in Spring MVC>># [.small]#<<web.adoc#mvc-config-path-matching,Same in Spring MVC>>#
Spring WebFlux uses parsed representation of path patterns -- i.e. `PathPattern`, and also Customize options related to path matching. For details on the individual options, see the
the incoming request path -- i.e. `RequestPath`, which eliminates the need to indicate {api-spring-framework}/web/reactive/config/PathMatchConfigurer.html[PathMatchConfigurer] Javadoc.
whether to decode the request path, or remove semicolon content, since `PathPattern`
can now access decoded path segment values and match safely.
Spring WebFlux also does not support suffix pattern matching so effectively there are only two
minor options to customize related to path matching -- whether to match trailing slashes
(`true` by default) and whether the match is case-sensitive (`false`).
To customize those options:
[source,java,indent=0] [source,java,indent=0]
[subs="verbatim,quotes"] [subs="verbatim,quotes"]
@ -2949,12 +2941,29 @@ To customize those options:
@Override @Override
public void configurePathMatch(PathMatchConfigurer configurer) { public void configurePathMatch(PathMatchConfigurer configurer) {
// ... configurer
.setUseCaseSensitiveMatch(true)
.setUseTrailingSlashMatch(false)
.addPathPrefix("/api",
HandlerTypePredicate.forAnnotation(RestController.class));
} }
} }
---- ----
[TIP]
====
Spring WebFlux relies on a parsed representation of the request path called
`RequestPath` for access to decoded path segment values, with semicolon content removed
(i.e. path/matrix variables). That means, unlike Spring MVC, there is no need to indicate
neither whether to decode the request path, nor whether to remove semicolon content for
path matching purposes.
Spring WebFlux also does not support suffix pattern matching, unlike Spring MVC, where we
also <<web.adoc#mvc-ann-requestmapping-suffix-pattern-match,recommend>> moving away from
reliance on it.
====
[[webflux-config-advanced-java]] [[webflux-config-advanced-java]]

View File

@ -4644,9 +4644,9 @@ Or in XML:
=== Path Matching === Path Matching
[.small]#<<web-reactive.adoc#webflux-config-path-matching,Same in Spring WebFlux>># [.small]#<<web-reactive.adoc#webflux-config-path-matching,Same in Spring WebFlux>>#
This allows customizing options related to URL matching and treatment of the URL. Customize options related to path matching, and treatment of the URL.
For details on the individual options check out the For details on the individual options, see the
{api-spring-framework}/web/servlet/config/annotation/PathMatchConfigurer.html[PathMatchConfigurer] API. {api-spring-framework}/web/servlet/config/annotation/PathMatchConfigurer.html[PathMatchConfigurer] Javadoc.
Example in Java config: Example in Java config:
@ -4664,7 +4664,9 @@ Example in Java config:
.setUseTrailingSlashMatch(false) .setUseTrailingSlashMatch(false)
.setUseRegisteredSuffixPatternMatch(true) .setUseRegisteredSuffixPatternMatch(true)
.setPathMatcher(antPathMatcher()) .setPathMatcher(antPathMatcher())
.setUrlPathHelper(urlPathHelper()); .setUrlPathHelper(urlPathHelper())
.addPathPrefix("/api",
HandlerTypePredicate.forAnnotation(RestController.class));
} }
@Bean @Bean