Skip to content

Support multiple @RequestMapping annotations #32134

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
Expand Down Expand Up @@ -80,6 +81,7 @@
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Mapping
@Repeatable(RequestMappings.class)
@Reflective(ControllerMappingReflectiveProcessor.class)
public @interface RequestMapping {

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package org.springframework.web.bind.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* Container annotation that aggregates several {@link RequestMapping} annotations.
*
* <p>Can be used natively, declaring several nested {@link RequestMapping} annotations.
* Can also be used in conjunction with Java 8's support for repeatable annotations,
* where {@link RequestMapping} can simply be declared several times on the same method,
* implicitly generating this container annotation.
*
* @see RequestMapping
* @since 6.2
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestMappings {
RequestMapping[] value();
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import java.util.function.Function;
import java.util.stream.Collectors;

import org.springframework.lang.NonNull;
import reactor.core.publisher.Mono;

import org.springframework.aop.support.AopUtils;
Expand Down Expand Up @@ -168,7 +169,7 @@ public void afterPropertiesSet() {
/**
* Scan beans in the ApplicationContext, detect and register handler methods.
* @see #isHandler(Class)
* @see #getMappingForMethod(Method, Class)
* @see #getListMappingsForMethod(Method, Class)
* @see #handlerMethodsInitialized(Map)
*/
protected void initHandlerMethods() {
Expand Down Expand Up @@ -204,22 +205,24 @@ protected void detectHandlerMethods(final Object handler) {

if (handlerType != null) {
final Class<?> userType = ClassUtils.getUserClass(handlerType);
Map<Method, T> methods = MethodIntrospector.selectMethods(userType,
(MethodIntrospector.MetadataLookup<T>) method -> getMappingForMethod(method, userType));
Map<Method, List<T>> methods = MethodIntrospector.selectMethods(userType,
(MethodIntrospector.MetadataLookup<List<T>>) method -> getListMappingsForMethod(method, userType));
if (logger.isTraceEnabled()) {
logger.trace(formatMappings(userType, methods));
}
else if (mappingsLogger.isDebugEnabled()) {
mappingsLogger.debug(formatMappings(userType, methods));
}
methods.forEach((method, mapping) -> {
methods.forEach((method, mappings) -> {
Method invocableMethod = AopUtils.selectInvocableMethod(method, userType);
registerHandlerMethod(handler, invocableMethod, mapping);
for (T mapping : mappings) {
registerHandlerMethod(handler, invocableMethod, mapping);
}
});
}
}

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

/**
* Provide the mapping for a handler method. A method for which no
* Provide the list of mappings for a handler method. A method for which no
* mapping can be provided is not a handler method.
* @param method the method to provide a mapping for
* @param handlerType the handler type, possibly a subtype of the method's
* declaring class
* @return the mapping, or {@code null} if the method is not mapped
* @return the list of mappings, or an empty list if the method is not mapped
*/
@Nullable
protected abstract T getMappingForMethod(Method method, Class<?> handlerType);
@NonNull
protected abstract List<T> getListMappingsForMethod(Method method, Class<?> handlerType);

/**
* Return the request mapping paths that are not patterns.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
Expand All @@ -34,6 +35,7 @@
import org.springframework.core.annotation.MergedAnnotations;
import org.springframework.core.annotation.MergedAnnotations.SearchStrategy;
import org.springframework.core.annotation.RepeatableContainers;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Controller;
import org.springframework.util.Assert;
Expand Down Expand Up @@ -152,42 +154,59 @@ protected boolean isHandler(Class<?> beanType) {

/**
* Uses type-level and method-level {@link RequestMapping @RequestMapping}
* and {@link HttpExchange @HttpExchange} annotations to create the
* {@link RequestMappingInfo}.
* @return the created {@code RequestMappingInfo}, or {@code null} if the method
* and {@link HttpExchange @HttpExchange} annotations to create the list
* of {@link RequestMappingInfo}.
* @return the created list of {@code RequestMappingInfo}, or an empty list if the method
* does not have a {@code @RequestMapping} or {@code @HttpExchange} annotation
* @see #getCustomMethodCondition(Method)
* @see #getCustomTypeCondition(Class)
*/
@Override
@Nullable
protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
RequestMappingInfo info = createRequestMappingInfo(method);
if (info != null) {
RequestMappingInfo typeInfo = createRequestMappingInfo(handlerType);
if (typeInfo != null) {
info = typeInfo.combine(info);
@NonNull
protected List<RequestMappingInfo> getListMappingsForMethod(Method method, Class<?> handlerType) {
List<RequestMappingInfo> result = new ArrayList<>();
List<RequestMappingInfo> infos = buildListOfRequestMappingInfo(method);
if (!infos.isEmpty()) {
List<RequestMappingInfo> typeInfos = buildListOfRequestMappingInfo(handlerType);
if (!typeInfos.isEmpty()) {
List<RequestMappingInfo> requestMappingInfos = new ArrayList<>();
for (RequestMappingInfo info : infos) {
for (RequestMappingInfo typeInfo : typeInfos) {
requestMappingInfos.add(typeInfo.combine(info));
}
}
infos = requestMappingInfos;
}
if (info.getPatternsCondition().isEmptyPathMapping()) {
info = info.mutate().paths("", "/").options(this.config).build();
for (RequestMappingInfo info : infos) {
if (info.getPatternsCondition().isEmptyPathMapping()) {
info = info.mutate().paths("", "/").options(this.config).build();
}

result.add(info);
}
for (Map.Entry<String, Predicate<Class<?>>> entry : this.pathPrefixes.entrySet()) {
if (entry.getValue().test(handlerType)) {
String prefix = entry.getKey();
if (this.embeddedValueResolver != null) {
prefix = this.embeddedValueResolver.resolveStringValue(prefix);

for (int idx = 0; idx < result.size(); idx++) {
RequestMappingInfo info = result.get(idx);

for (Map.Entry<String, Predicate<Class<?>>> 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).options(this.config).build().combine(info);
result.set(idx, info);
break;
}
info = RequestMappingInfo.paths(prefix).options(this.config).build().combine(info);
break;
}
}
}
return info;
return Collections.unmodifiableList(result);
}

@Nullable
private RequestMappingInfo createRequestMappingInfo(AnnotatedElement element) {
RequestMappingInfo requestMappingInfo = null;
@NonNull
private List<RequestMappingInfo> buildListOfRequestMappingInfo(AnnotatedElement element) {
List<RequestMappingInfo> requestMappingInfos = new ArrayList<>();
RequestCondition<?> customCondition = (element instanceof Class<?> clazz ?
getCustomTypeCondition(clazz) : getCustomMethodCondition((Method) element));

Expand All @@ -200,22 +219,25 @@ private RequestMappingInfo createRequestMappingInfo(AnnotatedElement element) {
logger.warn("Multiple @RequestMapping annotations found on %s, but only the first will be used: %s"
.formatted(element, requestMappings));
}
requestMappingInfo = createRequestMappingInfo((RequestMapping) requestMappings.get(0).annotation, customCondition);

for (AnnotationDescriptor requestMapping : requestMappings) {
requestMappingInfos.add(createRequestMappingInfo((RequestMapping) requestMapping.annotation, customCondition));
}
}

List<AnnotationDescriptor> httpExchanges = descriptors.stream()
.filter(desc -> desc.annotation instanceof HttpExchange).toList();
if (!httpExchanges.isEmpty()) {
Assert.state(requestMappingInfo == null,
Assert.state(requestMappings.isEmpty(),
() -> "%s is annotated with @RequestMapping and @HttpExchange annotations, but only one is allowed: %s"
.formatted(element, Stream.of(requestMappings, httpExchanges).flatMap(List::stream).toList()));
Assert.state(httpExchanges.size() == 1,
() -> "Multiple @HttpExchange annotations found on %s, but only one is allowed: %s"
.formatted(element, httpExchanges));
requestMappingInfo = createRequestMappingInfo((HttpExchange) httpExchanges.get(0).annotation, customCondition);

for (AnnotationDescriptor httpExchange : httpExchanges) {
requestMappingInfos.add(createRequestMappingInfo((HttpExchange) httpExchange.annotation, customCondition));
}
}

return requestMappingInfo;
return Collections.unmodifiableList(requestMappingInfos);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.lang.NonNull;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;

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

@Override
protected String getMappingForMethod(Method method, Class<?> handlerType) {
@NonNull
protected List<String> getListMappingsForMethod(Method method, Class<?> handlerType) {
String methodName = method.getName();
return methodName.startsWith("handler") ? methodName : null;
return methodName.startsWith("handler") ? Collections.singletonList(methodName) : Collections.emptyList();
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.lang.NonNull;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;

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

@Override
protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
@NonNull
protected List<RequestMappingInfo> getListMappingsForMethod(Method method, Class<?> handlerType) {
List<RequestMappingInfo> results = new ArrayList<>();
RequestMapping annot = AnnotatedElementUtils.findMergedAnnotation(method, RequestMapping.class);
if (annot != null) {
BuilderConfiguration options = new BuilderConfiguration();
options.setPatternParser(getPathPatternParser());
return paths(annot.value()).methods(annot.method())
results.add(paths(annot.value()).methods(annot.method())
.params(annot.params()).headers(annot.headers())
.consumes(annot.consumes()).produces(annot.produces())
.options(options).build();
}
else {
return null;
.options(options).build());
}

return results;
}
}

Expand Down
Loading