Skip to content

Commit 48c1746

Browse files
committed
Refactor HttpServiceProxyFactory for use as a bean
Closes gh-28505
1 parent 29a9016 commit 48c1746

File tree

12 files changed

+228
-166
lines changed

12 files changed

+228
-166
lines changed

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

Lines changed: 109 additions & 135 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828
import org.aopalliance.intercept.MethodInvocation;
2929

3030
import org.springframework.aop.framework.ProxyFactory;
31+
import org.springframework.beans.factory.InitializingBean;
32+
import org.springframework.context.EmbeddedValueResolverAware;
3133
import org.springframework.core.MethodIntrospector;
3234
import org.springframework.core.ReactiveAdapterRegistry;
3335
import org.springframework.core.annotation.AnnotatedElementUtils;
@@ -42,189 +44,161 @@
4244
* Factory for creating a client proxy given an HTTP service interface with
4345
* {@link HttpExchange @HttpExchange} methods.
4446
*
47+
* <p>This class is intended to be declared as a bean in a Spring configuration.
48+
*
4549
* @author Rossen Stoyanchev
4650
* @since 6.0
4751
*/
48-
public final class HttpServiceProxyFactory {
52+
public final class HttpServiceProxyFactory implements InitializingBean, EmbeddedValueResolverAware {
4953

5054
private final HttpClientAdapter clientAdapter;
5155

52-
private final List<HttpServiceArgumentResolver> argumentResolvers;
56+
@Nullable
57+
private List<HttpServiceArgumentResolver> customArgumentResolvers;
58+
59+
@Nullable
60+
private List<HttpServiceArgumentResolver> argumentResolvers;
5361

5462
@Nullable
55-
private final StringValueResolver embeddedValueResolver;
63+
private ConversionService conversionService;
5664

57-
private final ReactiveAdapterRegistry reactiveAdapterRegistry;
65+
@Nullable
66+
private StringValueResolver embeddedValueResolver;
5867

59-
private final Duration blockTimeout;
68+
private ReactiveAdapterRegistry reactiveAdapterRegistry = ReactiveAdapterRegistry.getSharedInstance();
6069

70+
private Duration blockTimeout = Duration.ofSeconds(5);
6171

62-
private HttpServiceProxyFactory(
63-
HttpClientAdapter clientAdapter, List<HttpServiceArgumentResolver> argumentResolvers,
64-
@Nullable StringValueResolver embeddedValueResolver, ReactiveAdapterRegistry reactiveAdapterRegistry,
65-
Duration blockTimeout) {
6672

73+
/**
74+
* Create an instance with the underlying HTTP client to use.
75+
* @param clientAdapter an adapter for the client
76+
*/
77+
public HttpServiceProxyFactory(HttpClientAdapter clientAdapter) {
78+
Assert.notNull(clientAdapter, "HttpClientAdapter is required");
6779
this.clientAdapter = clientAdapter;
68-
this.argumentResolvers = argumentResolvers;
69-
this.embeddedValueResolver = embeddedValueResolver;
70-
this.reactiveAdapterRegistry = reactiveAdapterRegistry;
71-
this.blockTimeout = blockTimeout;
7280
}
7381

7482

7583
/**
76-
* Return a proxy that implements the given HTTP service interface to perform
77-
* HTTP requests and retrieve responses through an HTTP client.
78-
* @param serviceType the HTTP service to create a proxy for
79-
* @param <S> the HTTP service type
80-
* @return the created proxy
84+
* Register a custom argument resolver, invoked ahead of default resolvers.
85+
* @param resolver the resolver to add
8186
*/
82-
public <S> S createClient(Class<S> serviceType) {
83-
84-
List<HttpServiceMethod> methods =
85-
MethodIntrospector.selectMethods(serviceType, this::isExchangeMethod)
86-
.stream()
87-
.map(method ->
88-
new HttpServiceMethod(
89-
method, serviceType, this.argumentResolvers, this.clientAdapter,
90-
this.embeddedValueResolver, this.reactiveAdapterRegistry, this.blockTimeout))
91-
.toList();
92-
93-
return ProxyFactory.getProxy(serviceType, new HttpServiceMethodInterceptor(methods));
87+
public void addCustomArgumentResolver(HttpServiceArgumentResolver resolver) {
88+
if (this.customArgumentResolvers == null) {
89+
this.customArgumentResolvers = new ArrayList<>();
90+
}
91+
this.customArgumentResolvers.add(resolver);
9492
}
9593

96-
private boolean isExchangeMethod(Method method) {
97-
return AnnotatedElementUtils.hasAnnotation(method, HttpExchange.class);
94+
/**
95+
* Set the custom argument resolvers to use, ahead of default resolvers.
96+
* @param resolvers the resolvers to use
97+
*/
98+
public void setCustomArgumentResolvers(List<HttpServiceArgumentResolver> resolvers) {
99+
this.customArgumentResolvers = new ArrayList<>(resolvers);
98100
}
99101

100-
101102
/**
102-
* Return a builder for an {@link HttpServiceProxyFactory}.
103-
* @param adapter an adapter for the underlying HTTP client
104-
* @return the builder
103+
* Set the {@link ConversionService} to use where input values need to
104+
* be formatted as Strings.
105+
* <p>By default this is {@link DefaultFormattingConversionService}.
105106
*/
106-
public static Builder builder(HttpClientAdapter adapter) {
107-
return new Builder(adapter);
107+
public void setConversionService(ConversionService conversionService) {
108+
this.conversionService = conversionService;
108109
}
109110

110-
111111
/**
112-
* Builder for {@link HttpServiceProxyFactory}.
112+
* Set the StringValueResolver to use for resolving placeholders and
113+
* expressions in {@link HttpExchange#url()}.
114+
* @param resolver the resolver to use
113115
*/
114-
public final static class Builder {
115-
116-
private final HttpClientAdapter clientAdapter;
117-
118-
private final List<HttpServiceArgumentResolver> customResolvers = new ArrayList<>();
119-
120-
@Nullable
121-
private ConversionService conversionService;
122-
123-
@Nullable
124-
private StringValueResolver embeddedValueResolver;
116+
@Override
117+
public void setEmbeddedValueResolver(StringValueResolver resolver) {
118+
this.embeddedValueResolver = resolver;
119+
}
125120

126-
private ReactiveAdapterRegistry reactiveAdapterRegistry = ReactiveAdapterRegistry.getSharedInstance();
121+
/**
122+
* Set the {@link ReactiveAdapterRegistry} to use to support different
123+
* asynchronous types for HTTP service method return values.
124+
* <p>By default this is {@link ReactiveAdapterRegistry#getSharedInstance()}.
125+
*/
126+
public void setReactiveAdapterRegistry(ReactiveAdapterRegistry registry) {
127+
this.reactiveAdapterRegistry = registry;
128+
}
127129

128-
private Duration blockTimeout = Duration.ofSeconds(5);
130+
/**
131+
* Configure how long to wait for a response for an HTTP service method
132+
* with a synchronous (blocking) method signature.
133+
* <p>By default this is 5 seconds.
134+
* @param blockTimeout the timeout value
135+
*/
136+
public void setBlockTimeout(Duration blockTimeout) {
137+
this.blockTimeout = blockTimeout;
138+
}
129139

130-
private Builder(HttpClientAdapter clientAdapter) {
131-
Assert.notNull(clientAdapter, "HttpClientAdapter is required");
132-
this.clientAdapter = clientAdapter;
133-
}
134140

135-
/**
136-
* Register a custom argument resolver. This will be inserted ahead of
137-
* default resolvers.
138-
* @return the same builder instance
139-
*/
140-
public Builder addCustomResolver(HttpServiceArgumentResolver resolver) {
141-
this.customResolvers.add(resolver);
142-
return this;
143-
}
141+
@Override
142+
public void afterPropertiesSet() throws Exception {
144143

145-
/**
146-
* Set the {@link ConversionService} to use where input values need to
147-
* be formatted as Strings.
148-
* <p>By default this is {@link DefaultFormattingConversionService}.
149-
* @return the same builder instance
150-
*/
151-
public Builder setConversionService(ConversionService conversionService) {
152-
this.conversionService = conversionService;
153-
return this;
154-
}
144+
this.conversionService = (this.conversionService != null ?
145+
this.conversionService : new DefaultFormattingConversionService());
155146

156-
/**
157-
* Set the StringValueResolver to use for resolving placeholders and
158-
* expressions in {@link HttpExchange#url()}.
159-
* @param embeddedValueResolver the resolver to use
160-
* @return the same builder instance
161-
* @see org.springframework.context.EmbeddedValueResolverAware
162-
*/
163-
public Builder setEmbeddedValueResolver(@Nullable StringValueResolver embeddedValueResolver) {
164-
this.embeddedValueResolver = embeddedValueResolver;
165-
return this;
166-
}
147+
this.argumentResolvers = initArgumentResolvers(this.conversionService);
148+
}
167149

168-
/**
169-
* Set the {@link ReactiveAdapterRegistry} to use to support different
170-
* asynchronous types for HTTP service method return values.
171-
* <p>By default this is {@link ReactiveAdapterRegistry#getSharedInstance()}.
172-
* @return the same builder instance
173-
*/
174-
public Builder setReactiveAdapterRegistry(ReactiveAdapterRegistry registry) {
175-
this.reactiveAdapterRegistry = registry;
176-
return this;
177-
}
150+
private List<HttpServiceArgumentResolver> initArgumentResolvers(ConversionService conversionService) {
151+
List<HttpServiceArgumentResolver> resolvers = new ArrayList<>();
178152

179-
/**
180-
* Configure how long to wait for a response for an HTTP service method
181-
* with a synchronous (blocking) method signature.
182-
* <p>By default this is 5 seconds.
183-
* @param blockTimeout the timeout value
184-
* @return the same builder instance
185-
*/
186-
public Builder setBlockTimeout(Duration blockTimeout) {
187-
this.blockTimeout = blockTimeout;
188-
return this;
153+
// Custom
154+
if (this.customArgumentResolvers != null) {
155+
resolvers.addAll(this.customArgumentResolvers);
189156
}
190157

191-
/**
192-
* Build and return the {@link HttpServiceProxyFactory} instance.
193-
*/
194-
public HttpServiceProxyFactory build() {
158+
// Annotation-based
159+
resolvers.add(new RequestHeaderArgumentResolver(conversionService));
160+
resolvers.add(new RequestBodyArgumentResolver(this.reactiveAdapterRegistry));
161+
resolvers.add(new PathVariableArgumentResolver(conversionService));
162+
resolvers.add(new RequestParamArgumentResolver(conversionService));
163+
resolvers.add(new CookieValueArgumentResolver(conversionService));
164+
resolvers.add(new RequestAttributeArgumentResolver());
195165

196-
ConversionService conversionService = initConversionService();
197-
List<HttpServiceArgumentResolver> resolvers = initArgumentResolvers(conversionService);
166+
// Specific type
167+
resolvers.add(new UrlArgumentResolver());
168+
resolvers.add(new HttpMethodArgumentResolver());
198169

199-
return new HttpServiceProxyFactory(
200-
this.clientAdapter, resolvers, this.embeddedValueResolver, this.reactiveAdapterRegistry,
201-
this.blockTimeout);
202-
}
170+
return resolvers;
171+
}
203172

204-
private ConversionService initConversionService() {
205-
return (this.conversionService != null ?
206-
this.conversionService : new DefaultFormattingConversionService());
207-
}
208173

209-
private List<HttpServiceArgumentResolver> initArgumentResolvers(ConversionService conversionService) {
174+
/**
175+
* Return a proxy that implements the given HTTP service interface to perform
176+
* HTTP requests and retrieve responses through an HTTP client.
177+
* @param serviceType the HTTP service to create a proxy for
178+
* @param <S> the HTTP service type
179+
* @return the created proxy
180+
*/
181+
public <S> S createClient(Class<S> serviceType) {
210182

211-
List<HttpServiceArgumentResolver> resolvers = new ArrayList<>(this.customResolvers);
183+
List<HttpServiceMethod> httpServiceMethods =
184+
MethodIntrospector.selectMethods(serviceType, this::isExchangeMethod).stream()
185+
.map(method -> createHttpServiceMethod(serviceType, method))
186+
.toList();
212187

213-
// Annotation-based
214-
resolvers.add(new RequestHeaderArgumentResolver(conversionService));
215-
resolvers.add(new RequestBodyArgumentResolver(this.reactiveAdapterRegistry));
216-
resolvers.add(new PathVariableArgumentResolver(conversionService));
217-
resolvers.add(new RequestParamArgumentResolver(conversionService));
218-
resolvers.add(new CookieValueArgumentResolver(conversionService));
219-
resolvers.add(new RequestAttributeArgumentResolver());
188+
return ProxyFactory.getProxy(serviceType, new HttpServiceMethodInterceptor(httpServiceMethods));
189+
}
220190

221-
// Specific type
222-
resolvers.add(new UrlArgumentResolver());
223-
resolvers.add(new HttpMethodArgumentResolver());
191+
private boolean isExchangeMethod(Method method) {
192+
return AnnotatedElementUtils.hasAnnotation(method, HttpExchange.class);
193+
}
224194

225-
return resolvers;
226-
}
195+
private <S> HttpServiceMethod createHttpServiceMethod(Class<S> serviceType, Method method) {
196+
Assert.notNull(this.argumentResolvers,
197+
"No argument resolvers: afterPropertiesSet was not called");
227198

199+
return new HttpServiceMethod(
200+
method, serviceType, this.argumentResolvers, this.clientAdapter,
201+
this.embeddedValueResolver, this.reactiveAdapterRegistry, this.blockTimeout);
228202
}
229203

230204

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import java.util.List;
2020

21+
import org.junit.jupiter.api.BeforeEach;
2122
import org.junit.jupiter.api.Test;
2223

2324
import org.springframework.util.ObjectUtils;
@@ -36,7 +37,15 @@ class CookieValueArgumentResolverTests {
3637

3738
private final TestHttpClientAdapter client = new TestHttpClientAdapter();
3839

39-
private final Service service = HttpServiceProxyFactory.builder(this.client).build().createClient(Service.class);
40+
private Service service;
41+
42+
43+
@BeforeEach
44+
void setUp() throws Exception {
45+
HttpServiceProxyFactory proxyFactory = new HttpServiceProxyFactory(this.client);
46+
proxyFactory.afterPropertiesSet();
47+
this.service = proxyFactory.createClient(Service.class);
48+
}
4049

4150

4251
// Base class functionality should be tested in NamedValueArgumentResolverTests.

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

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

19+
import org.junit.jupiter.api.BeforeEach;
1920
import org.junit.jupiter.api.Test;
2021

2122
import org.springframework.http.HttpMethod;
@@ -34,7 +35,15 @@ public class HttpMethodArgumentResolverTests {
3435

3536
private final TestHttpClientAdapter client = new TestHttpClientAdapter();
3637

37-
private final Service service = HttpServiceProxyFactory.builder(this.client).build().createClient(Service.class);
38+
private Service service;
39+
40+
41+
@BeforeEach
42+
void setUp() throws Exception {
43+
HttpServiceProxyFactory proxyFactory = new HttpServiceProxyFactory(this.client);
44+
proxyFactory.afterPropertiesSet();
45+
this.service = proxyFactory.createClient(Service.class);
46+
}
3847

3948

4049
@Test

0 commit comments

Comments
 (0)