diff --git a/framework-docs/modules/ROOT/pages/integration/rest-clients.adoc b/framework-docs/modules/ROOT/pages/integration/rest-clients.adoc index 482217e6d62d..938fb04e1225 100644 --- a/framework-docs/modules/ROOT/pages/integration/rest-clients.adoc +++ b/framework-docs/modules/ROOT/pages/integration/rest-clients.adoc @@ -937,6 +937,9 @@ method parameters: | `HttpMethod` | Dynamically set the HTTP method for the request, overriding the annotation's `method` attribute +| `HttpHeaders` +| Add request headers. This does not override the annotation's `headers` attribute. + | `@RequestHeader` | Add a request header or multiple headers. The argument may be a `Map` or `MultiValueMap` with multiple headers, a `Collection` of values, or an diff --git a/spring-web/src/main/java/org/springframework/web/service/invoker/HttpHeadersArgumentResolver.java b/spring-web/src/main/java/org/springframework/web/service/invoker/HttpHeadersArgumentResolver.java new file mode 100644 index 000000000000..7540c2e072f8 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/service/invoker/HttpHeadersArgumentResolver.java @@ -0,0 +1,65 @@ +/* + * Copyright 2002-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.service.invoker; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; +import org.springframework.core.MethodParameter; +import org.springframework.http.HttpHeaders; +import org.springframework.util.Assert; + +import java.util.Optional; + +/** + * {@link HttpServiceArgumentResolver} that resolves the target + * request's HTTP headers from an {@link HttpHeaders} argument. + * + * @author Yanming Zhou + */ +public class HttpHeadersArgumentResolver implements HttpServiceArgumentResolver { + + private static final Log logger = LogFactory.getLog(HttpHeadersArgumentResolver.class); + + + @Override + public boolean resolve( + @Nullable Object argument, MethodParameter parameter, HttpRequestValues.Builder requestValues) { + + parameter = parameter.nestedIfOptional(); + + if (!parameter.getNestedParameterType().equals(HttpHeaders.class)) { + return false; + } + + if (argument instanceof Optional optionalValue) { + argument = optionalValue.orElse(null); + } + + if (argument == null) { + Assert.isTrue(parameter.isOptional(), "HttpHeaders is required"); + return true; + } + + ((HttpHeaders) argument).forEach((name, values) -> { + requestValues.addHeader(name, values.toArray(new String[0])); + }); + + return true; + } + +} diff --git a/spring-web/src/main/java/org/springframework/web/service/invoker/HttpServiceProxyFactory.java b/spring-web/src/main/java/org/springframework/web/service/invoker/HttpServiceProxyFactory.java index d064cafd76af..a72805b2afa2 100644 --- a/spring-web/src/main/java/org/springframework/web/service/invoker/HttpServiceProxyFactory.java +++ b/spring-web/src/main/java/org/springframework/web/service/invoker/HttpServiceProxyFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -47,6 +47,7 @@ * {@link Builder Builder}. * * @author Rossen Stoyanchev + * @author Yanming Zhou * @since 6.0 * @see org.springframework.web.client.support.RestClientAdapter * @see org.springframework.web.reactive.function.client.support.WebClientAdapter @@ -210,6 +211,7 @@ private List initArgumentResolvers() { resolvers.add(new UrlArgumentResolver()); resolvers.add(new UriBuilderFactoryArgumentResolver()); resolvers.add(new HttpMethodArgumentResolver()); + resolvers.add(new HttpHeadersArgumentResolver()); return resolvers; } diff --git a/spring-web/src/test/java/org/springframework/web/service/invoker/HttpHeadersArgumentResolverTests.java b/spring-web/src/test/java/org/springframework/web/service/invoker/HttpHeadersArgumentResolverTests.java new file mode 100644 index 000000000000..3eb85cd38967 --- /dev/null +++ b/spring-web/src/test/java/org/springframework/web/service/invoker/HttpHeadersArgumentResolverTests.java @@ -0,0 +1,73 @@ +/* + * Copyright 2002-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.service.invoker; + +import org.junit.jupiter.api.Test; + +import org.springframework.http.HttpHeaders; +import org.springframework.web.service.annotation.GetExchange; +import org.springframework.web.service.annotation.HttpExchange; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link HttpHeadersArgumentResolver}. + * + * @author Yanming Zhou + */ +class HttpHeadersArgumentResolverTests { + + private final TestExchangeAdapter client = new TestExchangeAdapter(); + + private final Service service = + HttpServiceProxyFactory.builderFor(this.client).build().createClient(Service.class); + + @Test + void headers() { + HttpHeaders headers = new HttpHeaders(); + headers.add("foo", "bar"); + headers.add("test", "testValue1"); + headers.add("test", "testValue2"); + this.service.execute(headers); + + HttpHeaders actualHeaders = this.client.getRequestValues().getHeaders(); + assertThat(actualHeaders.get("foo")).containsOnly("bar"); + assertThat(actualHeaders.get("test")).containsExactlyInAnyOrder("testValue1", "testValue2"); + } + + @Test + void doesNotOverrideAnnotationHeaders() { + HttpHeaders headers = new HttpHeaders(); + headers.add("foo", "bar"); + this.service.executeWithAnnotationHeaders(headers); + + HttpHeaders actualHeaders = this.client.getRequestValues().getHeaders(); + assertThat(actualHeaders.get("foo")).containsExactlyInAnyOrder("foo", "bar"); + assertThat(actualHeaders.get("bar")).containsOnly("bar"); + } + + private interface Service { + + @GetExchange + void execute(HttpHeaders headers); + + @HttpExchange(method = "GET", headers = {"foo=foo", "bar=bar"}) + void executeWithAnnotationHeaders(HttpHeaders headers); + + } + +}