Skip to content

Commit 72027b1

Browse files
committed
Log warning if @RequestMapping method has no explicit mapping
Commit c0b52d0 introduced support for throwing an exception if a @RequestMapping handler method in a Spring MVC controller was mapped to an empty path. This had negative side effects for applications that intentionally mapped to an empty path, potentially alongside a mapping to an explicit path for the same handler method. This commit addresses this by logging a warning (instead of throwing an exception) if a @RequestMapping method is mapped only to empty paths. This commit also introduces the same support for WebFlux-based @RequestMapping handler methods. Closes gh-22543
1 parent e4525cf commit 72027b1

File tree

7 files changed

+104
-29
lines changed

7 files changed

+104
-29
lines changed

spring-webflux/src/main/java/org/springframework/web/reactive/result/method/AbstractHandlerMethodMapping.java

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
import org.springframework.lang.Nullable;
4242
import org.springframework.util.Assert;
4343
import org.springframework.util.ClassUtils;
44+
import org.springframework.util.StringUtils;
4445
import org.springframework.web.cors.CorsConfiguration;
4546
import org.springframework.web.cors.reactive.CorsUtils;
4647
import org.springframework.web.method.HandlerMethod;
@@ -57,9 +58,10 @@
5758
*
5859
* @author Rossen Stoyanchev
5960
* @author Brian Clozel
61+
* @author Sam Brannen
6062
* @since 5.0
6163
* @param <T> the mapping for a {@link HandlerMethod} containing the conditions
62-
* needed to match the handler method to incoming request.
64+
* needed to match the handler method to an incoming request.
6365
*/
6466
public abstract class AbstractHandlerMethodMapping<T> extends AbstractHandlerMapping implements InitializingBean {
6567

@@ -207,8 +209,8 @@ protected void detectHandlerMethods(final Object handler) {
207209
if (logger.isTraceEnabled()) {
208210
logger.trace(formatMappings(userType, methods));
209211
}
210-
methods.forEach((key, mapping) -> {
211-
Method invocableMethod = AopUtils.selectInvocableMethod(key, userType);
212+
methods.forEach((method, mapping) -> {
213+
Method invocableMethod = AopUtils.selectInvocableMethod(method, userType);
212214
registerHandlerMethod(handler, invocableMethod, mapping);
213215
});
214216
}
@@ -412,6 +414,12 @@ protected CorsConfiguration getCorsConfiguration(Object handler, ServerWebExchan
412414
@Nullable
413415
protected abstract T getMappingForMethod(Method method, Class<?> handlerType);
414416

417+
/**
418+
* Extract and return the URL paths contained in the supplied mapping.
419+
* @since 5.2
420+
*/
421+
protected abstract Set<String> getMappingPathPatterns(T mapping);
422+
415423
/**
416424
* Check if a mapping matches the current request and return a (potentially
417425
* new) mapping with conditions relevant to the current request.
@@ -482,8 +490,7 @@ public void register(T mapping, Object handler, Method method) {
482490
this.readWriteLock.writeLock().lock();
483491
try {
484492
HandlerMethod handlerMethod = createHandlerMethod(handler, method);
485-
assertUniqueMethodMapping(handlerMethod, mapping);
486-
493+
validateMethodMapping(handlerMethod, mapping);
487494
this.mappingLookup.put(mapping, handlerMethod);
488495

489496
CorsConfiguration corsConfig = initCorsConfiguration(handler, method, mapping);
@@ -498,13 +505,23 @@ public void register(T mapping, Object handler, Method method) {
498505
}
499506
}
500507

501-
private void assertUniqueMethodMapping(HandlerMethod newHandlerMethod, T mapping) {
502-
HandlerMethod handlerMethod = this.mappingLookup.get(mapping);
503-
if (handlerMethod != null && !handlerMethod.equals(newHandlerMethod)) {
508+
private void validateMethodMapping(HandlerMethod handlerMethod, T mapping) {
509+
// Log a warning if the supplied mapping maps the supplied HandlerMethod
510+
// only to empty paths.
511+
if (logger.isWarnEnabled() && getMappingPathPatterns(mapping).stream().noneMatch(StringUtils::hasText)) {
512+
logger.warn(String.format(
513+
"Handler method '%s' in bean '%s' is not mapped to an explicit path. " +
514+
"If you wish to map to all paths, please map explicitly to \"/**\" or \"**\".",
515+
handlerMethod, handlerMethod.getBean()));
516+
}
517+
518+
// Assert that the supplied mapping is unique.
519+
HandlerMethod existingHandlerMethod = this.mappingLookup.get(mapping);
520+
if (existingHandlerMethod != null && !existingHandlerMethod.equals(handlerMethod)) {
504521
throw new IllegalStateException(
505-
"Ambiguous mapping. Cannot map '" + newHandlerMethod.getBean() + "' method \n" +
506-
newHandlerMethod + "\nto " + mapping + ": There is already '" +
507-
handlerMethod.getBean() + "' bean method\n" + handlerMethod + " mapped.");
522+
"Ambiguous mapping. Cannot map '" + handlerMethod.getBean() + "' method \n" +
523+
handlerMethod + "\nto " + mapping + ": There is already '" +
524+
existingHandlerMethod.getBean() + "' bean method\n" + existingHandlerMethod + " mapped.");
508525
}
509526
}
510527

spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerMapping.java

Lines changed: 14 additions & 1 deletion
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.
@@ -21,7 +21,9 @@
2121
import java.util.Collections;
2222
import java.util.LinkedHashMap;
2323
import java.util.Map;
24+
import java.util.Set;
2425
import java.util.function.Predicate;
26+
import java.util.stream.Collectors;
2527

2628
import org.springframework.context.EmbeddedValueResolverAware;
2729
import org.springframework.core.annotation.AnnotatedElementUtils;
@@ -41,13 +43,15 @@
4143
import org.springframework.web.reactive.result.condition.RequestCondition;
4244
import org.springframework.web.reactive.result.method.RequestMappingInfo;
4345
import org.springframework.web.reactive.result.method.RequestMappingInfoHandlerMapping;
46+
import org.springframework.web.util.pattern.PathPattern;
4447

4548
/**
4649
* An extension of {@link RequestMappingInfoHandlerMapping} that creates
4750
* {@link RequestMappingInfo} instances from class-level and method-level
4851
* {@link RequestMapping @RequestMapping} annotations.
4952
*
5053
* @author Rossen Stoyanchev
54+
* @author Sam Brannen
5155
* @since 5.0
5256
*/
5357
public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMapping
@@ -161,6 +165,15 @@ protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handler
161165
return info;
162166
}
163167

168+
/**
169+
* Get the URL path patterns associated with the supplied {@link RequestMappingInfo}.
170+
* @since 5.2
171+
*/
172+
@Override
173+
protected Set<String> getMappingPathPatterns(RequestMappingInfo info) {
174+
return info.getPatternsCondition().getPatterns().stream().map(PathPattern::getPatternString).collect(Collectors.toSet());
175+
}
176+
164177
/**
165178
* Delegates to {@link #createRequestMappingInfo(RequestMapping, RequestCondition)},
166179
* supplying the appropriate custom {@link RequestCondition} depending on whether

spring-webflux/src/test/java/org/springframework/web/reactive/result/method/HandlerMethodMappingTests.java

Lines changed: 10 additions & 1 deletion
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.
@@ -17,7 +17,9 @@
1717
package org.springframework.web.reactive.result.method;
1818

1919
import java.lang.reflect.Method;
20+
import java.util.Collections;
2021
import java.util.Comparator;
22+
import java.util.Set;
2123

2224
import org.hamcrest.Matchers;
2325
import org.junit.Before;
@@ -44,6 +46,7 @@
4446
* Unit tests for {@link AbstractHandlerMethodMapping}.
4547
*
4648
* @author Rossen Stoyanchev
49+
* @author Sam Brannen
4750
*/
4851
public class HandlerMethodMappingTests {
4952

@@ -155,6 +158,11 @@ protected String getMappingForMethod(Method method, Class<?> handlerType) {
155158
return methodName.startsWith("handler") ? methodName : null;
156159
}
157160

161+
@Override
162+
protected Set<String> getMappingPathPatterns(String mapping) {
163+
return Collections.emptySet();
164+
}
165+
158166
@Override
159167
protected String getMatchingMapping(String pattern, ServerWebExchange exchange) {
160168
PathContainer lookupPath = exchange.getRequest().getPath().pathWithinApplication();
@@ -182,4 +190,5 @@ public void handlerMethod1() {
182190
public void handlerMethod2() {
183191
}
184192
}
193+
185194
}

spring-webflux/src/test/java/org/springframework/web/reactive/result/method/RequestMappingInfoHandlerMappingTests.java

Lines changed: 9 additions & 1 deletion
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.
@@ -26,6 +26,7 @@
2626
import java.util.Map;
2727
import java.util.Set;
2828
import java.util.function.Consumer;
29+
import java.util.stream.Collectors;
2930

3031
import org.junit.Before;
3132
import org.junit.Test;
@@ -69,7 +70,9 @@
6970

7071
/**
7172
* Unit tests for {@link RequestMappingInfoHandlerMapping}.
73+
*
7274
* @author Rossen Stoyanchev
75+
* @author Sam Brannen
7376
*/
7477
public class RequestMappingInfoHandlerMappingTests {
7578

@@ -475,6 +478,11 @@ protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handler
475478
return null;
476479
}
477480
}
481+
482+
@Override
483+
protected Set<String> getMappingPathPatterns(RequestMappingInfo info) {
484+
return info.getPatternsCondition().getPatterns().stream().map(PathPattern::getPatternString).collect(Collectors.toSet());
485+
}
478486
}
479487

480488
}

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

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@
6262
* @author Sam Brannen
6363
* @since 3.1
6464
* @param <T> the mapping for a {@link HandlerMethod} containing the conditions
65-
* needed to match the handler method to incoming request.
65+
* needed to match the handler method to an incoming request.
6666
*/
6767
public abstract class AbstractHandlerMethodMapping<T> extends AbstractHandlerMapping implements InitializingBean {
6868

@@ -496,7 +496,7 @@ protected CorsConfiguration getCorsConfiguration(Object handler, HttpServletRequ
496496
protected abstract T getMappingForMethod(Method method, Class<?> handlerType);
497497

498498
/**
499-
* Extract and return the URL paths contained in a mapping.
499+
* Extract and return the URL paths contained in the supplied mapping.
500500
*/
501501
protected abstract Set<String> getMappingPathPatterns(T mapping);
502502

@@ -616,11 +616,11 @@ public void register(T mapping, Object handler, Method method) {
616616
}
617617

618618
private void validateMethodMapping(HandlerMethod handlerMethod, T mapping) {
619-
// Assert that the supplied mapping maps the supplied HandlerMethod
620-
// to explicit, non-empty paths.
621-
if (!getMappingPathPatterns(mapping).stream().allMatch(StringUtils::hasText)) {
622-
throw new IllegalStateException(String.format("Missing path mapping. " +
623-
"Handler method '%s' in bean '%s' must be mapped to a non-empty path. " +
619+
// Log a warning if the supplied mapping maps the supplied HandlerMethod
620+
// only to empty paths.
621+
if (logger.isWarnEnabled() && getMappingPathPatterns(mapping).stream().noneMatch(StringUtils::hasText)) {
622+
logger.warn(String.format(
623+
"Handler method '%s' in bean '%s' is not mapped to an explicit path. " +
624624
"If you wish to map to all paths, please map explicitly to \"/**\" or \"**\".",
625625
handlerMethod, handlerMethod.getBean()));
626626
}

spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/RequestMappingInfoHandlerMapping.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ protected RequestMappingInfoHandlerMapping() {
7575

7676

7777
/**
78-
* Get the URL path patterns associated with this {@link RequestMappingInfo}.
78+
* Get the URL path patterns associated with the supplied {@link RequestMappingInfo}.
7979
*/
8080
@Override
8181
protected Set<String> getMappingPathPatterns(RequestMappingInfo info) {

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

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -797,11 +797,28 @@ public void equivalentMappingsWithSameMethodName() {
797797
}
798798

799799
@Test
800-
public void unmappedPathMapping() {
801-
assertThatThrownBy(() -> initServletWithControllers(UnmappedPathController.class))
802-
.isInstanceOf(BeanCreationException.class)
803-
.hasCauseInstanceOf(IllegalStateException.class)
804-
.hasMessageContaining("Missing path mapping");
800+
public void unmappedPathMapping() throws Exception {
801+
initServletWithControllers(UnmappedPathController.class);
802+
803+
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/bogus-unmapped");
804+
MockHttpServletResponse response = new MockHttpServletResponse();
805+
getServlet().service(request, response);
806+
assertEquals("get", response.getContentAsString());
807+
}
808+
809+
@Test
810+
public void explicitAndEmptyPathsControllerMapping() throws Exception {
811+
initServletWithControllers(ExplicitAndEmptyPathsController.class);
812+
813+
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/");
814+
MockHttpServletResponse response = new MockHttpServletResponse();
815+
getServlet().service(request, response);
816+
assertEquals("get", response.getContentAsString());
817+
818+
request = new MockHttpServletRequest("GET", "");
819+
response = new MockHttpServletResponse();
820+
getServlet().service(request, response);
821+
assertEquals("get", response.getContentAsString());
805822
}
806823

807824
@Test
@@ -2733,11 +2750,22 @@ public void doGet(HttpServletRequest req, HttpServletResponse resp, @RequestPara
27332750
}
27342751

27352752
@Controller
2736-
@RequestMapping // path intentionally omitted
2753+
// @RequestMapping intentionally omitted
27372754
static class UnmappedPathController {
27382755

27392756
@GetMapping // path intentionally omitted
2740-
public void get(@RequestParam(required = false) String id) {
2757+
public void get(Writer writer) throws IOException {
2758+
writer.write("get");
2759+
}
2760+
}
2761+
2762+
@Controller
2763+
// @RequestMapping intentionally omitted
2764+
static class ExplicitAndEmptyPathsController {
2765+
2766+
@GetMapping({"/", ""})
2767+
public void get(Writer writer) throws IOException {
2768+
writer.write("get");
27412769
}
27422770
}
27432771

0 commit comments

Comments
 (0)