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
This commit is contained in:
Sam Brannen
2019-04-04 16:55:54 +02:00
parent de69871354
commit c0b52d09f5
6 changed files with 71 additions and 32 deletions

View File

@@ -78,7 +78,7 @@ public class RequestMappingInfoHandlerMappingTests {
private HandlerMethod barMethod;
private HandlerMethod emptyMethod;
private HandlerMethod rootMethod;
@Before
@@ -88,7 +88,7 @@ public class RequestMappingInfoHandlerMappingTests {
this.fooMethod = new HandlerMethod(testController, "foo");
this.fooParamMethod = new HandlerMethod(testController, "fooParam");
this.barMethod = new HandlerMethod(testController, "bar");
this.emptyMethod = new HandlerMethod(testController, "empty");
this.rootMethod = new HandlerMethod(testController, "root");
this.handlerMapping = new TestRequestMappingInfoHandlerMapping();
this.handlerMapping.registerHandler(testController);
@@ -125,12 +125,12 @@ public class RequestMappingInfoHandlerMappingTests {
MockHttpServletRequest request = new MockHttpServletRequest("GET", "");
HandlerMethod handlerMethod = getHandler(request);
assertEquals(this.emptyMethod.getMethod(), handlerMethod.getMethod());
assertEquals(this.rootMethod.getMethod(), handlerMethod.getMethod());
request = new MockHttpServletRequest("GET", "/");
handlerMethod = getHandler(request);
assertEquals(this.emptyMethod.getMethod(), handlerMethod.getMethod());
assertEquals(this.rootMethod.getMethod(), handlerMethod.getMethod());
}
@Test
@@ -465,8 +465,8 @@ public class RequestMappingInfoHandlerMappingTests {
public void bar() {
}
@RequestMapping(value = "")
public void empty() {
@RequestMapping("/")
public void root() {
}
@RequestMapping(value = "/person/{id}", method = RequestMethod.PUT, consumes="application/xml")

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2019 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.
@@ -228,7 +228,7 @@ public class RequestMappingHandlerMappingTests {
@RequestMapping(consumes = MediaType.APPLICATION_JSON_VALUE)
static class ComposedAnnotationController {
@RequestMapping
@RequestMapping("/**")
public void handle() {
}
@@ -236,7 +236,7 @@ public class RequestMappingHandlerMappingTests {
public void postJson() {
}
@GetMapping(value = "/get", consumes = MediaType.ALL_VALUE)
@GetMapping(path = "/get", consumes = MediaType.ALL_VALUE)
public void get() {
}
@@ -266,7 +266,7 @@ public class RequestMappingHandlerMappingTests {
@Retention(RetentionPolicy.RUNTIME)
@interface PostJson {
@AliasFor(annotation = RequestMapping.class, attribute = "path")
@AliasFor(annotation = RequestMapping.class)
String[] value() default {};
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2019 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.
@@ -67,10 +67,10 @@ import org.springframework.beans.factory.BeanCreationException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.PropertyPlaceholderConfigurer;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.beans.propertyeditors.CustomDateEditor;
import org.springframework.context.annotation.AnnotationConfigUtils;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
import org.springframework.core.MethodParameter;
import org.springframework.core.convert.converter.Converter;
import org.springframework.format.annotation.DateTimeFormat;
@@ -151,11 +151,13 @@ import org.springframework.web.servlet.support.RequestContextUtils;
import org.springframework.web.servlet.view.AbstractView;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
import static org.assertj.core.api.Assertions.*;
import static org.junit.Assert.*;
/**
* @author Rossen Stoyanchev
* @author Juergen Hoeller
* @author Sam Brannen
*/
public class ServletAnnotationControllerHandlerMethodTests extends AbstractServletHandlerMethodTests {
@@ -251,7 +253,7 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl
@Test
public void defaultExpressionParameters() throws Exception {
initServlet(wac -> {
RootBeanDefinition ppc = new RootBeanDefinition(PropertyPlaceholderConfigurer.class);
RootBeanDefinition ppc = new RootBeanDefinition(PropertySourcesPlaceholderConfigurer.class);
ppc.getPropertyValues().add("properties", "myKey=foo");
wac.registerBeanDefinition("ppc", ppc);
}, DefaultExpressionValueParamController.class);
@@ -787,15 +789,19 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl
}
@Test
public void equivalentMappingsWithSameMethodName() throws Exception {
try {
initServletWithControllers(ChildController.class);
fail("Expected 'method already mapped' error");
}
catch (BeanCreationException e) {
assertTrue(e.getCause() instanceof IllegalStateException);
assertTrue(e.getCause().getMessage().contains("Ambiguous mapping"));
}
public void equivalentMappingsWithSameMethodName() {
assertThatThrownBy(() -> initServletWithControllers(ChildController.class))
.isInstanceOf(BeanCreationException.class)
.hasCauseInstanceOf(IllegalStateException.class)
.hasMessageContaining("Ambiguous mapping");
}
@Test
public void unmappedPathMapping() {
assertThatThrownBy(() -> initServletWithControllers(UnmappedPathController.class))
.isInstanceOf(BeanCreationException.class)
.hasCauseInstanceOf(IllegalStateException.class)
.hasMessageContaining("Missing path mapping");
}
@Test
@@ -1993,7 +1999,7 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl
@Controller
static class ControllerWithEmptyValueMapping {
@RequestMapping("")
@RequestMapping("/**")
public void myPath2(HttpServletResponse response) throws IOException {
throw new IllegalStateException("test");
}
@@ -2012,7 +2018,7 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl
@Controller
private static class ControllerWithErrorThrown {
@RequestMapping("")
@RequestMapping("/**")
public void myPath2(HttpServletResponse response) throws IOException {
throw new AssertionError("test");
}
@@ -2726,6 +2732,15 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl
}
}
@Controller
@RequestMapping // path intentionally omitted
static class UnmappedPathController {
@GetMapping // path intentionally omitted
public void get(@RequestParam(required = false) String id) {
}
}
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Controller
@@ -3586,7 +3601,7 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl
@Controller
static class HttpHeadersResponseController {
@RequestMapping(value = "", method = RequestMethod.POST)
@RequestMapping(value = "/*", method = RequestMethod.POST)
@ResponseStatus(HttpStatus.CREATED)
public HttpHeaders create() throws URISyntaxException {
HttpHeaders headers = new HttpHeaders();