Skip to content

Commit b1384dd

Browse files
committed
Add HttpServiceProxyFactory builder
See gh-28386
1 parent 8a46e96 commit b1384dd

File tree

5 files changed

+133
-40
lines changed

5 files changed

+133
-40
lines changed

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

+127-20
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,11 @@
1818

1919
import java.lang.reflect.Method;
2020
import java.time.Duration;
21-
import java.util.HashMap;
21+
import java.util.ArrayList;
2222
import java.util.List;
2323
import java.util.Map;
24+
import java.util.function.Function;
25+
import java.util.stream.Collectors;
2426

2527
import org.aopalliance.intercept.MethodInterceptor;
2628
import org.aopalliance.intercept.MethodInvocation;
@@ -29,62 +31,167 @@
2931
import org.springframework.core.MethodIntrospector;
3032
import org.springframework.core.ReactiveAdapterRegistry;
3133
import org.springframework.core.annotation.AnnotatedElementUtils;
34+
import org.springframework.core.convert.ConversionService;
35+
import org.springframework.format.support.DefaultFormattingConversionService;
36+
import org.springframework.lang.Nullable;
37+
import org.springframework.util.Assert;
3238
import org.springframework.web.service.annotation.HttpExchange;
3339

3440

3541
/**
36-
* Factory to create a proxy for an HTTP service with {@link HttpExchange} methods.
42+
* Factory for creating a client proxy given an HTTP service interface with
43+
* {@link HttpExchange @HttpExchange} methods.
3744
*
3845
* @author Rossen Stoyanchev
3946
* @since 6.0
4047
*/
41-
public class HttpServiceProxyFactory {
42-
43-
private final List<HttpServiceArgumentResolver> argumentResolvers;
48+
public final class HttpServiceProxyFactory {
4449

4550
private final HttpClientAdapter clientAdapter;
4651

52+
private final List<HttpServiceArgumentResolver> argumentResolvers;
53+
4754
private final ReactiveAdapterRegistry reactiveAdapterRegistry;
4855

4956
private final Duration blockTimeout;
5057

5158

52-
public HttpServiceProxyFactory(
53-
List<HttpServiceArgumentResolver> argumentResolvers, HttpClientAdapter clientAdapter,
59+
private HttpServiceProxyFactory(
60+
HttpClientAdapter clientAdapter, List<HttpServiceArgumentResolver> argumentResolvers,
5461
ReactiveAdapterRegistry reactiveAdapterRegistry, Duration blockTimeout) {
5562

56-
this.argumentResolvers = argumentResolvers;
5763
this.clientAdapter = clientAdapter;
64+
this.argumentResolvers = argumentResolvers;
5865
this.reactiveAdapterRegistry = reactiveAdapterRegistry;
5966
this.blockTimeout = blockTimeout;
6067
}
6168

6269

6370
/**
64-
* Create a proxy for executing requests to the given HTTP service.
71+
* Return a proxy that implements the given HTTP service interface to perform
72+
* HTTP requests and retrieves responses through an HTTP client.
6573
* @param serviceType the HTTP service to create a proxy for
66-
* @param <S> the service type
74+
* @param <S> the HTTP service type
6775
* @return the created proxy
6876
*/
6977
public <S> S createClient(Class<S> serviceType) {
7078

7179
List<HttpServiceMethod> methods =
72-
MethodIntrospector.selectMethods(serviceType, this::isHttpRequestMethod)
80+
MethodIntrospector.selectMethods(serviceType, this::isExchangeMethod)
7381
.stream()
74-
.map(method -> initServiceMethod(method, serviceType))
82+
.map(method ->
83+
new HttpServiceMethod(
84+
method, serviceType, this.argumentResolvers,
85+
this.clientAdapter, this.reactiveAdapterRegistry, this.blockTimeout))
7586
.toList();
7687

7788
return ProxyFactory.getProxy(serviceType, new HttpServiceMethodInterceptor(methods));
7889
}
7990

80-
private boolean isHttpRequestMethod(Method method) {
91+
private boolean isExchangeMethod(Method method) {
8192
return AnnotatedElementUtils.hasAnnotation(method, HttpExchange.class);
8293
}
8394

84-
private HttpServiceMethod initServiceMethod(Method method, Class<?> serviceType) {
85-
return new HttpServiceMethod(
86-
method, serviceType, this.argumentResolvers,
87-
this.clientAdapter, this.reactiveAdapterRegistry, this.blockTimeout);
95+
96+
/**
97+
* Return a builder for an {@link HttpServiceProxyFactory}.
98+
* @param adapter an adapter for the underlying HTTP client
99+
* @return the builder
100+
*/
101+
public static Builder builder(HttpClientAdapter adapter) {
102+
return new Builder(adapter);
103+
}
104+
105+
106+
/**
107+
* Builder for {@link HttpServiceProxyFactory}.
108+
*/
109+
public final static class Builder {
110+
111+
private final HttpClientAdapter clientAdapter;
112+
113+
private final List<HttpServiceArgumentResolver> customResolvers = new ArrayList<>();
114+
115+
@Nullable
116+
private ConversionService conversionService;
117+
118+
private ReactiveAdapterRegistry reactiveAdapterRegistry = ReactiveAdapterRegistry.getSharedInstance();
119+
120+
private Duration blockTimeout = Duration.ofSeconds(5);
121+
122+
private Builder(HttpClientAdapter clientAdapter) {
123+
Assert.notNull(clientAdapter, "HttpClientAdapter is required");
124+
this.clientAdapter = clientAdapter;
125+
}
126+
127+
/**
128+
* Register a custom argument resolver. This will be inserted ahead of
129+
* default resolvers.
130+
* @return the same builder instance
131+
*/
132+
public Builder addCustomResolver(HttpServiceArgumentResolver resolver) {
133+
this.customResolvers.add(resolver);
134+
return this;
135+
}
136+
137+
/**
138+
* Set the {@link ConversionService} to use where input values need to
139+
* be formatted as Strings.
140+
* <p>By default this is {@link DefaultFormattingConversionService}.
141+
* @return the same builder instance
142+
*/
143+
public Builder setConversionService(ConversionService conversionService) {
144+
this.conversionService = conversionService;
145+
return this;
146+
}
147+
148+
/**
149+
* Set the {@link ReactiveAdapterRegistry} to use to support different
150+
* asynchronous types for HTTP Service method return values.
151+
* <p>By default this is {@link ReactiveAdapterRegistry#getSharedInstance()}.
152+
* @return the same builder instance
153+
*/
154+
public Builder setReactiveAdapterRegistry(ReactiveAdapterRegistry registry) {
155+
this.reactiveAdapterRegistry = registry;
156+
return this;
157+
}
158+
159+
/**
160+
* Configure how long to wait for a response for an HTTP Service method
161+
* with a synchronous (blocking) method signature.
162+
* <p>By default this is 5 seconds.
163+
* @param blockTimeout the timeout value
164+
* @return the same builder instance
165+
*/
166+
public Builder setBlockTimeout(Duration blockTimeout) {
167+
this.blockTimeout = blockTimeout;
168+
return this;
169+
}
170+
171+
/**
172+
* Build and return the {@link HttpServiceProxyFactory} instance.
173+
*/
174+
public HttpServiceProxyFactory build() {
175+
176+
ConversionService conversionService = initConversionService();
177+
List<HttpServiceArgumentResolver> resolvers = initArgumentResolvers(conversionService);
178+
179+
return new HttpServiceProxyFactory(
180+
this.clientAdapter, resolvers, this.reactiveAdapterRegistry, this.blockTimeout);
181+
}
182+
183+
private ConversionService initConversionService() {
184+
return (this.conversionService != null ?
185+
this.conversionService : new DefaultFormattingConversionService());
186+
}
187+
188+
private List<HttpServiceArgumentResolver> initArgumentResolvers(ConversionService conversionService) {
189+
List<HttpServiceArgumentResolver> resolvers = new ArrayList<>(this.customResolvers);
190+
resolvers.add(new HttpMethodArgumentResolver());
191+
resolvers.add(new PathVariableArgumentResolver(conversionService));
192+
return resolvers;
193+
}
194+
88195
}
89196

90197

@@ -93,10 +200,11 @@ private HttpServiceMethod initServiceMethod(Method method, Class<?> serviceType)
93200
*/
94201
private static final class HttpServiceMethodInterceptor implements MethodInterceptor {
95202

96-
private final Map<Method, HttpServiceMethod> httpServiceMethods = new HashMap<>();
203+
private final Map<Method, HttpServiceMethod> httpServiceMethods;
97204

98205
private HttpServiceMethodInterceptor(List<HttpServiceMethod> methods) {
99-
methods.forEach(serviceMethod -> this.httpServiceMethods.put(serviceMethod.getMethod(), serviceMethod));
206+
this.httpServiceMethods = methods.stream()
207+
.collect(Collectors.toMap(HttpServiceMethod::getMethod, Function.identity()));
100208
}
101209

102210
@Override
@@ -105,7 +213,6 @@ public Object invoke(MethodInvocation invocation) {
105213
HttpServiceMethod httpServiceMethod = this.httpServiceMethods.get(method);
106214
return httpServiceMethod.invoke(invocation.getArguments());
107215
}
108-
109216
}
110217

111218
}

spring-web/src/test/java/org/springframework/web/service/invoker/HttpMethodArgumentResolverTests.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ public class HttpMethodArgumentResolverTests {
3434

3535
private final TestHttpClientAdapter clientAdapter = new TestHttpClientAdapter();
3636

37-
private final Service service = this.clientAdapter.createService(Service.class, new HttpMethodArgumentResolver());
37+
private final Service service = this.clientAdapter.createService(Service.class);
3838

3939

4040
@Test

spring-web/src/test/java/org/springframework/web/service/invoker/PathVariableArgumentResolverTests.java

+1-3
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121

2222
import org.junit.jupiter.api.Test;
2323

24-
import org.springframework.core.convert.support.DefaultConversionService;
2524
import org.springframework.lang.Nullable;
2625
import org.springframework.web.bind.annotation.PathVariable;
2726
import org.springframework.web.service.annotation.GetExchange;
@@ -39,8 +38,7 @@ class PathVariableArgumentResolverTests {
3938

4039
private final TestHttpClientAdapter clientAdapter = new TestHttpClientAdapter();
4140

42-
private final Service service = this.clientAdapter.createService(
43-
Service.class, new PathVariableArgumentResolver(new DefaultConversionService()));
41+
private final Service service = this.clientAdapter.createService(Service.class);
4442

4543

4644
@Test

spring-web/src/test/java/org/springframework/web/service/invoker/TestHttpClientAdapter.java

+2-9
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,10 @@
1616

1717
package org.springframework.web.service.invoker;
1818

19-
import java.time.Duration;
20-
import java.util.Arrays;
21-
2219
import reactor.core.publisher.Flux;
2320
import reactor.core.publisher.Mono;
2421

2522
import org.springframework.core.ParameterizedTypeReference;
26-
import org.springframework.core.ReactiveAdapterRegistry;
2723
import org.springframework.http.HttpHeaders;
2824
import org.springframework.http.ResponseEntity;
2925
import org.springframework.lang.Nullable;
@@ -53,11 +49,8 @@ class TestHttpClientAdapter implements HttpClientAdapter {
5349
/**
5450
* Create the proxy for the give service type.
5551
*/
56-
public <S> S createService(Class<S> serviceType, HttpServiceArgumentResolver... resolvers) {
57-
58-
HttpServiceProxyFactory factory = new HttpServiceProxyFactory(
59-
Arrays.asList(resolvers), this, ReactiveAdapterRegistry.getSharedInstance(), Duration.ofSeconds(5));
60-
52+
public <S> S createService(Class<S> serviceType) {
53+
HttpServiceProxyFactory factory = HttpServiceProxyFactory.builder(this).build();
6154
return factory.createClient(serviceType);
6255
}
6356

spring-webflux/src/test/java/org/springframework/web/reactive/function/client/support/WebClientHttpServiceProxyTests.java

+2-7
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919

2020
import java.io.IOException;
2121
import java.time.Duration;
22-
import java.util.Collections;
2322
import java.util.function.Consumer;
2423

2524
import okhttp3.mockwebserver.MockResponse;
@@ -30,7 +29,6 @@
3029
import reactor.core.publisher.Mono;
3130
import reactor.test.StepVerifier;
3231

33-
import org.springframework.core.ReactiveAdapterRegistry;
3432
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
3533
import org.springframework.web.reactive.function.client.WebClient;
3634
import org.springframework.web.service.annotation.GetExchange;
@@ -59,11 +57,8 @@ void setUp() {
5957
.baseUrl(this.server.url("/").toString())
6058
.build();
6159

62-
WebClientAdapter webClientAdapter = new WebClientAdapter(webClient);
63-
64-
HttpServiceProxyFactory proxyFactory = new HttpServiceProxyFactory(
65-
Collections.emptyList(), webClientAdapter, ReactiveAdapterRegistry.getSharedInstance(),
66-
Duration.ofSeconds(5));
60+
WebClientAdapter clientAdapter = new WebClientAdapter(webClient);
61+
HttpServiceProxyFactory proxyFactory = HttpServiceProxyFactory.builder(clientAdapter).build();
6762

6863
this.httpService = proxyFactory.createClient(TestHttpService.class);
6964
}

0 commit comments

Comments
 (0)