diff --git a/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/requestheader.adoc b/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/requestheader.adoc index fa6304c61c6a..dee7cd0cfc3d 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/requestheader.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/requestheader.adoc @@ -63,3 +63,61 @@ TIP: Built-in support is available for converting a comma-separated string into array or collection of strings or other types known to the type conversion system. For example, a method parameter annotated with `@RequestHeader("Accept")` may be of type `String` but also of `String[]` or `List`. + +We can use `@RequestHeader` as a meta-annotation, so you can create custom annotations for repeatedly used headers, like the `Accept-Language` header. +The next example demonstrates how we could do this with an annotation named `@LanguageRequestHeader`: + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- +@Target({ElementType.PARAMETER, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@RequestHeader(value = HttpHeaders.ACCEPT_LANGUAGE, defaultValue = "en") +public @interface LanguageRequestHeader {} +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- +@Target(AnnotationTarget.VALUE_PARAMETER, AnnotationTarget.TYPE) +@Retention(AnnotationRetention.RUNTIME) +@MustBeDocumented +@RequestHeader(value = HttpHeaders.ACCEPT_LANGUAGE, defaultValue = "en") +annotation class LanguageRequestHeader +---- +====== + +Now that `@LanguageRequestHeader` has been specified, we can use it to signal Spring to resolve our `@LanguageRequestHeader` from the current `Accept-Language` request header: + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- +@GetMapping("/product") +public Flux fetchProduct(@LanguageRequestHeader String lang) { + + // .. fetch product and return them ... +} +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- +@GetMapping("/product") +fun fetchProduct(@LanguageRequestHeader lang: String): Flux { + + // .. fetch product and return them ... +} +---- +====== + + +TIP: We can replace the lang parameter type from `String` to `java.util.Locale` or to your own Language enum, and Spring will handle the conversion of the header value. You may need to implement the xref:core/validation/convert.adoc[`Converter`] interface and register it. diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/requestheader.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/requestheader.adoc index d6c00e5f24a8..47cf1f9f12b6 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/requestheader.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/requestheader.adoc @@ -63,3 +63,62 @@ TIP: Built-in support is available for converting a comma-separated string into array or collection of strings or other types known to the type conversion system. For example, a method parameter annotated with `@RequestHeader("Accept")` can be of type `String` but also `String[]` or `List`. + + +We can use `@RequestHeader` as a meta-annotation, so you can create custom annotations for repeatedly used headers, like the `Accept-Language` header. +The next example demonstrates how we could do this with an annotation named `@LanguageRequestHeader`: + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- +@Target({ElementType.PARAMETER, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@RequestHeader(value = HttpHeaders.ACCEPT_LANGUAGE, defaultValue = "en") +public @interface LanguageRequestHeader {} +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- +@Target(AnnotationTarget.VALUE_PARAMETER, AnnotationTarget.TYPE) +@Retention(AnnotationRetention.RUNTIME) +@MustBeDocumented +@RequestHeader(value = HttpHeaders.ACCEPT_LANGUAGE, defaultValue = "en") +annotation class LanguageRequestHeader +---- +====== + +Now that `@LanguageRequestHeader` has been specified, we can use it to signal Spring to resolve our `@LanguageRequestHeader` from the current `Accept-Language` request header: + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- +@GetMapping("/product") +public List fetchProduct(@LanguageRequestHeader String lang) { + + // .. fetch product and return them ... +} +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- +@GetMapping("/product") +fun fetchProduct(@LanguageRequestHeader lang: String): List { + + // .. fetch product and return them ... +} +---- +====== + + +TIP: We can replace the lang parameter type from `String` to `java.util.Locale` or to your own Language enum, and Spring will handle the conversion of the header value. You may need to implement the xref:core/validation/convert.adoc[`Converter`] interface and register it. diff --git a/spring-core/src/main/java/org/springframework/core/MethodParameter.java b/spring-core/src/main/java/org/springframework/core/MethodParameter.java index 1d5e2a484c3f..c320e2a81209 100644 --- a/spring-core/src/main/java/org/springframework/core/MethodParameter.java +++ b/spring-core/src/main/java/org/springframework/core/MethodParameter.java @@ -40,6 +40,8 @@ import kotlin.reflect.jvm.ReflectJvmMapping; import org.jspecify.annotations.Nullable; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.core.annotation.MergedAnnotations; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.ObjectUtils; @@ -650,6 +652,27 @@ public boolean hasParameterAnnotations() { return null; } + /** + * Return the parameter annotation of the given type, if available, + * either directly declared or as a meta-annotation. + * @param annotationType the annotation type to look for + * @return the annotation object, or {@code null} if not found + */ + public @Nullable A getParameterNestedAnnotation(Class annotationType) { + A annotation = getParameterAnnotation(annotationType); + if (annotation != null) { + return annotation; + } + Annotation[] annotationsToSearch = getParameterAnnotations(); + for (Annotation toSearch : annotationsToSearch) { + annotation = AnnotationUtils.findAnnotation(toSearch.annotationType(), annotationType); + if (annotation != null) { + return MergedAnnotations.from(toSearch).get(annotationType).synthesize(); + } + } + return null; + } + /** * Return whether the parameter is declared with the given annotation type. * @param annotationType the annotation type to look for @@ -659,6 +682,16 @@ public boolean hasParameterAnnotation(Class annotation return (getParameterAnnotation(annotationType) != null); } + /** + * Return whether the parameter is declared with the given annotation type, + * either directly or as a meta-annotation. + * @param annotationType the annotation type to look for + * @see #getParameterNestedAnnotation(Class) + */ + public boolean hasParameterNestedAnnotation(Class annotationType) { + return getParameterNestedAnnotation(annotationType) != null; + } + /** * Initialize parameter name discovery for this method parameter. *

This method does not actually try to retrieve the parameter name at diff --git a/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestHeader.java b/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestHeader.java index bfb8318dd180..a5168fbc3878 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestHeader.java +++ b/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestHeader.java @@ -27,7 +27,8 @@ /** * Annotation which indicates that a method parameter should be bound to a web request header. * - *

Supported for annotated handler methods in Spring MVC and Spring WebFlux. + *

Supported for declared as a meta-annotation or directly annotated handler methods + * in Spring MVC and Spring WebFlux. * *

If the method parameter is {@link java.util.Map Map<String, String>}, * {@link org.springframework.util.MultiValueMap MultiValueMap<String, String>}, @@ -41,7 +42,7 @@ * @see RequestParam * @see CookieValue */ -@Target(ElementType.PARAMETER) +@Target({ ElementType.PARAMETER, ElementType.ANNOTATION_TYPE }) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface RequestHeader { diff --git a/spring-web/src/main/java/org/springframework/web/method/annotation/HandlerMethodValidationException.java b/spring-web/src/main/java/org/springframework/web/method/annotation/HandlerMethodValidationException.java index 3de44e57f04b..2ad0fd1722df 100644 --- a/spring-web/src/main/java/org/springframework/web/method/annotation/HandlerMethodValidationException.java +++ b/spring-web/src/main/java/org/springframework/web/method/annotation/HandlerMethodValidationException.java @@ -155,7 +155,7 @@ public void visitResults(Visitor visitor) { } continue; } - RequestHeader requestHeader = param.getParameterAnnotation(RequestHeader.class); + RequestHeader requestHeader = param.getParameterNestedAnnotation(RequestHeader.class); if (requestHeader != null) { visitor.requestHeader(requestHeader, result); continue; diff --git a/spring-web/src/main/java/org/springframework/web/method/annotation/RequestHeaderMethodArgumentResolver.java b/spring-web/src/main/java/org/springframework/web/method/annotation/RequestHeaderMethodArgumentResolver.java index ccab91237ae5..fe22636aa2d1 100644 --- a/spring-web/src/main/java/org/springframework/web/method/annotation/RequestHeaderMethodArgumentResolver.java +++ b/spring-web/src/main/java/org/springframework/web/method/annotation/RequestHeaderMethodArgumentResolver.java @@ -61,14 +61,14 @@ public RequestHeaderMethodArgumentResolver(@Nullable ConfigurableBeanFactory bea @Override public boolean supportsParameter(MethodParameter parameter) { - return (parameter.hasParameterAnnotation(RequestHeader.class) && + return (parameter.hasParameterNestedAnnotation(RequestHeader.class) && !Map.class.isAssignableFrom(parameter.nestedIfOptional().getNestedParameterType())) && !HttpHeaders.class.isAssignableFrom(parameter.nestedIfOptional().getNestedParameterType()); } @Override protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) { - RequestHeader ann = parameter.getParameterAnnotation(RequestHeader.class); + RequestHeader ann = parameter.getParameterNestedAnnotation(RequestHeader.class); Assert.state(ann != null, "No RequestHeader annotation"); return new RequestHeaderNamedValueInfo(ann); } diff --git a/spring-web/src/main/java/org/springframework/web/service/invoker/RequestHeaderArgumentResolver.java b/spring-web/src/main/java/org/springframework/web/service/invoker/RequestHeaderArgumentResolver.java index a675f0c3ce70..bd40edc3e7de 100644 --- a/spring-web/src/main/java/org/springframework/web/service/invoker/RequestHeaderArgumentResolver.java +++ b/spring-web/src/main/java/org/springframework/web/service/invoker/RequestHeaderArgumentResolver.java @@ -60,7 +60,7 @@ public RequestHeaderArgumentResolver(ConversionService conversionService) { @Override protected @Nullable NamedValueInfo createNamedValueInfo(MethodParameter parameter) { - RequestHeader annot = parameter.getParameterAnnotation(RequestHeader.class); + RequestHeader annot = parameter.getParameterNestedAnnotation(RequestHeader.class); return (annot == null ? null : new NamedValueInfo(annot.name(), annot.required(), annot.defaultValue(), "request header", true)); } diff --git a/spring-web/src/test/java/org/springframework/web/method/annotation/RequestHeaderMethodArgumentResolverTests.java b/spring-web/src/test/java/org/springframework/web/method/annotation/RequestHeaderMethodArgumentResolverTests.java index a0c338ae1e35..b2aeac98f064 100644 --- a/spring-web/src/test/java/org/springframework/web/method/annotation/RequestHeaderMethodArgumentResolverTests.java +++ b/spring-web/src/test/java/org/springframework/web/method/annotation/RequestHeaderMethodArgumentResolverTests.java @@ -16,6 +16,10 @@ package org.springframework.web.method.annotation; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; import java.lang.reflect.Method; import java.time.Instant; import java.time.format.DateTimeFormatter; @@ -69,6 +73,7 @@ class RequestHeaderMethodArgumentResolverTests { private MethodParameter paramUuid; private MethodParameter paramUuidOptional; private MethodParameter paramUuidPlaceholder; + private MethodParameter paramNestedAnnotation; private MockHttpServletRequest servletRequest; @@ -94,6 +99,7 @@ void setup() throws Exception { paramUuid = new SynthesizingMethodParameter(method, 9); paramUuidOptional = new SynthesizingMethodParameter(method, 10); paramUuidPlaceholder = new SynthesizingMethodParameter(method, 11); + paramNestedAnnotation = new SynthesizingMethodParameter(method, 12); servletRequest = new MockHttpServletRequest(); webRequest = new ServletWebRequest(servletRequest, new MockHttpServletResponse()); @@ -113,6 +119,8 @@ void supportsParameter() { assertThat(resolver.supportsParameter(paramNamedDefaultValueStringHeader)).as("String parameter not supported").isTrue(); assertThat(resolver.supportsParameter(paramNamedValueStringArray)).as("String array parameter not supported").isTrue(); assertThat(resolver.supportsParameter(paramNamedValueMap)).as("non-@RequestParam parameter supported").isFalse(); + assertThat(resolver.supportsParameter(paramNestedAnnotation)).as("String parameter with nested annotation not supported").isTrue(); + } @Test @@ -332,6 +340,16 @@ public void uuidPlaceholderConversionWithEmptyValue() { } } + @Test + void resolveStringNestedAnnotationArgument() throws Exception { + String expected = "foo"; + servletRequest.addHeader("name", expected); + + Object result = resolver.resolveArgument(paramNestedAnnotation, null, webRequest, null); + + assertThat(result).isEqualTo(expected); + } + void params( @RequestHeader(name = "name", defaultValue = "bar") String param1, @RequestHeader("name") String[] param2, @@ -344,7 +362,15 @@ void params( @RequestHeader("name") Instant instantParam, @RequestHeader("name") UUID uuid, @RequestHeader(name = "name", required = false) UUID uuidOptional, - @RequestHeader(name = "${systemProperty}") UUID uuidPlaceholder) { + @RequestHeader(name = "${systemProperty}") UUID uuidPlaceholder, + @NameRequestHeader String param7) { + } + + + @Target(ElementType.PARAMETER) + @Retention(RetentionPolicy.RUNTIME) + @RequestHeader(name = "name", defaultValue = "bar") + private @interface NameRequestHeader { } } diff --git a/spring-web/src/test/java/org/springframework/web/method/support/HandlerMethodValidationExceptionTests.java b/spring-web/src/test/java/org/springframework/web/method/support/HandlerMethodValidationExceptionTests.java index 952d410e4b3f..2ef411a396c5 100644 --- a/spring-web/src/test/java/org/springframework/web/method/support/HandlerMethodValidationExceptionTests.java +++ b/spring-web/src/test/java/org/springframework/web/method/support/HandlerMethodValidationExceptionTests.java @@ -17,6 +17,10 @@ package org.springframework.web.method.support; import java.lang.annotation.Annotation; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; import java.lang.reflect.Method; import java.util.Arrays; import java.util.List; @@ -68,7 +72,7 @@ class HandlerMethodValidationExceptionTests { private final HandlerMethod handlerMethod = handlerMethod(new ValidController(), - controller -> controller.handle(person, person, person, List.of(), person, "", "", "", "", "", "")); + controller -> controller.handle(person, person, person, List.of(), person, "", "", "", "", "", "", "")); private final TestVisitor visitor = new TestVisitor(); @@ -87,7 +91,7 @@ void traverse() { @ModelAttribute: modelAttribute1, @ModelAttribute: modelAttribute2, \ @RequestBody: requestBody, @RequestBody: requestBodyList, @RequestPart: requestPart, \ @RequestParam: requestParam1, @RequestParam: requestParam2, \ - @RequestHeader: header, @PathVariable: pathVariable, \ + @RequestHeader: header, @RequestHeader: nestedAnnotationHeader, @PathVariable: pathVariable, \ @CookieValue: cookie, @MatrixVariable: matrixVariable"""); } @@ -103,7 +107,7 @@ void traverseRemaining() { Other: modelAttribute1, @ModelAttribute: modelAttribute2, \ @RequestBody: requestBody, @RequestBody: requestBodyList, @RequestPart: requestPart, \ Other: requestParam1, @RequestParam: requestParam2, \ - @RequestHeader: header, @PathVariable: pathVariable, \ + @RequestHeader: header, @RequestHeader: nestedAnnotationHeader, @PathVariable: pathVariable, \ @CookieValue: cookie, @MatrixVariable: matrixVariable"""); } @@ -137,7 +141,6 @@ private static MethodValidationResult createMethodValidationResult(HandlerMethod } - @SuppressWarnings("unused") private record Person(@Size(min = 1, max = 10) String name) { @@ -161,11 +164,17 @@ void handle( @Size(min = 5) String requestParam1, @Size(min = 5) @RequestParam String requestParam2, @Size(min = 5) @RequestHeader String header, + @Size(min = 5) @HeaderRequestHeader String nestedAnnotationHeader, @Size(min = 5) @PathVariable String pathVariable, @Size(min = 5) @CookieValue String cookie, @Size(min = 5) @MatrixVariable String matrixVariable) { } + @Target(ElementType.PARAMETER) + @Retention(RetentionPolicy.RUNTIME) + @RequestHeader(name = "header") + private @interface HeaderRequestHeader { + } } diff --git a/spring-web/src/test/java/org/springframework/web/service/invoker/RequestHeaderArgumentResolverTests.java b/spring-web/src/test/java/org/springframework/web/service/invoker/RequestHeaderArgumentResolverTests.java index b93f74e256b1..ee44d195fbed 100644 --- a/spring-web/src/test/java/org/springframework/web/service/invoker/RequestHeaderArgumentResolverTests.java +++ b/spring-web/src/test/java/org/springframework/web/service/invoker/RequestHeaderArgumentResolverTests.java @@ -16,6 +16,10 @@ package org.springframework.web.service.invoker; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; import java.util.List; import org.junit.jupiter.api.Test; @@ -57,6 +61,12 @@ void doesNotOverrideAnnotationHeaders() { assertRequestHeaders("myHeader", "1", "2"); } + @Test + void doesNestedAnnotationNotOverrideAnnotationHeaders() { + this.service.executeWithAnnotationHeadersAndNestedAnnotation("2"); + assertRequestHeaders("myHeader", "1", "2"); + } + private void assertRequestHeaders(String key, String... values) { List actualValues = this.client.getRequestValues().getHeaders().get(key); if (ObjectUtils.isEmpty(values)) { @@ -76,6 +86,14 @@ private interface Service { @HttpExchange(method = "GET", headers = "myHeader=1") void executeWithAnnotationHeaders(@RequestHeader String myHeader); + @HttpExchange(method = "GET", headers = "myHeader=1") + void executeWithAnnotationHeadersAndNestedAnnotation(@MyHeader String myHeader); + } + @Target(ElementType.PARAMETER) + @Retention(RetentionPolicy.RUNTIME) + @RequestHeader(name = "myHeader") + private @interface MyHeader { + } } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/HandlerMethodArgumentResolverSupport.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/HandlerMethodArgumentResolverSupport.java index 7c5297f37f19..130f327a4fbd 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/HandlerMethodArgumentResolverSupport.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/HandlerMethodArgumentResolverSupport.java @@ -118,6 +118,31 @@ protected boolean checkAnnotatedParamNoReactiveWrapper( return false; } + return checkAnnotatedParamNoReactiveWrapperCommon(parameter, annotation, typePredicate); + } + + + /** + * Evaluate the {@code Predicate} on the method parameter type if it has the + * given annotation, either directly declared or as a meta-annotation, + * nesting within {@link java.util.Optional} if necessary, + * but raise an {@code IllegalStateException} if the same matches the generic + * type within a reactive type wrapper. + */ + protected boolean checkNestedAnnotatedParamNoReactiveWrapper( + MethodParameter parameter, Class annotationType, BiPredicate> typePredicate) { + + A annotation = parameter.getParameterNestedAnnotation(annotationType); + if (annotation == null) { + return false; + } + + return checkAnnotatedParamNoReactiveWrapperCommon(parameter, annotation, typePredicate); + } + + private boolean checkAnnotatedParamNoReactiveWrapperCommon( + MethodParameter parameter, A annotation, BiPredicate> typePredicate) { + parameter = parameter.nestedIfOptional(); Class type = parameter.getNestedParameterType(); diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestHeaderMethodArgumentResolver.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestHeaderMethodArgumentResolver.java index e117d3f86064..7c369dbc687d 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestHeaderMethodArgumentResolver.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestHeaderMethodArgumentResolver.java @@ -65,7 +65,7 @@ public RequestHeaderMethodArgumentResolver(@Nullable ConfigurableBeanFactory fac @Override public boolean supportsParameter(MethodParameter param) { - return checkAnnotatedParamNoReactiveWrapper(param, RequestHeader.class, this::singleParam); + return checkNestedAnnotatedParamNoReactiveWrapper(param, RequestHeader.class, this::singleParam); } private boolean singleParam(RequestHeader annotation, Class type) { @@ -74,7 +74,7 @@ private boolean singleParam(RequestHeader annotation, Class type) { @Override protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) { - RequestHeader ann = parameter.getParameterAnnotation(RequestHeader.class); + RequestHeader ann = parameter.getParameterNestedAnnotation(RequestHeader.class); Assert.state(ann != null, "No RequestHeader annotation"); return new RequestHeaderNamedValueInfo(ann); } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestHeaderMethodArgumentResolverTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestHeaderMethodArgumentResolverTests.java index 7d45ede9fc44..72a96787d397 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestHeaderMethodArgumentResolverTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestHeaderMethodArgumentResolverTests.java @@ -16,6 +16,10 @@ package org.springframework.web.reactive.result.method.annotation; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; import java.lang.reflect.Method; import java.time.Instant; import java.time.format.DateTimeFormatter; @@ -68,6 +72,7 @@ class RequestHeaderMethodArgumentResolverTests { private MethodParameter paramInstant; private MethodParameter paramMono; private MethodParameter primitivePlaceholderParam; + private MethodParameter paramWithNestedAnnotated; @BeforeEach @@ -92,6 +97,7 @@ void setup() throws Exception { this.paramInstant = new SynthesizingMethodParameter(method, 7); this.paramMono = new SynthesizingMethodParameter(method, 8); this.primitivePlaceholderParam = new SynthesizingMethodParameter(method, 9); + this.paramWithNestedAnnotated = new SynthesizingMethodParameter(method, 10); } @@ -103,6 +109,8 @@ void supportsParameter() { assertThatIllegalStateException() .isThrownBy(() -> this.resolver.supportsParameter(this.paramMono)) .withMessageStartingWith("RequestHeaderMethodArgumentResolver does not support reactive type wrapper"); + assertThat(resolver.supportsParameter(paramWithNestedAnnotated)).as("String parameter with nested annotated not supported").isTrue(); + } @Test @@ -265,6 +273,16 @@ void instantConversion() { assertThat(result).isEqualTo(Instant.from(DateTimeFormatter.RFC_1123_DATE_TIME.parse(rfc1123val))); } + @Test + void resolveStringWithNestedAnnotatedArgument() { + String expected = "foo"; + ServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/").header("name", expected)); + + Mono mono = this.resolver.resolveArgument( + this.paramWithNestedAnnotated, this.bindingContext, exchange); + + assertThat(mono.block()).isEqualTo(expected); + } @SuppressWarnings("unused") public void params( @@ -277,7 +295,14 @@ public void params( @RequestHeader("name") Date dateParam, @RequestHeader("name") Instant instantParam, @RequestHeader Mono alsoNotSupported, - @RequestHeader(value = "${systemProperty}", required = false) int primitivePlaceholderParam) { + @RequestHeader(value = "${systemProperty}", required = false) int primitivePlaceholderParam, + @NameRequestHeader String param6) { + } + + @Target(ElementType.PARAMETER) + @Retention(RetentionPolicy.RUNTIME) + @RequestHeader(name = "name", defaultValue = "bar") + private @interface NameRequestHeader { } }