Skip to content

Commit

Permalink
Support multiple RequestMapping and HttpExchange annotations
Browse files Browse the repository at this point in the history
  • Loading branch information
youssef3wi committed Jan 29, 2024
1 parent 9c4b4ab commit e8b784f
Show file tree
Hide file tree
Showing 7 changed files with 223 additions and 114 deletions.
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

0 comments on commit e8b784f

Please sign in to comment.