Skip to content

Commit 268f3c8

Browse files
OlgaMaciaszekrstoyanchev
authored andcommitted
Add RestTemplate support for HTTP interface client
See gh-30117
1 parent bf82ed7 commit 268f3c8

File tree

21 files changed

+1271
-226
lines changed

21 files changed

+1271
-226
lines changed

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

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -382,12 +382,24 @@ One, declare an interface with `@HttpExchange` methods:
382382
}
383383
----
384384

385-
Two, create a proxy that will perform the declared HTTP exchanges:
385+
Two, create a proxy that will perform the declared HTTP exchanges,
386+
either using `WebClient`:
386387

387388
[source,java,indent=0,subs="verbatim,quotes"]
388389
----
389390
WebClient client = WebClient.builder().baseUrl("https://api.github.com/").build();
390-
HttpServiceProxyFactory factory = HttpServiceProxyFactory.builder(WebClientAdapter.forClient(client)).build();
391+
HttpServiceProxyFactory factory = HttpServiceProxyFactory.builder().exchangeAdapter(WebClientAdapter.forClient(webClient)).build();
392+
393+
RepositoryService service = factory.createClient(RepositoryService.class);
394+
----
395+
396+
or using `RestTemplate`:
397+
398+
[source,java,indent=0,subs="verbatim,quotes"]
399+
----
400+
RestTemplate restTemplate = new RestTemplate();
401+
restTemplate.setUriTemplateHandler(new DefaultUriBuilderFactory("https://api.github.com/"));
402+
HttpServiceProxyFactory factory = HttpServiceProxyFactory.builder().exchangeAdapter(RestTemplateAdapter.forTemplate(restTemplate)).build();
391403
392404
RepositoryService service = factory.createClient(RepositoryService.class);
393405
----
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
/*
2+
* Copyright 2002-2023 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.client.support;
18+
19+
import java.net.URI;
20+
import java.util.List;
21+
import java.util.stream.Collectors;
22+
23+
import org.springframework.core.ParameterizedTypeReference;
24+
import org.springframework.http.HttpCookie;
25+
import org.springframework.http.HttpHeaders;
26+
import org.springframework.http.HttpMethod;
27+
import org.springframework.http.RequestEntity;
28+
import org.springframework.http.ResponseEntity;
29+
import org.springframework.util.Assert;
30+
import org.springframework.util.LinkedMultiValueMap;
31+
import org.springframework.util.MultiValueMap;
32+
import org.springframework.web.client.RestTemplate;
33+
import org.springframework.web.service.invoker.HttpExchangeAdapter;
34+
import org.springframework.web.service.invoker.HttpRequestValues;
35+
import org.springframework.web.service.invoker.HttpServiceProxyFactory;
36+
37+
/**
38+
* An {@link HttpExchangeAdapter} that enables an {@link HttpServiceProxyFactory} to use
39+
* {@link RestTemplate} for request execution.
40+
* <p>
41+
* Use static factory methods in this class to create an {@link HttpServiceProxyFactory}
42+
* configured with a given {@link RestTemplate}.
43+
*
44+
* @author Olga Maciaszek-Sharma
45+
* @since 6.1
46+
*/
47+
public final class RestTemplateAdapter implements HttpExchangeAdapter {
48+
49+
private final RestTemplate restTemplate;
50+
51+
// Private constructor; use static factory methods to instantiate
52+
private RestTemplateAdapter(RestTemplate restTemplate) {
53+
this.restTemplate = restTemplate;
54+
}
55+
56+
@Override
57+
public Void exchange(HttpRequestValues requestValues) {
58+
this.restTemplate.exchange(newRequest(requestValues), Void.class);
59+
return null;
60+
}
61+
62+
@Override
63+
public HttpHeaders exchangeForHeaders(HttpRequestValues requestValues) {
64+
return this.restTemplate.exchange(newRequest(requestValues), Void.class).getHeaders();
65+
}
66+
67+
@Override
68+
public <T> T exchangeForBody(HttpRequestValues requestValues, ParameterizedTypeReference<T> bodyType) {
69+
return this.restTemplate.exchange(newRequest(requestValues), bodyType).getBody();
70+
}
71+
72+
@Override
73+
public ResponseEntity<Void> exchangeForBodilessEntity(HttpRequestValues requestValues) {
74+
return this.restTemplate.exchange(newRequest(requestValues), Void.class);
75+
}
76+
77+
@Override
78+
public <T> ResponseEntity<T> exchangeForEntity(HttpRequestValues requestValues,
79+
ParameterizedTypeReference<T> bodyType) {
80+
return this.restTemplate.exchange(newRequest(requestValues), bodyType);
81+
}
82+
83+
@Override
84+
public boolean supportsRequestAttributes() {
85+
return false;
86+
}
87+
88+
private RequestEntity<?> newRequest(HttpRequestValues requestValues) {
89+
URI uri;
90+
if (requestValues.getUri() != null) {
91+
uri = requestValues.getUri();
92+
}
93+
else if (requestValues.getUriTemplate() != null) {
94+
uri = this.restTemplate.getUriTemplateHandler().expand(requestValues.getUriTemplate(),
95+
requestValues.getUriVariables());
96+
}
97+
else {
98+
throw new IllegalStateException("Neither full URL nor URI template");
99+
}
100+
101+
HttpMethod httpMethod = requestValues.getHttpMethod();
102+
Assert.notNull(httpMethod, "HttpMethod is required");
103+
104+
RequestEntity.BodyBuilder builder = RequestEntity.method(httpMethod, uri)
105+
.headers(requestValues.getHeaders());
106+
107+
if (!requestValues.getCookies().isEmpty()) {
108+
MultiValueMap<String, HttpCookie> cookies = new LinkedMultiValueMap<>();
109+
requestValues.getCookies()
110+
.forEach((name, values) -> values.forEach(value ->
111+
cookies.add(name, new HttpCookie(name, value))));
112+
113+
builder.header(HttpHeaders.COOKIE,
114+
cookies.values()
115+
.stream()
116+
.flatMap(List::stream)
117+
.map(HttpCookie::toString)
118+
.collect(Collectors.joining("; ")));
119+
}
120+
121+
if (requestValues.getBodyValue() != null) {
122+
return builder.body(requestValues.getBodyValue());
123+
}
124+
return builder.build();
125+
}
126+
127+
/**
128+
* Create a {@link RestTemplateAdapter} for the given {@link RestTemplate} instance.
129+
* @param restTemplate the {@link RestTemplate} to use
130+
* @return the created adapter instance
131+
*/
132+
public static RestTemplateAdapter forTemplate(RestTemplate restTemplate) {
133+
return new RestTemplateAdapter(restTemplate);
134+
}
135+
136+
}

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

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@
2929
import org.springframework.util.Assert;
3030

3131
/**
32+
* A base reactive adapter that implements both {@link HttpClientAdapter}
33+
* and {@link HttpExchangeAdapter}. Allows to ensure backwards compatibility
34+
* with the deprecated {@link HttpClientAdapter} and handles blocking from reactive
35+
* publishers to objects where necessary.
3236
*
3337
* @author Rossen Stoyanchev
3438
* @since 6.1
@@ -51,48 +55,48 @@ protected AbstractReactorHttpExchangeAdapter() {
5155

5256

5357
/**
54-
*
55-
* @param reactiveAdapterRegistry
58+
* Configure the registry for adapting various reactive types.
59+
* <p>By default this is an instance of {@link ReactiveAdapterRegistry} with
60+
* default settings.
5661
*/
5762
public void setReactiveAdapterRegistry(ReactiveAdapterRegistry reactiveAdapterRegistry) {
5863
this.reactiveAdapterRegistry = reactiveAdapterRegistry;
5964
}
6065

6166
/**
62-
*
63-
* @return
67+
* Return the configured reactive type registry of adapters.
6468
*/
6569
@Override
6670
public ReactiveAdapterRegistry getReactiveAdapterRegistry() {
6771
return this.reactiveAdapterRegistry;
6872
}
6973

7074
/**
71-
*
72-
* @param blockTimeout
75+
* Configure how long to block for the response of an HTTP service method with a
76+
* synchronous (blocking) method signature.
77+
* <p>
78+
* By default, this is not set, in which case the behavior depends on connection and
79+
* request timeout settings of the underlying HTTP client. We recommend configuring
80+
* timeout values directly on the underlying HTTP client, which provides more
81+
* control over such settings.
7382
*/
7483
public void setBlockTimeout(@Nullable Duration blockTimeout) {
7584
this.blockTimeout = blockTimeout;
7685
}
7786

78-
/**
79-
*
80-
* @return
81-
*/
8287
@Override
8388
@Nullable
8489
public Duration getBlockTimeout() {
8590
return this.blockTimeout;
8691
}
8792

88-
8993
@Override
90-
public void exchange(HttpRequestValues requestValues) {
94+
public Void exchange(HttpRequestValues requestValues) {
9195
if (this.blockTimeout != null) {
92-
exchangeForMono(requestValues).block(this.blockTimeout);
96+
return exchangeForMono(requestValues).block(this.blockTimeout);
9397
}
9498
else {
95-
exchangeForMono(requestValues).block();
99+
return exchangeForMono(requestValues).block();
96100
}
97101
}
98102

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

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
* {@linkplain HttpServiceProxyFactory#createClient(Class) HTTP service proxy}.
2929
*
3030
* @author Rossen Stoyanchev
31+
* @author Olga Maciaszek-Sharma
3132
* @since 6.0
3233
* @deprecated in favor of {@link ReactorHttpExchangeAdapter}
3334
*/
@@ -91,50 +92,58 @@ public interface HttpClientAdapter {
9192

9293
/**
9394
* Adapt this {@link HttpClientAdapter} to {@link ReactorHttpExchangeAdapter}.
94-
* @return
95+
* @return a {@link ReactorHttpExchangeAdapter} instance created that delegating to
96+
* the underlying {@link HttpClientAdapter} implementation
9597
* @since 6.1
9698
*/
9799
default ReactorHttpExchangeAdapter asHttpExchangeAdapter() {
98100

101+
HttpClientAdapter delegate = this;
102+
99103
return new AbstractReactorHttpExchangeAdapter() {
100104

101105
@Override
102106
public Mono<Void> exchangeForMono(HttpRequestValues requestValues) {
103-
return requestToVoid(requestValues);
107+
return delegate.requestToVoid(requestValues);
104108
}
105109

106110
@Override
107111
public Mono<HttpHeaders> exchangeForHeadersMono(HttpRequestValues requestValues) {
108-
return requestToHeaders(requestValues);
112+
return delegate.requestToHeaders(requestValues);
109113
}
110114

111115
@Override
112116
public <T> Mono<T> exchangeForBodyMono(HttpRequestValues requestValues, ParameterizedTypeReference<T> bodyType) {
113-
return requestToBody(requestValues, bodyType);
117+
return delegate.requestToBody(requestValues, bodyType);
114118
}
115119

116120
@Override
117121
public <T> Flux<T> exchangeForBodyFlux(HttpRequestValues requestValues, ParameterizedTypeReference<T> bodyType) {
118-
return requestToBodyFlux(requestValues, bodyType);
122+
return delegate.requestToBodyFlux(requestValues, bodyType);
119123
}
120124

121125
@Override
122126
public Mono<ResponseEntity<Void>> exchangeForBodilessEntityMono(HttpRequestValues requestValues) {
123-
return requestToBodilessEntity(requestValues);
127+
return delegate.requestToBodilessEntity(requestValues);
124128
}
125129

126130
@Override
127131
public <T> Mono<ResponseEntity<T>> exchangeForEntityMono(
128132
HttpRequestValues requestValues, ParameterizedTypeReference<T> bodyType) {
129133

130-
return requestToEntity(requestValues, bodyType);
134+
return delegate.requestToEntity(requestValues, bodyType);
131135
}
132136

133137
@Override
134138
public <T> Mono<ResponseEntity<Flux<T>>> exchangeForEntityFlux(
135139
HttpRequestValues requestValues, ParameterizedTypeReference<T> bodyType) {
136140

137-
return requestToEntityFlux(requestValues, bodyType);
141+
return delegate.requestToEntityFlux(requestValues, bodyType);
142+
}
143+
144+
@Override
145+
public boolean supportsRequestAttributes() {
146+
return true;
138147
}
139148
};
140149
}

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ public interface HttpExchangeAdapter {
3434
* Perform the given request, and release the response content, if any.
3535
* @param requestValues the request to perform
3636
*/
37-
void exchange(HttpRequestValues requestValues);
37+
Void exchange(HttpRequestValues requestValues);
3838

3939
/**
4040
* Perform the given request, release the response content, and return the
@@ -48,8 +48,8 @@ public interface HttpExchangeAdapter {
4848
* Perform the given request and decode the response content to the given type.
4949
* @param requestValues the request to perform
5050
* @param bodyType the target type to decode to
51-
* @return the decoded response.
5251
* @param <T> the type the response is decoded to
52+
* @return the decoded response.
5353
*/
5454
@Nullable
5555
<T> T exchangeForBody(HttpRequestValues requestValues, ParameterizedTypeReference<T> bodyType);
@@ -66,4 +66,9 @@ public interface HttpExchangeAdapter {
6666
*/
6767
<T> ResponseEntity<T> exchangeForEntity(HttpRequestValues requestValues, ParameterizedTypeReference<T> bodyType);
6868

69+
/**
70+
* A flag that indicates whether request attributes are supported by a specific client
71+
* adapter.
72+
*/
73+
boolean supportsRequestAttributes();
6974
}

0 commit comments

Comments
 (0)