Skip to content

Commit a2b90d9

Browse files
committed
Add HttpExchangeAdapter decoration
Closes gh-35059
1 parent 0e84761 commit a2b90d9

File tree

4 files changed

+291
-1
lines changed

4 files changed

+291
-1
lines changed
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/*
2+
* Copyright 2002-present 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.jspecify.annotations.Nullable;
20+
21+
import org.springframework.core.ParameterizedTypeReference;
22+
import org.springframework.http.HttpHeaders;
23+
import org.springframework.http.ResponseEntity;
24+
25+
/**
26+
* {@link HttpExchangeAdapter} that wraps and delegates to another adapter instance.
27+
*
28+
* @author Rossen Stoyanchev
29+
* @since 7.0
30+
*/
31+
public class HttpExchangeAdapterDecorator implements HttpExchangeAdapter {
32+
33+
private final HttpExchangeAdapter delegate;
34+
35+
36+
public HttpExchangeAdapterDecorator(HttpExchangeAdapter delegate) {
37+
this.delegate = delegate;
38+
}
39+
40+
41+
/**
42+
* Return the wrapped delgate {@code HttpExchangeAdapter}.
43+
*/
44+
public HttpExchangeAdapter getHttpExchangeAdapter() {
45+
return this.delegate;
46+
}
47+
48+
49+
@Override
50+
public boolean supportsRequestAttributes() {
51+
return this.delegate.supportsRequestAttributes();
52+
}
53+
54+
@Override
55+
public void exchange(HttpRequestValues requestValues) {
56+
this.delegate.exchange(requestValues);
57+
}
58+
59+
@Override
60+
public HttpHeaders exchangeForHeaders(HttpRequestValues requestValues) {
61+
return this.delegate.exchangeForHeaders(requestValues);
62+
}
63+
64+
@Override
65+
public <T> @Nullable T exchangeForBody(HttpRequestValues requestValues, ParameterizedTypeReference<T> bodyType) {
66+
return this.delegate.exchangeForBody(requestValues, bodyType);
67+
}
68+
69+
@Override
70+
public ResponseEntity<Void> exchangeForBodilessEntity(HttpRequestValues requestValues) {
71+
return this.delegate.exchangeForBodilessEntity(requestValues);
72+
}
73+
74+
@Override
75+
public <T> ResponseEntity<T> exchangeForEntity(HttpRequestValues requestValues, ParameterizedTypeReference<T> bodyType) {
76+
return this.delegate.exchangeForEntity(requestValues, bodyType);
77+
}
78+
79+
}

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

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,8 @@ public static final class Builder {
136136

137137
private @Nullable HttpExchangeAdapter exchangeAdapter;
138138

139+
private Function<HttpExchangeAdapter, HttpExchangeAdapter> exchangeAdapterDecorator = Function.identity();
140+
139141
private final List<HttpServiceArgumentResolver> customArgumentResolvers = new ArrayList<>();
140142

141143
private final List<HttpRequestValues.Processor> requestValuesProcessors = new ArrayList<>();
@@ -158,6 +160,17 @@ public Builder exchangeAdapter(HttpExchangeAdapter adapter) {
158160
return this;
159161
}
160162

163+
/**
164+
* Provide a function to wrap the configured {@code HttpExchangeAdapter}.
165+
* @param decorator a client adapted to {@link HttpExchangeAdapter}
166+
* @return this same builder instance
167+
* @since 7.0
168+
*/
169+
public Builder exchangeAdapterDecorator(Function<HttpExchangeAdapter, HttpExchangeAdapter> decorator) {
170+
this.exchangeAdapterDecorator = this.exchangeAdapterDecorator.andThen(decorator);
171+
return this;
172+
}
173+
161174
/**
162175
* Register a custom argument resolver, invoked ahead of default resolvers.
163176
* @param resolver the resolver to add
@@ -207,9 +220,10 @@ public Builder embeddedValueResolver(StringValueResolver embeddedValueResolver)
207220
*/
208221
public HttpServiceProxyFactory build() {
209222
Assert.notNull(this.exchangeAdapter, "HttpClientAdapter is required");
223+
HttpExchangeAdapter adapterToUse = this.exchangeAdapterDecorator.apply(this.exchangeAdapter);
210224

211225
return new HttpServiceProxyFactory(
212-
this.exchangeAdapter, initArgumentResolvers(), this.requestValuesProcessors,
226+
adapterToUse, initArgumentResolvers(), this.requestValuesProcessors,
213227
this.embeddedValueResolver);
214228
}
215229

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
/*
2+
* Copyright 2002-present 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.time.Duration;
20+
21+
import org.jspecify.annotations.Nullable;
22+
import reactor.core.publisher.Flux;
23+
import reactor.core.publisher.Mono;
24+
25+
import org.springframework.core.ParameterizedTypeReference;
26+
import org.springframework.core.ReactiveAdapterRegistry;
27+
import org.springframework.http.HttpHeaders;
28+
import org.springframework.http.ResponseEntity;
29+
30+
/**
31+
* {@link ReactorHttpExchangeAdapter} that wraps and delegates to another adapter instance.
32+
*
33+
* @author Rossen Stoyanchev
34+
* @since 7.0
35+
*/
36+
public class ReactorHttpExchangeAdapterDecorator extends HttpExchangeAdapterDecorator
37+
implements ReactorHttpExchangeAdapter {
38+
39+
40+
public ReactorHttpExchangeAdapterDecorator(HttpExchangeAdapter delegate) {
41+
super(delegate);
42+
}
43+
44+
45+
/**
46+
* Return the wrapped delgate {@code HttpExchangeAdapter}.
47+
*/
48+
@Override
49+
public ReactorHttpExchangeAdapter getHttpExchangeAdapter() {
50+
return (ReactorHttpExchangeAdapter) super.getHttpExchangeAdapter();
51+
}
52+
53+
54+
@Override
55+
public boolean supportsRequestAttributes() {
56+
return getHttpExchangeAdapter().supportsRequestAttributes();
57+
}
58+
59+
@Override
60+
public void exchange(HttpRequestValues requestValues) {
61+
getHttpExchangeAdapter().exchange(requestValues);
62+
}
63+
64+
@Override
65+
public HttpHeaders exchangeForHeaders(HttpRequestValues requestValues) {
66+
return getHttpExchangeAdapter().exchangeForHeaders(requestValues);
67+
}
68+
69+
@Override
70+
public <T> @Nullable T exchangeForBody(HttpRequestValues requestValues, ParameterizedTypeReference<T> bodyType) {
71+
return getHttpExchangeAdapter().exchangeForBody(requestValues, bodyType);
72+
}
73+
74+
@Override
75+
public ResponseEntity<Void> exchangeForBodilessEntity(HttpRequestValues requestValues) {
76+
return getHttpExchangeAdapter().exchangeForBodilessEntity(requestValues);
77+
}
78+
79+
@Override
80+
public <T> ResponseEntity<T> exchangeForEntity(HttpRequestValues requestValues, ParameterizedTypeReference<T> bodyType) {
81+
return getHttpExchangeAdapter().exchangeForEntity(requestValues, bodyType);
82+
}
83+
84+
@Override
85+
public ReactiveAdapterRegistry getReactiveAdapterRegistry() {
86+
return getHttpExchangeAdapter().getReactiveAdapterRegistry();
87+
}
88+
89+
@Override
90+
public @Nullable Duration getBlockTimeout() {
91+
return getHttpExchangeAdapter().getBlockTimeout();
92+
}
93+
94+
@Override
95+
public Mono<Void> exchangeForMono(HttpRequestValues requestValues) {
96+
return getHttpExchangeAdapter().exchangeForMono(requestValues);
97+
}
98+
99+
@Override
100+
public Mono<HttpHeaders> exchangeForHeadersMono(HttpRequestValues requestValues) {
101+
return getHttpExchangeAdapter().exchangeForHeadersMono(requestValues);
102+
}
103+
104+
@Override
105+
public <T> Mono<T> exchangeForBodyMono(HttpRequestValues requestValues, ParameterizedTypeReference<T> bodyType) {
106+
return getHttpExchangeAdapter().exchangeForBodyMono(requestValues, bodyType);
107+
}
108+
109+
@Override
110+
public <T> Flux<T> exchangeForBodyFlux(HttpRequestValues requestValues, ParameterizedTypeReference<T> bodyType) {
111+
return getHttpExchangeAdapter().exchangeForBodyFlux(requestValues, bodyType);
112+
}
113+
114+
@Override
115+
public Mono<ResponseEntity<Void>> exchangeForBodilessEntityMono(HttpRequestValues values) {
116+
return getHttpExchangeAdapter().exchangeForBodilessEntityMono(values);
117+
}
118+
119+
@Override
120+
public <T> Mono<ResponseEntity<T>> exchangeForEntityMono(HttpRequestValues values, ParameterizedTypeReference<T> bodyType) {
121+
return getHttpExchangeAdapter().exchangeForEntityMono(values, bodyType);
122+
}
123+
124+
@Override
125+
public <T> Mono<ResponseEntity<Flux<T>>> exchangeForEntityFlux(HttpRequestValues values, ParameterizedTypeReference<T> bodyType) {
126+
return getHttpExchangeAdapter().exchangeForEntityFlux(values, bodyType);
127+
}
128+
129+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/*
2+
* Copyright 2002-present 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.jspecify.annotations.Nullable;
20+
import org.junit.jupiter.api.Test;
21+
22+
import org.springframework.core.ParameterizedTypeReference;
23+
import org.springframework.web.service.annotation.GetExchange;
24+
25+
import static org.assertj.core.api.Assertions.assertThat;
26+
import static org.mockito.Mockito.mock;
27+
28+
/**
29+
* Unit tests for {@link HttpServiceProxyFactory}.
30+
* @author Rossen Stoyanchev
31+
*/
32+
public class HttpServiceProxyFactoryTests {
33+
34+
35+
@Test
36+
void httpExchangeAdapterDecorator() {
37+
38+
HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(mock(HttpExchangeAdapter.class))
39+
.exchangeAdapterDecorator(TestDecorator::new)
40+
.build();
41+
42+
Service service = factory.createClient(Service.class);
43+
assertThat(service.execute()).isEqualTo("decorated");
44+
}
45+
46+
47+
48+
private interface Service {
49+
50+
@GetExchange
51+
String execute();
52+
}
53+
54+
55+
private static class TestDecorator extends HttpExchangeAdapterDecorator {
56+
57+
public TestDecorator(HttpExchangeAdapter delegate) {
58+
super(delegate);
59+
}
60+
61+
@SuppressWarnings("unchecked")
62+
@Override
63+
public <T> @Nullable T exchangeForBody(HttpRequestValues values, ParameterizedTypeReference<T> bodyType) {
64+
return (T) "decorated";
65+
}
66+
}
67+
68+
}

0 commit comments

Comments
 (0)