Skip to content

Commit c32c713

Browse files
Add meta-annotations support for @RequestHeader in spring-web (Servlets)
Signed-off-by: zakaria-shahen <zakaria-shahen@users.noreply.github.com>
1 parent dd8313f commit c32c713

File tree

8 files changed

+93
-10
lines changed

8 files changed

+93
-10
lines changed

spring-core/src/main/java/org/springframework/core/MethodParameter.java

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@
4040
import kotlin.reflect.jvm.ReflectJvmMapping;
4141
import org.jspecify.annotations.Nullable;
4242

43+
import org.springframework.core.annotation.AnnotationUtils;
44+
import org.springframework.core.annotation.MergedAnnotations;
4345
import org.springframework.util.Assert;
4446
import org.springframework.util.ClassUtils;
4547
import org.springframework.util.ObjectUtils;
@@ -650,6 +652,27 @@ public boolean hasParameterAnnotations() {
650652
return null;
651653
}
652654

655+
/**
656+
* Return the parameter annotation of the given type, if available,
657+
* either directly declared or as a meta-annotation.
658+
* @param annotationType the annotation type to look for
659+
* @return the annotation object, or {@code null} if not found
660+
*/
661+
public <A extends Annotation> @Nullable A getParameterNestedAnnotation(Class<A> annotationType) {
662+
A annotation = getParameterAnnotation(annotationType);
663+
if (annotation != null) {
664+
return annotation;
665+
}
666+
Annotation[] annotationsToSearch = getParameterAnnotations();
667+
for (Annotation toSearch : annotationsToSearch) {
668+
annotation = AnnotationUtils.findAnnotation(toSearch.annotationType(), annotationType);
669+
if (annotation != null) {
670+
return MergedAnnotations.from(toSearch).get(annotationType).synthesize();
671+
}
672+
}
673+
return null;
674+
}
675+
653676
/**
654677
* Return whether the parameter is declared with the given annotation type.
655678
* @param annotationType the annotation type to look for
@@ -659,6 +682,16 @@ public <A extends Annotation> boolean hasParameterAnnotation(Class<A> annotation
659682
return (getParameterAnnotation(annotationType) != null);
660683
}
661684

685+
/**
686+
* Return whether the parameter is declared with the given annotation type,
687+
* either directly or as a meta-annotation.
688+
* @param annotationType the annotation type to look for
689+
* @see #getParameterNestedAnnotation(Class)
690+
*/
691+
public <A extends Annotation> boolean hasParameterNestedAnnotation(Class<A> annotationType) {
692+
return getParameterNestedAnnotation(annotationType) != null;
693+
}
694+
662695
/**
663696
* Initialize parameter name discovery for this method parameter.
664697
* <p>This method does not actually try to retrieve the parameter name at

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
* @see RequestParam
4242
* @see CookieValue
4343
*/
44-
@Target(ElementType.PARAMETER)
44+
@Target({ ElementType.PARAMETER, ElementType.ANNOTATION_TYPE })
4545
@Retention(RetentionPolicy.RUNTIME)
4646
@Documented
4747
public @interface RequestHeader {

spring-web/src/main/java/org/springframework/web/method/annotation/HandlerMethodValidationException.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ public void visitResults(Visitor visitor) {
155155
}
156156
continue;
157157
}
158-
RequestHeader requestHeader = param.getParameterAnnotation(RequestHeader.class);
158+
RequestHeader requestHeader = param.getParameterNestedAnnotation(RequestHeader.class);
159159
if (requestHeader != null) {
160160
visitor.requestHeader(requestHeader, result);
161161
continue;

spring-web/src/main/java/org/springframework/web/method/annotation/RequestHeaderMethodArgumentResolver.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,14 +61,14 @@ public RequestHeaderMethodArgumentResolver(@Nullable ConfigurableBeanFactory bea
6161

6262
@Override
6363
public boolean supportsParameter(MethodParameter parameter) {
64-
return (parameter.hasParameterAnnotation(RequestHeader.class) &&
64+
return (parameter.hasParameterNestedAnnotation(RequestHeader.class) &&
6565
!Map.class.isAssignableFrom(parameter.nestedIfOptional().getNestedParameterType())) &&
6666
!HttpHeaders.class.isAssignableFrom(parameter.nestedIfOptional().getNestedParameterType());
6767
}
6868

6969
@Override
7070
protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) {
71-
RequestHeader ann = parameter.getParameterAnnotation(RequestHeader.class);
71+
RequestHeader ann = parameter.getParameterNestedAnnotation(RequestHeader.class);
7272
Assert.state(ann != null, "No RequestHeader annotation");
7373
return new RequestHeaderNamedValueInfo(ann);
7474
}

spring-web/src/main/java/org/springframework/web/service/invoker/RequestHeaderArgumentResolver.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ public RequestHeaderArgumentResolver(ConversionService conversionService) {
6060

6161
@Override
6262
protected @Nullable NamedValueInfo createNamedValueInfo(MethodParameter parameter) {
63-
RequestHeader annot = parameter.getParameterAnnotation(RequestHeader.class);
63+
RequestHeader annot = parameter.getParameterNestedAnnotation(RequestHeader.class);
6464
return (annot == null ? null :
6565
new NamedValueInfo(annot.name(), annot.required(), annot.defaultValue(), "request header", true));
6666
}

spring-web/src/test/java/org/springframework/web/method/annotation/RequestHeaderMethodArgumentResolverTests.java

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@
1616

1717
package org.springframework.web.method.annotation;
1818

19+
import java.lang.annotation.ElementType;
20+
import java.lang.annotation.Retention;
21+
import java.lang.annotation.RetentionPolicy;
22+
import java.lang.annotation.Target;
1923
import java.lang.reflect.Method;
2024
import java.time.Instant;
2125
import java.time.format.DateTimeFormatter;
@@ -69,6 +73,7 @@ class RequestHeaderMethodArgumentResolverTests {
6973
private MethodParameter paramUuid;
7074
private MethodParameter paramUuidOptional;
7175
private MethodParameter paramUuidPlaceholder;
76+
private MethodParameter paramNestedAnnotation;
7277

7378
private MockHttpServletRequest servletRequest;
7479

@@ -94,6 +99,7 @@ void setup() throws Exception {
9499
paramUuid = new SynthesizingMethodParameter(method, 9);
95100
paramUuidOptional = new SynthesizingMethodParameter(method, 10);
96101
paramUuidPlaceholder = new SynthesizingMethodParameter(method, 11);
102+
paramNestedAnnotation = new SynthesizingMethodParameter(method, 12);
97103

98104
servletRequest = new MockHttpServletRequest();
99105
webRequest = new ServletWebRequest(servletRequest, new MockHttpServletResponse());
@@ -113,6 +119,8 @@ void supportsParameter() {
113119
assertThat(resolver.supportsParameter(paramNamedDefaultValueStringHeader)).as("String parameter not supported").isTrue();
114120
assertThat(resolver.supportsParameter(paramNamedValueStringArray)).as("String array parameter not supported").isTrue();
115121
assertThat(resolver.supportsParameter(paramNamedValueMap)).as("non-@RequestParam parameter supported").isFalse();
122+
assertThat(resolver.supportsParameter(paramNestedAnnotation)).as("String parameter with nested annotation not supported").isTrue();
123+
116124
}
117125

118126
@Test
@@ -332,6 +340,16 @@ public void uuidPlaceholderConversionWithEmptyValue() {
332340
}
333341
}
334342

343+
@Test
344+
void resolveStringNestedAnnotationArgument() throws Exception {
345+
String expected = "foo";
346+
servletRequest.addHeader("name", expected);
347+
348+
Object result = resolver.resolveArgument(paramNestedAnnotation, null, webRequest, null);
349+
350+
assertThat(result).isEqualTo(expected);
351+
}
352+
335353
void params(
336354
@RequestHeader(name = "name", defaultValue = "bar") String param1,
337355
@RequestHeader("name") String[] param2,
@@ -344,7 +362,15 @@ void params(
344362
@RequestHeader("name") Instant instantParam,
345363
@RequestHeader("name") UUID uuid,
346364
@RequestHeader(name = "name", required = false) UUID uuidOptional,
347-
@RequestHeader(name = "${systemProperty}") UUID uuidPlaceholder) {
365+
@RequestHeader(name = "${systemProperty}") UUID uuidPlaceholder,
366+
@NameRequestHeader String param7) {
367+
}
368+
369+
370+
@Target(ElementType.PARAMETER)
371+
@Retention(RetentionPolicy.RUNTIME)
372+
@RequestHeader(name = "name", defaultValue = "bar")
373+
private @interface NameRequestHeader {
348374
}
349375

350376
}

spring-web/src/test/java/org/springframework/web/method/support/HandlerMethodValidationExceptionTests.java

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
package org.springframework.web.method.support;
1818

19-
import java.lang.annotation.Annotation;
19+
import java.lang.annotation.*;
2020
import java.lang.reflect.Method;
2121
import java.util.Arrays;
2222
import java.util.List;
@@ -68,7 +68,7 @@ class HandlerMethodValidationExceptionTests {
6868

6969

7070
private final HandlerMethod handlerMethod = handlerMethod(new ValidController(),
71-
controller -> controller.handle(person, person, person, List.of(), person, "", "", "", "", "", ""));
71+
controller -> controller.handle(person, person, person, List.of(), person, "", "", "", "", "", "", ""));
7272

7373
private final TestVisitor visitor = new TestVisitor();
7474

@@ -87,7 +87,7 @@ void traverse() {
8787
@ModelAttribute: modelAttribute1, @ModelAttribute: modelAttribute2, \
8888
@RequestBody: requestBody, @RequestBody: requestBodyList, @RequestPart: requestPart, \
8989
@RequestParam: requestParam1, @RequestParam: requestParam2, \
90-
@RequestHeader: header, @PathVariable: pathVariable, \
90+
@RequestHeader: header, @RequestHeader: nestedAnnotationHeader, @PathVariable: pathVariable, \
9191
@CookieValue: cookie, @MatrixVariable: matrixVariable""");
9292
}
9393

@@ -103,7 +103,7 @@ void traverseRemaining() {
103103
Other: modelAttribute1, @ModelAttribute: modelAttribute2, \
104104
@RequestBody: requestBody, @RequestBody: requestBodyList, @RequestPart: requestPart, \
105105
Other: requestParam1, @RequestParam: requestParam2, \
106-
@RequestHeader: header, @PathVariable: pathVariable, \
106+
@RequestHeader: header, @RequestHeader: nestedAnnotationHeader, @PathVariable: pathVariable, \
107107
@CookieValue: cookie, @MatrixVariable: matrixVariable""");
108108
}
109109

@@ -161,11 +161,17 @@ void handle(
161161
@Size(min = 5) String requestParam1,
162162
@Size(min = 5) @RequestParam String requestParam2,
163163
@Size(min = 5) @RequestHeader String header,
164+
@Size(min = 5) @HeaderRequestHeader String nestedAnnotationHeader,
164165
@Size(min = 5) @PathVariable String pathVariable,
165166
@Size(min = 5) @CookieValue String cookie,
166167
@Size(min = 5) @MatrixVariable String matrixVariable) {
167168
}
168169

170+
@Target(ElementType.PARAMETER)
171+
@Retention(RetentionPolicy.RUNTIME)
172+
@RequestHeader(name = "header")
173+
private @interface HeaderRequestHeader {
174+
}
169175
}
170176

171177

spring-web/src/test/java/org/springframework/web/service/invoker/RequestHeaderArgumentResolverTests.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@
1616

1717
package org.springframework.web.service.invoker;
1818

19+
import java.lang.annotation.ElementType;
20+
import java.lang.annotation.Retention;
21+
import java.lang.annotation.RetentionPolicy;
22+
import java.lang.annotation.Target;
1923
import java.util.List;
2024

2125
import org.junit.jupiter.api.Test;
@@ -57,6 +61,12 @@ void doesNotOverrideAnnotationHeaders() {
5761
assertRequestHeaders("myHeader", "1", "2");
5862
}
5963

64+
@Test
65+
void doesNestedAnnotationNotOverrideAnnotationHeaders() {
66+
this.service.executeWithAnnotationHeadersAndNestedAnnotation("2");
67+
assertRequestHeaders("myHeader", "1", "2");
68+
}
69+
6070
private void assertRequestHeaders(String key, String... values) {
6171
List<String> actualValues = this.client.getRequestValues().getHeaders().get(key);
6272
if (ObjectUtils.isEmpty(values)) {
@@ -76,6 +86,14 @@ private interface Service {
7686
@HttpExchange(method = "GET", headers = "myHeader=1")
7787
void executeWithAnnotationHeaders(@RequestHeader String myHeader);
7888

89+
@HttpExchange(method = "GET", headers = "myHeader=1")
90+
void executeWithAnnotationHeadersAndNestedAnnotation(@MyHeader String myHeader);
91+
7992
}
8093

94+
@Target(ElementType.PARAMETER)
95+
@Retention(RetentionPolicy.RUNTIME)
96+
@RequestHeader(name = "myHeader")
97+
private @interface MyHeader {
98+
}
8199
}

0 commit comments

Comments
 (0)