Skip to content

Commit e8b784f

Browse files
committed
Support multiple RequestMapping and HttpExchange annotations
1 parent 9c4b4ab commit e8b784f

File tree

7 files changed

+223
-114
lines changed

7 files changed

+223
-114
lines changed

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import java.lang.annotation.Documented;
2020
import java.lang.annotation.ElementType;
21+
import java.lang.annotation.Repeatable;
2122
import java.lang.annotation.Retention;
2223
import java.lang.annotation.RetentionPolicy;
2324
import java.lang.annotation.Target;
@@ -80,6 +81,7 @@
8081
@Retention(RetentionPolicy.RUNTIME)
8182
@Documented
8283
@Mapping
84+
@Repeatable(RequestMappings.class)
8385
@Reflective(ControllerMappingReflectiveProcessor.class)
8486
public @interface RequestMapping {
8587

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package org.springframework.web.bind.annotation;
2+
3+
import java.lang.annotation.Documented;
4+
import java.lang.annotation.ElementType;
5+
import java.lang.annotation.Retention;
6+
import java.lang.annotation.RetentionPolicy;
7+
import java.lang.annotation.Target;
8+
9+
/**
10+
* Container annotation that aggregates several {@link RequestMapping} annotations.
11+
*
12+
* <p>Can be used natively, declaring several nested {@link RequestMapping} annotations.
13+
* Can also be used in conjunction with Java 8's support for repeatable annotations,
14+
* where {@link RequestMapping} can simply be declared several times on the same method,
15+
* implicitly generating this container annotation.
16+
*
17+
* @see RequestMapping
18+
* @since 6.2
19+
*/
20+
@Target({ElementType.TYPE, ElementType.METHOD})
21+
@Retention(RetentionPolicy.RUNTIME)
22+
@Documented
23+
public @interface RequestMappings {
24+
RequestMapping[] value();
25+
}

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

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import java.util.function.Function;
3232
import java.util.stream.Collectors;
3333

34+
import org.springframework.lang.NonNull;
3435
import reactor.core.publisher.Mono;
3536

3637
import org.springframework.aop.support.AopUtils;
@@ -168,7 +169,7 @@ public void afterPropertiesSet() {
168169
/**
169170
* Scan beans in the ApplicationContext, detect and register handler methods.
170171
* @see #isHandler(Class)
171-
* @see #getMappingForMethod(Method, Class)
172+
* @see #getListMappingsForMethod(Method, Class)
172173
* @see #handlerMethodsInitialized(Map)
173174
*/
174175
protected void initHandlerMethods() {
@@ -204,22 +205,24 @@ protected void detectHandlerMethods(final Object handler) {
204205

205206
if (handlerType != null) {
206207
final Class<?> userType = ClassUtils.getUserClass(handlerType);
207-
Map<Method, T> methods = MethodIntrospector.selectMethods(userType,
208-
(MethodIntrospector.MetadataLookup<T>) method -> getMappingForMethod(method, userType));
208+
Map<Method, List<T>> methods = MethodIntrospector.selectMethods(userType,
209+
(MethodIntrospector.MetadataLookup<List<T>>) method -> getListMappingsForMethod(method, userType));
209210
if (logger.isTraceEnabled()) {
210211
logger.trace(formatMappings(userType, methods));
211212
}
212213
else if (mappingsLogger.isDebugEnabled()) {
213214
mappingsLogger.debug(formatMappings(userType, methods));
214215
}
215-
methods.forEach((method, mapping) -> {
216+
methods.forEach((method, mappings) -> {
216217
Method invocableMethod = AopUtils.selectInvocableMethod(method, userType);
217-
registerHandlerMethod(handler, invocableMethod, mapping);
218+
for (T mapping : mappings) {
219+
registerHandlerMethod(handler, invocableMethod, mapping);
220+
}
218221
});
219222
}
220223
}
221224

222-
private String formatMappings(Class<?> userType, Map<Method, T> methods) {
225+
private String formatMappings(Class<?> userType, Map<Method, List<T>> methods) {
223226
String packageName = ClassUtils.getPackageName(userType);
224227
String formattedType = (StringUtils.hasText(packageName) ?
225228
Arrays.stream(packageName.split("\\."))
@@ -423,15 +426,15 @@ protected CorsConfiguration getCorsConfiguration(Object handler, ServerWebExchan
423426
protected abstract boolean isHandler(Class<?> beanType);
424427

425428
/**
426-
* Provide the mapping for a handler method. A method for which no
429+
* Provide the list of mappings for a handler method. A method for which no
427430
* mapping can be provided is not a handler method.
428431
* @param method the method to provide a mapping for
429432
* @param handlerType the handler type, possibly a subtype of the method's
430433
* declaring class
431-
* @return the mapping, or {@code null} if the method is not mapped
434+
* @return the list of mappings, or an empty list if the method is not mapped
432435
*/
433-
@Nullable
434-
protected abstract T getMappingForMethod(Method method, Class<?> handlerType);
436+
@NonNull
437+
protected abstract List<T> getListMappingsForMethod(Method method, Class<?> handlerType);
435438

436439
/**
437440
* Return the request mapping paths that are not patterns.

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

Lines changed: 52 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import java.lang.reflect.AnnotatedElement;
2121
import java.lang.reflect.Method;
2222
import java.lang.reflect.Parameter;
23+
import java.util.ArrayList;
2324
import java.util.Collections;
2425
import java.util.LinkedHashMap;
2526
import java.util.List;
@@ -34,6 +35,7 @@
3435
import org.springframework.core.annotation.MergedAnnotations;
3536
import org.springframework.core.annotation.MergedAnnotations.SearchStrategy;
3637
import org.springframework.core.annotation.RepeatableContainers;
38+
import org.springframework.lang.NonNull;
3739
import org.springframework.lang.Nullable;
3840
import org.springframework.stereotype.Controller;
3941
import org.springframework.util.Assert;
@@ -152,42 +154,59 @@ protected boolean isHandler(Class<?> beanType) {
152154

153155
/**
154156
* Uses type-level and method-level {@link RequestMapping @RequestMapping}
155-
* and {@link HttpExchange @HttpExchange} annotations to create the
156-
* {@link RequestMappingInfo}.
157-
* @return the created {@code RequestMappingInfo}, or {@code null} if the method
157+
* and {@link HttpExchange @HttpExchange} annotations to create the list
158+
* of {@link RequestMappingInfo}.
159+
* @return the created list of {@code RequestMappingInfo}, or an empty list if the method
158160
* does not have a {@code @RequestMapping} or {@code @HttpExchange} annotation
159161
* @see #getCustomMethodCondition(Method)
160162
* @see #getCustomTypeCondition(Class)
161163
*/
162164
@Override
163-
@Nullable
164-
protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
165-
RequestMappingInfo info = createRequestMappingInfo(method);
166-
if (info != null) {
167-
RequestMappingInfo typeInfo = createRequestMappingInfo(handlerType);
168-
if (typeInfo != null) {
169-
info = typeInfo.combine(info);
165+
@NonNull
166+
protected List<RequestMappingInfo> getListMappingsForMethod(Method method, Class<?> handlerType) {
167+
List<RequestMappingInfo> result = new ArrayList<>();
168+
List<RequestMappingInfo> infos = buildListOfRequestMappingInfo(method);
169+
if (!infos.isEmpty()) {
170+
List<RequestMappingInfo> typeInfos = buildListOfRequestMappingInfo(handlerType);
171+
if (!typeInfos.isEmpty()) {
172+
List<RequestMappingInfo> requestMappingInfos = new ArrayList<>();
173+
for (RequestMappingInfo info : infos) {
174+
for (RequestMappingInfo typeInfo : typeInfos) {
175+
requestMappingInfos.add(typeInfo.combine(info));
176+
}
177+
}
178+
infos = requestMappingInfos;
170179
}
171-
if (info.getPatternsCondition().isEmptyPathMapping()) {
172-
info = info.mutate().paths("", "/").options(this.config).build();
180+
for (RequestMappingInfo info : infos) {
181+
if (info.getPatternsCondition().isEmptyPathMapping()) {
182+
info = info.mutate().paths("", "/").options(this.config).build();
183+
}
184+
185+
result.add(info);
173186
}
174-
for (Map.Entry<String, Predicate<Class<?>>> entry : this.pathPrefixes.entrySet()) {
175-
if (entry.getValue().test(handlerType)) {
176-
String prefix = entry.getKey();
177-
if (this.embeddedValueResolver != null) {
178-
prefix = this.embeddedValueResolver.resolveStringValue(prefix);
187+
188+
for (int idx = 0; idx < result.size(); idx++) {
189+
RequestMappingInfo info = result.get(idx);
190+
191+
for (Map.Entry<String, Predicate<Class<?>>> entry : this.pathPrefixes.entrySet()) {
192+
if (entry.getValue().test(handlerType)) {
193+
String prefix = entry.getKey();
194+
if (this.embeddedValueResolver != null) {
195+
prefix = this.embeddedValueResolver.resolveStringValue(prefix);
196+
}
197+
info = RequestMappingInfo.paths(prefix).options(this.config).build().combine(info);
198+
result.set(idx, info);
199+
break;
179200
}
180-
info = RequestMappingInfo.paths(prefix).options(this.config).build().combine(info);
181-
break;
182201
}
183202
}
184203
}
185-
return info;
204+
return Collections.unmodifiableList(result);
186205
}
187206

188-
@Nullable
189-
private RequestMappingInfo createRequestMappingInfo(AnnotatedElement element) {
190-
RequestMappingInfo requestMappingInfo = null;
207+
@NonNull
208+
private List<RequestMappingInfo> buildListOfRequestMappingInfo(AnnotatedElement element) {
209+
List<RequestMappingInfo> requestMappingInfos = new ArrayList<>();
191210
RequestCondition<?> customCondition = (element instanceof Class<?> clazz ?
192211
getCustomTypeCondition(clazz) : getCustomMethodCondition((Method) element));
193212

@@ -200,22 +219,25 @@ private RequestMappingInfo createRequestMappingInfo(AnnotatedElement element) {
200219
logger.warn("Multiple @RequestMapping annotations found on %s, but only the first will be used: %s"
201220
.formatted(element, requestMappings));
202221
}
203-
requestMappingInfo = createRequestMappingInfo((RequestMapping) requestMappings.get(0).annotation, customCondition);
222+
223+
for (AnnotationDescriptor requestMapping : requestMappings) {
224+
requestMappingInfos.add(createRequestMappingInfo((RequestMapping) requestMapping.annotation, customCondition));
225+
}
204226
}
205227

206228
List<AnnotationDescriptor> httpExchanges = descriptors.stream()
207229
.filter(desc -> desc.annotation instanceof HttpExchange).toList();
208230
if (!httpExchanges.isEmpty()) {
209-
Assert.state(requestMappingInfo == null,
231+
Assert.state(requestMappings.isEmpty(),
210232
() -> "%s is annotated with @RequestMapping and @HttpExchange annotations, but only one is allowed: %s"
211233
.formatted(element, Stream.of(requestMappings, httpExchanges).flatMap(List::stream).toList()));
212-
Assert.state(httpExchanges.size() == 1,
213-
() -> "Multiple @HttpExchange annotations found on %s, but only one is allowed: %s"
214-
.formatted(element, httpExchanges));
215-
requestMappingInfo = createRequestMappingInfo((HttpExchange) httpExchanges.get(0).annotation, customCondition);
234+
235+
for (AnnotationDescriptor httpExchange : httpExchanges) {
236+
requestMappingInfos.add(createRequestMappingInfo((HttpExchange) httpExchange.annotation, customCondition));
237+
}
216238
}
217239

218-
return requestMappingInfo;
240+
return Collections.unmodifiableList(requestMappingInfos);
219241
}
220242

221243
/**

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525

2626
import org.junit.jupiter.api.BeforeEach;
2727
import org.junit.jupiter.api.Test;
28+
import org.springframework.lang.NonNull;
2829
import reactor.core.publisher.Mono;
2930
import reactor.test.StepVerifier;
3031

@@ -200,9 +201,10 @@ protected boolean isHandler(Class<?> beanType) {
200201
}
201202

202203
@Override
203-
protected String getMappingForMethod(Method method, Class<?> handlerType) {
204+
@NonNull
205+
protected List<String> getListMappingsForMethod(Method method, Class<?> handlerType) {
204206
String methodName = method.getName();
205-
return methodName.startsWith("handler") ? methodName : null;
207+
return methodName.startsWith("handler") ? Collections.singletonList(methodName) : Collections.emptyList();
206208
}
207209

208210
@Override

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

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929

3030
import org.junit.jupiter.api.BeforeEach;
3131
import org.junit.jupiter.api.Test;
32+
import org.springframework.lang.NonNull;
3233
import reactor.core.publisher.Mono;
3334
import reactor.test.StepVerifier;
3435

@@ -523,19 +524,20 @@ protected boolean isHandler(Class<?> beanType) {
523524
}
524525

525526
@Override
526-
protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
527+
@NonNull
528+
protected List<RequestMappingInfo> getListMappingsForMethod(Method method, Class<?> handlerType) {
529+
List<RequestMappingInfo> results = new ArrayList<>();
527530
RequestMapping annot = AnnotatedElementUtils.findMergedAnnotation(method, RequestMapping.class);
528531
if (annot != null) {
529532
BuilderConfiguration options = new BuilderConfiguration();
530533
options.setPatternParser(getPathPatternParser());
531-
return paths(annot.value()).methods(annot.method())
534+
results.add(paths(annot.value()).methods(annot.method())
532535
.params(annot.params()).headers(annot.headers())
533536
.consumes(annot.consumes()).produces(annot.produces())
534-
.options(options).build();
535-
}
536-
else {
537-
return null;
537+
.options(options).build());
538538
}
539+
540+
return results;
539541
}
540542
}
541543

0 commit comments

Comments
 (0)