Skip to content

Commit c0b52d0

Browse files
committed
Require explicit path mappings for @RequestMapping methods
Prior to this commit, handler methods in Spring MVC controllers were not required to provide explicit path mappings via @RequestMapping (or any of its specializations such as @GetMapping). Such handler methods were effectively mapped to all paths. Consequently, developers may have unwittingly mapped all requests to a single handler method. This commit addresses this by enforcing that @RequestMapping methods are mapped to an explicit path. Note, however, that this is enforced after type-level and method-level @RequestMapping information has been merged. Developers wishing to map to all paths should now add an explicit path mapping to "/**" or "**". Closes gh-22543
1 parent de69871 commit c0b52d0

File tree

6 files changed

+71
-32
lines changed

6 files changed

+71
-32
lines changed

spring-web/src/main/java/org/springframework/web/bind/annotation/GetMapping.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@
3131
* <p>Specifically, {@code @GetMapping} is a <em>composed annotation</em> that
3232
* acts as a shortcut for {@code @RequestMapping(method = RequestMethod.GET)}.
3333
*
34-
*
3534
* @author Sam Brannen
3635
* @since 4.3
3736
* @see PostMapping

spring-web/src/main/java/org/springframework/web/bind/annotation/RequestMapping.java

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -88,26 +88,33 @@
8888

8989
/**
9090
* The primary mapping expressed by this annotation.
91-
* <p>This is an alias for {@link #path}. For example
91+
* <p>This is an alias for {@link #path}. For example,
9292
* {@code @RequestMapping("/foo")} is equivalent to
9393
* {@code @RequestMapping(path="/foo")}.
9494
* <p><b>Supported at the type level as well as at the method level!</b>
9595
* When used at the type level, all method-level mappings inherit
9696
* this primary mapping, narrowing it for a specific handler method.
97+
* <p><strong>NOTE</strong>: Each handler method must be mapped to a
98+
* non-empty path, either at the type level, at the method level, or a
99+
* combination of the two. If you wish to map to all paths, please map
100+
* explicitly to {@code "/**"} or {@code "**"}.
97101
*/
98102
@AliasFor("path")
99103
String[] value() default {};
100104

101105
/**
102-
* The path mapping URIs (e.g. "/myPath.do").
103-
* Ant-style path patterns are also supported (e.g. "/myPath/*.do").
104-
* At the method level, relative paths (e.g. "edit.do") are supported
106+
* The path mapping URIs (e.g. {@code "/myPath.do"}).
107+
* <p>Ant-style path patterns are also supported (e.g. {@code "/myPath/*.do"}).
108+
* At the method level, relative paths (e.g. {@code "edit.do"}) are supported
105109
* within the primary mapping expressed at the type level.
106-
* Path mapping URIs may contain placeholders (e.g. "/${connect}").
110+
* Path mapping URIs may contain placeholders (e.g. <code>"/${connect}"</code>).
107111
* <p><b>Supported at the type level as well as at the method level!</b>
108112
* When used at the type level, all method-level mappings inherit
109113
* this primary mapping, narrowing it for a specific handler method.
110-
* @see org.springframework.web.bind.annotation.ValueConstants#DEFAULT_NONE
114+
* <p><strong>NOTE</strong>: Each handler method must be mapped to a
115+
* non-empty path, either at the type level, at the method level, or a
116+
* combination of the two. If you wish to map to all paths, please map
117+
* explicitly to {@code "/**"} or {@code "**"}.
111118
* @since 4.2
112119
*/
113120
@AliasFor("value")

spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMethodMapping.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
import org.springframework.util.ClassUtils;
4444
import org.springframework.util.LinkedMultiValueMap;
4545
import org.springframework.util.MultiValueMap;
46+
import org.springframework.util.StringUtils;
4647
import org.springframework.web.cors.CorsConfiguration;
4748
import org.springframework.web.cors.CorsUtils;
4849
import org.springframework.web.method.HandlerMethod;
@@ -58,6 +59,7 @@
5859
* @author Arjen Poutsma
5960
* @author Rossen Stoyanchev
6061
* @author Juergen Hoeller
62+
* @author Sam Brannen
6163
* @since 3.1
6264
* @param <T> the mapping for a {@link HandlerMethod} containing the conditions
6365
* needed to match the handler method to incoming request.
@@ -587,6 +589,7 @@ public void register(T mapping, Object handler, Method method) {
587589
this.readWriteLock.writeLock().lock();
588590
try {
589591
HandlerMethod handlerMethod = createHandlerMethod(handler, method);
592+
assertMappedPathMethodMapping(handlerMethod, mapping);
590593
assertUniqueMethodMapping(handlerMethod, mapping);
591594
this.mappingLookup.put(mapping, handlerMethod);
592595

@@ -613,6 +616,21 @@ public void register(T mapping, Object handler, Method method) {
613616
}
614617
}
615618

619+
/**
620+
* Assert that the supplied {@code mapping} maps the supplied {@link HandlerMethod}
621+
* to explicit, non-empty paths.
622+
* @since 5.2
623+
* @see StringUtils#hasText(String)
624+
*/
625+
private void assertMappedPathMethodMapping(HandlerMethod handlerMethod, T mapping) {
626+
if (!getMappingPathPatterns(mapping).stream().allMatch(StringUtils::hasText)) {
627+
throw new IllegalStateException(String.format("Missing path mapping. " +
628+
"Handler method '%s' in bean '%s' must be mapped to a non-empty path. " +
629+
"If you wish to map to all paths, please map explicitly to \"/**\" or \"**\".",
630+
handlerMethod, handlerMethod.getBean()));
631+
}
632+
}
633+
616634
private void assertUniqueMethodMapping(HandlerMethod newHandlerMethod, T mapping) {
617635
HandlerMethod handlerMethod = this.mappingLookup.get(mapping);
618636
if (handlerMethod != null && !handlerMethod.equals(newHandlerMethod)) {

spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/RequestMappingInfoHandlerMappingTests.java

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ public class RequestMappingInfoHandlerMappingTests {
7878

7979
private HandlerMethod barMethod;
8080

81-
private HandlerMethod emptyMethod;
81+
private HandlerMethod rootMethod;
8282

8383

8484
@Before
@@ -88,7 +88,7 @@ public void setup() throws Exception {
8888
this.fooMethod = new HandlerMethod(testController, "foo");
8989
this.fooParamMethod = new HandlerMethod(testController, "fooParam");
9090
this.barMethod = new HandlerMethod(testController, "bar");
91-
this.emptyMethod = new HandlerMethod(testController, "empty");
91+
this.rootMethod = new HandlerMethod(testController, "root");
9292

9393
this.handlerMapping = new TestRequestMappingInfoHandlerMapping();
9494
this.handlerMapping.registerHandler(testController);
@@ -125,12 +125,12 @@ public void getHandlerEmptyPathMatch() throws Exception {
125125
MockHttpServletRequest request = new MockHttpServletRequest("GET", "");
126126
HandlerMethod handlerMethod = getHandler(request);
127127

128-
assertEquals(this.emptyMethod.getMethod(), handlerMethod.getMethod());
128+
assertEquals(this.rootMethod.getMethod(), handlerMethod.getMethod());
129129

130130
request = new MockHttpServletRequest("GET", "/");
131131
handlerMethod = getHandler(request);
132132

133-
assertEquals(this.emptyMethod.getMethod(), handlerMethod.getMethod());
133+
assertEquals(this.rootMethod.getMethod(), handlerMethod.getMethod());
134134
}
135135

136136
@Test
@@ -465,8 +465,8 @@ public void fooParam() {
465465
public void bar() {
466466
}
467467

468-
@RequestMapping(value = "")
469-
public void empty() {
468+
@RequestMapping("/")
469+
public void root() {
470470
}
471471

472472
@RequestMapping(value = "/person/{id}", method = RequestMethod.PUT, consumes="application/xml")

spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMappingTests.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2018 the original author or authors.
2+
* Copyright 2002-2019 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -228,15 +228,15 @@ private RequestMappingInfo assertComposedAnnotationMapping(String methodName, St
228228
@RequestMapping(consumes = MediaType.APPLICATION_JSON_VALUE)
229229
static class ComposedAnnotationController {
230230

231-
@RequestMapping
231+
@RequestMapping("/**")
232232
public void handle() {
233233
}
234234

235235
@PostJson("/postJson")
236236
public void postJson() {
237237
}
238238

239-
@GetMapping(value = "/get", consumes = MediaType.ALL_VALUE)
239+
@GetMapping(path = "/get", consumes = MediaType.ALL_VALUE)
240240
public void get() {
241241
}
242242

@@ -266,7 +266,7 @@ public void patch() {
266266
@Retention(RetentionPolicy.RUNTIME)
267267
@interface PostJson {
268268

269-
@AliasFor(annotation = RequestMapping.class, attribute = "path")
269+
@AliasFor(annotation = RequestMapping.class)
270270
String[] value() default {};
271271
}
272272

spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ServletAnnotationControllerHandlerMethodTests.java

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2018 the original author or authors.
2+
* Copyright 2002-2019 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -67,10 +67,10 @@
6767
import org.springframework.beans.factory.annotation.Autowired;
6868
import org.springframework.beans.factory.annotation.Value;
6969
import org.springframework.beans.factory.config.BeanDefinition;
70-
import org.springframework.beans.factory.config.PropertyPlaceholderConfigurer;
7170
import org.springframework.beans.factory.support.RootBeanDefinition;
7271
import org.springframework.beans.propertyeditors.CustomDateEditor;
7372
import org.springframework.context.annotation.AnnotationConfigUtils;
73+
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
7474
import org.springframework.core.MethodParameter;
7575
import org.springframework.core.convert.converter.Converter;
7676
import org.springframework.format.annotation.DateTimeFormat;
@@ -151,11 +151,13 @@
151151
import org.springframework.web.servlet.view.AbstractView;
152152
import org.springframework.web.servlet.view.InternalResourceViewResolver;
153153

154+
import static org.assertj.core.api.Assertions.*;
154155
import static org.junit.Assert.*;
155156

156157
/**
157158
* @author Rossen Stoyanchev
158159
* @author Juergen Hoeller
160+
* @author Sam Brannen
159161
*/
160162
public class ServletAnnotationControllerHandlerMethodTests extends AbstractServletHandlerMethodTests {
161163

@@ -251,7 +253,7 @@ public void defaultParameters() throws Exception {
251253
@Test
252254
public void defaultExpressionParameters() throws Exception {
253255
initServlet(wac -> {
254-
RootBeanDefinition ppc = new RootBeanDefinition(PropertyPlaceholderConfigurer.class);
256+
RootBeanDefinition ppc = new RootBeanDefinition(PropertySourcesPlaceholderConfigurer.class);
255257
ppc.getPropertyValues().add("properties", "myKey=foo");
256258
wac.registerBeanDefinition("ppc", ppc);
257259
}, DefaultExpressionValueParamController.class);
@@ -787,15 +789,19 @@ public void nullCommandController() throws Exception {
787789
}
788790

789791
@Test
790-
public void equivalentMappingsWithSameMethodName() throws Exception {
791-
try {
792-
initServletWithControllers(ChildController.class);
793-
fail("Expected 'method already mapped' error");
794-
}
795-
catch (BeanCreationException e) {
796-
assertTrue(e.getCause() instanceof IllegalStateException);
797-
assertTrue(e.getCause().getMessage().contains("Ambiguous mapping"));
798-
}
792+
public void equivalentMappingsWithSameMethodName() {
793+
assertThatThrownBy(() -> initServletWithControllers(ChildController.class))
794+
.isInstanceOf(BeanCreationException.class)
795+
.hasCauseInstanceOf(IllegalStateException.class)
796+
.hasMessageContaining("Ambiguous mapping");
797+
}
798+
799+
@Test
800+
public void unmappedPathMapping() {
801+
assertThatThrownBy(() -> initServletWithControllers(UnmappedPathController.class))
802+
.isInstanceOf(BeanCreationException.class)
803+
.hasCauseInstanceOf(IllegalStateException.class)
804+
.hasMessageContaining("Missing path mapping");
799805
}
800806

801807
@Test
@@ -1993,7 +1999,7 @@ public void dataClassBindingWithLocalDate() throws Exception {
19931999
@Controller
19942000
static class ControllerWithEmptyValueMapping {
19952001

1996-
@RequestMapping("")
2002+
@RequestMapping("/**")
19972003
public void myPath2(HttpServletResponse response) throws IOException {
19982004
throw new IllegalStateException("test");
19992005
}
@@ -2012,7 +2018,7 @@ public void myPath2(Exception ex, HttpServletResponse response) throws IOExcepti
20122018
@Controller
20132019
private static class ControllerWithErrorThrown {
20142020

2015-
@RequestMapping("")
2021+
@RequestMapping("/**")
20162022
public void myPath2(HttpServletResponse response) throws IOException {
20172023
throw new AssertionError("test");
20182024
}
@@ -2726,6 +2732,15 @@ public void doGet(HttpServletRequest req, HttpServletResponse resp, @RequestPara
27262732
}
27272733
}
27282734

2735+
@Controller
2736+
@RequestMapping // path intentionally omitted
2737+
static class UnmappedPathController {
2738+
2739+
@GetMapping // path intentionally omitted
2740+
public void get(@RequestParam(required = false) String id) {
2741+
}
2742+
}
2743+
27292744
@Target({ElementType.TYPE})
27302745
@Retention(RetentionPolicy.RUNTIME)
27312746
@Controller
@@ -3586,7 +3601,7 @@ public String home() {
35863601
@Controller
35873602
static class HttpHeadersResponseController {
35883603

3589-
@RequestMapping(value = "", method = RequestMethod.POST)
3604+
@RequestMapping(value = "/*", method = RequestMethod.POST)
35903605
@ResponseStatus(HttpStatus.CREATED)
35913606
public HttpHeaders create() throws URISyntaxException {
35923607
HttpHeaders headers = new HttpHeaders();

0 commit comments

Comments
 (0)