Skip to content

Commit 3ad0eba

Browse files
committed
Add HttpHeaders argument resolver for HTTP service
Signed-off-by: Yanming Zhou <[email protected]>
1 parent 6873427 commit 3ad0eba

File tree

4 files changed

+143
-0
lines changed

4 files changed

+143
-0
lines changed

framework-docs/modules/ROOT/pages/integration/rest-clients.adoc

+3
Original file line numberDiff line numberDiff line change
@@ -937,6 +937,9 @@ method parameters:
937937
| `HttpMethod`
938938
| Dynamically set the HTTP method for the request, overriding the annotation's `method` attribute
939939

940+
| `HttpHeaders`
941+
| Add request headers. Header values are added and do not override already added header values.
942+
940943
| `@RequestHeader`
941944
| Add a request header or multiple headers. The argument may be a single value,
942945
a `Collection<?>` of values, `Map<String, ?>`,`MultiValueMap<String, ?>`.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/*
2+
* Copyright 2002-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.web.service.invoker;
18+
19+
import java.util.Optional;
20+
21+
import org.apache.commons.logging.Log;
22+
import org.apache.commons.logging.LogFactory;
23+
import org.jspecify.annotations.Nullable;
24+
25+
import org.springframework.core.MethodParameter;
26+
import org.springframework.http.HttpHeaders;
27+
import org.springframework.util.Assert;
28+
29+
/**
30+
* {@link HttpServiceArgumentResolver} that resolves the target
31+
* request's HTTP headers from an {@link HttpHeaders} argument.
32+
*
33+
* @author Yanming Zhou
34+
*/
35+
public class HttpHeadersArgumentResolver implements HttpServiceArgumentResolver {
36+
37+
private static final Log logger = LogFactory.getLog(HttpHeadersArgumentResolver.class);
38+
39+
40+
@Override
41+
public boolean resolve(
42+
@Nullable Object argument, MethodParameter parameter, HttpRequestValues.Builder requestValues) {
43+
44+
parameter = parameter.nestedIfOptional();
45+
46+
if (!parameter.getNestedParameterType().equals(HttpHeaders.class)) {
47+
return false;
48+
}
49+
50+
if (argument instanceof Optional<?> optionalValue) {
51+
argument = optionalValue.orElse(null);
52+
}
53+
54+
if (argument == null) {
55+
Assert.isTrue(parameter.isOptional(), "HttpHeaders is required");
56+
return true;
57+
}
58+
59+
((HttpHeaders) argument).forEach((name, values) ->
60+
requestValues.addHeader(name, values.toArray(new String[0])));
61+
62+
return true;
63+
}
64+
65+
}

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

+2
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
* {@link Builder Builder}.
4848
*
4949
* @author Rossen Stoyanchev
50+
* @author Yanming Zhou
5051
* @since 6.0
5152
* @see org.springframework.web.client.support.RestClientAdapter
5253
* @see org.springframework.web.reactive.function.client.support.WebClientAdapter
@@ -210,6 +211,7 @@ private List<HttpServiceArgumentResolver> initArgumentResolvers() {
210211
resolvers.add(new UrlArgumentResolver());
211212
resolvers.add(new UriBuilderFactoryArgumentResolver());
212213
resolvers.add(new HttpMethodArgumentResolver());
214+
resolvers.add(new HttpHeadersArgumentResolver());
213215

214216
return resolvers;
215217
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/*
2+
* Copyright 2002-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.web.service.invoker;
18+
19+
import org.junit.jupiter.api.Test;
20+
21+
import org.springframework.http.HttpHeaders;
22+
import org.springframework.web.service.annotation.GetExchange;
23+
import org.springframework.web.service.annotation.HttpExchange;
24+
25+
import static org.assertj.core.api.Assertions.assertThat;
26+
27+
/**
28+
* Tests for {@link HttpHeadersArgumentResolver}.
29+
*
30+
* @author Yanming Zhou
31+
*/
32+
class HttpHeadersArgumentResolverTests {
33+
34+
private final TestExchangeAdapter client = new TestExchangeAdapter();
35+
36+
private final Service service =
37+
HttpServiceProxyFactory.builderFor(this.client).build().createClient(Service.class);
38+
39+
@Test
40+
void headers() {
41+
HttpHeaders headers = new HttpHeaders();
42+
headers.add("foo", "bar");
43+
headers.add("test", "testValue1");
44+
headers.add("test", "testValue2");
45+
this.service.execute(headers);
46+
47+
HttpHeaders actualHeaders = this.client.getRequestValues().getHeaders();
48+
assertThat(actualHeaders.get("foo")).containsOnly("bar");
49+
assertThat(actualHeaders.get("test")).containsExactlyInAnyOrder("testValue1", "testValue2");
50+
}
51+
52+
@Test
53+
void doesNotOverrideAnnotationHeaders() {
54+
HttpHeaders headers = new HttpHeaders();
55+
headers.add("foo", "bar");
56+
this.service.executeWithAnnotationHeaders(headers);
57+
58+
HttpHeaders actualHeaders = this.client.getRequestValues().getHeaders();
59+
assertThat(actualHeaders.get("foo")).containsExactlyInAnyOrder("foo", "bar");
60+
assertThat(actualHeaders.get("bar")).containsOnly("bar");
61+
}
62+
63+
private interface Service {
64+
65+
@GetExchange
66+
void execute(HttpHeaders headers);
67+
68+
@HttpExchange(method = "GET", headers = {"foo=foo", "bar=bar"})
69+
void executeWithAnnotationHeaders(HttpHeaders headers);
70+
71+
}
72+
73+
}

0 commit comments

Comments
 (0)