Refactor HttpServiceProxyFactory for use as a bean

Closes gh-28505
This commit is contained in:
rstoyanchev
2022-05-24 08:44:09 +01:00
parent 29a9016809
commit 48c1746693
12 changed files with 241 additions and 179 deletions

View File

@@ -28,6 +28,8 @@ import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.EmbeddedValueResolverAware;
import org.springframework.core.MethodIntrospector;
import org.springframework.core.ReactiveAdapterRegistry;
import org.springframework.core.annotation.AnnotatedElementUtils;
@@ -42,36 +44,133 @@ import org.springframework.web.service.annotation.HttpExchange;
* Factory for creating a client proxy given an HTTP service interface with
* {@link HttpExchange @HttpExchange} methods.
*
* <p>This class is intended to be declared as a bean in a Spring configuration.
*
* @author Rossen Stoyanchev
* @since 6.0
*/
public final class HttpServiceProxyFactory {
public final class HttpServiceProxyFactory implements InitializingBean, EmbeddedValueResolverAware {
private final HttpClientAdapter clientAdapter;
private final List<HttpServiceArgumentResolver> argumentResolvers;
@Nullable
private List<HttpServiceArgumentResolver> customArgumentResolvers;
@Nullable
private final StringValueResolver embeddedValueResolver;
private List<HttpServiceArgumentResolver> argumentResolvers;
private final ReactiveAdapterRegistry reactiveAdapterRegistry;
@Nullable
private ConversionService conversionService;
private final Duration blockTimeout;
@Nullable
private StringValueResolver embeddedValueResolver;
private ReactiveAdapterRegistry reactiveAdapterRegistry = ReactiveAdapterRegistry.getSharedInstance();
private Duration blockTimeout = Duration.ofSeconds(5);
private HttpServiceProxyFactory(
HttpClientAdapter clientAdapter, List<HttpServiceArgumentResolver> argumentResolvers,
@Nullable StringValueResolver embeddedValueResolver, ReactiveAdapterRegistry reactiveAdapterRegistry,
Duration blockTimeout) {
/**
* Create an instance with the underlying HTTP client to use.
* @param clientAdapter an adapter for the client
*/
public HttpServiceProxyFactory(HttpClientAdapter clientAdapter) {
Assert.notNull(clientAdapter, "HttpClientAdapter is required");
this.clientAdapter = clientAdapter;
this.argumentResolvers = argumentResolvers;
this.embeddedValueResolver = embeddedValueResolver;
this.reactiveAdapterRegistry = reactiveAdapterRegistry;
}
/**
* Register a custom argument resolver, invoked ahead of default resolvers.
* @param resolver the resolver to add
*/
public void addCustomArgumentResolver(HttpServiceArgumentResolver resolver) {
if (this.customArgumentResolvers == null) {
this.customArgumentResolvers = new ArrayList<>();
}
this.customArgumentResolvers.add(resolver);
}
/**
* Set the custom argument resolvers to use, ahead of default resolvers.
* @param resolvers the resolvers to use
*/
public void setCustomArgumentResolvers(List<HttpServiceArgumentResolver> resolvers) {
this.customArgumentResolvers = new ArrayList<>(resolvers);
}
/**
* Set the {@link ConversionService} to use where input values need to
* be formatted as Strings.
* <p>By default this is {@link DefaultFormattingConversionService}.
*/
public void setConversionService(ConversionService conversionService) {
this.conversionService = conversionService;
}
/**
* Set the StringValueResolver to use for resolving placeholders and
* expressions in {@link HttpExchange#url()}.
* @param resolver the resolver to use
*/
@Override
public void setEmbeddedValueResolver(StringValueResolver resolver) {
this.embeddedValueResolver = resolver;
}
/**
* Set the {@link ReactiveAdapterRegistry} to use to support different
* asynchronous types for HTTP service method return values.
* <p>By default this is {@link ReactiveAdapterRegistry#getSharedInstance()}.
*/
public void setReactiveAdapterRegistry(ReactiveAdapterRegistry registry) {
this.reactiveAdapterRegistry = registry;
}
/**
* Configure how long to wait for a response for an HTTP service method
* with a synchronous (blocking) method signature.
* <p>By default this is 5 seconds.
* @param blockTimeout the timeout value
*/
public void setBlockTimeout(Duration blockTimeout) {
this.blockTimeout = blockTimeout;
}
@Override
public void afterPropertiesSet() throws Exception {
this.conversionService = (this.conversionService != null ?
this.conversionService : new DefaultFormattingConversionService());
this.argumentResolvers = initArgumentResolvers(this.conversionService);
}
private List<HttpServiceArgumentResolver> initArgumentResolvers(ConversionService conversionService) {
List<HttpServiceArgumentResolver> resolvers = new ArrayList<>();
// Custom
if (this.customArgumentResolvers != null) {
resolvers.addAll(this.customArgumentResolvers);
}
// Annotation-based
resolvers.add(new RequestHeaderArgumentResolver(conversionService));
resolvers.add(new RequestBodyArgumentResolver(this.reactiveAdapterRegistry));
resolvers.add(new PathVariableArgumentResolver(conversionService));
resolvers.add(new RequestParamArgumentResolver(conversionService));
resolvers.add(new CookieValueArgumentResolver(conversionService));
resolvers.add(new RequestAttributeArgumentResolver());
// Specific type
resolvers.add(new UrlArgumentResolver());
resolvers.add(new HttpMethodArgumentResolver());
return resolvers;
}
/**
* Return a proxy that implements the given HTTP service interface to perform
* HTTP requests and retrieve responses through an HTTP client.
@@ -81,150 +180,25 @@ public final class HttpServiceProxyFactory {
*/
public <S> S createClient(Class<S> serviceType) {
List<HttpServiceMethod> methods =
MethodIntrospector.selectMethods(serviceType, this::isExchangeMethod)
.stream()
.map(method ->
new HttpServiceMethod(
method, serviceType, this.argumentResolvers, this.clientAdapter,
this.embeddedValueResolver, this.reactiveAdapterRegistry, this.blockTimeout))
List<HttpServiceMethod> httpServiceMethods =
MethodIntrospector.selectMethods(serviceType, this::isExchangeMethod).stream()
.map(method -> createHttpServiceMethod(serviceType, method))
.toList();
return ProxyFactory.getProxy(serviceType, new HttpServiceMethodInterceptor(methods));
return ProxyFactory.getProxy(serviceType, new HttpServiceMethodInterceptor(httpServiceMethods));
}
private boolean isExchangeMethod(Method method) {
return AnnotatedElementUtils.hasAnnotation(method, HttpExchange.class);
}
private <S> HttpServiceMethod createHttpServiceMethod(Class<S> serviceType, Method method) {
Assert.notNull(this.argumentResolvers,
"No argument resolvers: afterPropertiesSet was not called");
/**
* Return a builder for an {@link HttpServiceProxyFactory}.
* @param adapter an adapter for the underlying HTTP client
* @return the builder
*/
public static Builder builder(HttpClientAdapter adapter) {
return new Builder(adapter);
}
/**
* Builder for {@link HttpServiceProxyFactory}.
*/
public final static class Builder {
private final HttpClientAdapter clientAdapter;
private final List<HttpServiceArgumentResolver> customResolvers = new ArrayList<>();
@Nullable
private ConversionService conversionService;
@Nullable
private StringValueResolver embeddedValueResolver;
private ReactiveAdapterRegistry reactiveAdapterRegistry = ReactiveAdapterRegistry.getSharedInstance();
private Duration blockTimeout = Duration.ofSeconds(5);
private Builder(HttpClientAdapter clientAdapter) {
Assert.notNull(clientAdapter, "HttpClientAdapter is required");
this.clientAdapter = clientAdapter;
}
/**
* Register a custom argument resolver. This will be inserted ahead of
* default resolvers.
* @return the same builder instance
*/
public Builder addCustomResolver(HttpServiceArgumentResolver resolver) {
this.customResolvers.add(resolver);
return this;
}
/**
* Set the {@link ConversionService} to use where input values need to
* be formatted as Strings.
* <p>By default this is {@link DefaultFormattingConversionService}.
* @return the same builder instance
*/
public Builder setConversionService(ConversionService conversionService) {
this.conversionService = conversionService;
return this;
}
/**
* Set the StringValueResolver to use for resolving placeholders and
* expressions in {@link HttpExchange#url()}.
* @param embeddedValueResolver the resolver to use
* @return the same builder instance
* @see org.springframework.context.EmbeddedValueResolverAware
*/
public Builder setEmbeddedValueResolver(@Nullable StringValueResolver embeddedValueResolver) {
this.embeddedValueResolver = embeddedValueResolver;
return this;
}
/**
* Set the {@link ReactiveAdapterRegistry} to use to support different
* asynchronous types for HTTP service method return values.
* <p>By default this is {@link ReactiveAdapterRegistry#getSharedInstance()}.
* @return the same builder instance
*/
public Builder setReactiveAdapterRegistry(ReactiveAdapterRegistry registry) {
this.reactiveAdapterRegistry = registry;
return this;
}
/**
* Configure how long to wait for a response for an HTTP service method
* with a synchronous (blocking) method signature.
* <p>By default this is 5 seconds.
* @param blockTimeout the timeout value
* @return the same builder instance
*/
public Builder setBlockTimeout(Duration blockTimeout) {
this.blockTimeout = blockTimeout;
return this;
}
/**
* Build and return the {@link HttpServiceProxyFactory} instance.
*/
public HttpServiceProxyFactory build() {
ConversionService conversionService = initConversionService();
List<HttpServiceArgumentResolver> resolvers = initArgumentResolvers(conversionService);
return new HttpServiceProxyFactory(
this.clientAdapter, resolvers, this.embeddedValueResolver, this.reactiveAdapterRegistry,
this.blockTimeout);
}
private ConversionService initConversionService() {
return (this.conversionService != null ?
this.conversionService : new DefaultFormattingConversionService());
}
private List<HttpServiceArgumentResolver> initArgumentResolvers(ConversionService conversionService) {
List<HttpServiceArgumentResolver> resolvers = new ArrayList<>(this.customResolvers);
// Annotation-based
resolvers.add(new RequestHeaderArgumentResolver(conversionService));
resolvers.add(new RequestBodyArgumentResolver(this.reactiveAdapterRegistry));
resolvers.add(new PathVariableArgumentResolver(conversionService));
resolvers.add(new RequestParamArgumentResolver(conversionService));
resolvers.add(new CookieValueArgumentResolver(conversionService));
resolvers.add(new RequestAttributeArgumentResolver());
// Specific type
resolvers.add(new UrlArgumentResolver());
resolvers.add(new HttpMethodArgumentResolver());
return resolvers;
}
return new HttpServiceMethod(
method, serviceType, this.argumentResolvers, this.clientAdapter,
this.embeddedValueResolver, this.reactiveAdapterRegistry, this.blockTimeout);
}

View File

@@ -18,6 +18,7 @@ package org.springframework.web.service.invoker;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.util.ObjectUtils;
@@ -36,7 +37,15 @@ class CookieValueArgumentResolverTests {
private final TestHttpClientAdapter client = new TestHttpClientAdapter();
private final Service service = HttpServiceProxyFactory.builder(this.client).build().createClient(Service.class);
private Service service;
@BeforeEach
void setUp() throws Exception {
HttpServiceProxyFactory proxyFactory = new HttpServiceProxyFactory(this.client);
proxyFactory.afterPropertiesSet();
this.service = proxyFactory.createClient(Service.class);
}
// Base class functionality should be tested in NamedValueArgumentResolverTests.

View File

@@ -16,6 +16,7 @@
package org.springframework.web.service.invoker;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.http.HttpMethod;
@@ -34,7 +35,15 @@ public class HttpMethodArgumentResolverTests {
private final TestHttpClientAdapter client = new TestHttpClientAdapter();
private final Service service = HttpServiceProxyFactory.builder(this.client).build().createClient(Service.class);
private Service service;
@BeforeEach
void setUp() throws Exception {
HttpServiceProxyFactory proxyFactory = new HttpServiceProxyFactory(this.client);
proxyFactory.afterPropertiesSet();
this.service = proxyFactory.createClient(Service.class);
}
@Test

View File

@@ -21,6 +21,7 @@ import java.util.Optional;
import io.reactivex.rxjava3.core.Completable;
import io.reactivex.rxjava3.core.Flowable;
import io.reactivex.rxjava3.core.Single;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@@ -56,9 +57,16 @@ public class HttpServiceMethodTests {
private static final ParameterizedTypeReference<String> BODY_TYPE = new ParameterizedTypeReference<>() {};
private final TestHttpClientAdapter clientAdapter = new TestHttpClientAdapter();
private final TestHttpClientAdapter client = new TestHttpClientAdapter();
private final HttpServiceProxyFactory proxyFactory = HttpServiceProxyFactory.builder(this.clientAdapter).build();
private HttpServiceProxyFactory proxyFactory;
@BeforeEach
void setUp() throws Exception {
this.proxyFactory = new HttpServiceProxyFactory(this.client);
this.proxyFactory.afterPropertiesSet();
}
@Test
@@ -150,7 +158,7 @@ public class HttpServiceMethodTests {
service.performGet();
HttpRequestValues requestValues = this.clientAdapter.getRequestValues();
HttpRequestValues requestValues = this.client.getRequestValues();
assertThat(requestValues.getHttpMethod()).isEqualTo(HttpMethod.GET);
assertThat(requestValues.getUriTemplate()).isEqualTo("");
assertThat(requestValues.getHeaders().getContentType()).isNull();
@@ -158,7 +166,7 @@ public class HttpServiceMethodTests {
service.performPost();
requestValues = this.clientAdapter.getRequestValues();
requestValues = this.client.getRequestValues();
assertThat(requestValues.getHttpMethod()).isEqualTo(HttpMethod.POST);
assertThat(requestValues.getUriTemplate()).isEqualTo("/url");
assertThat(requestValues.getHeaders().getContentType()).isEqualTo(MediaType.APPLICATION_JSON);
@@ -166,17 +174,17 @@ public class HttpServiceMethodTests {
}
@Test
void typeAndMethodAnnotatedService() {
void typeAndMethodAnnotatedService() throws Exception {
HttpServiceProxyFactory proxyFactory = HttpServiceProxyFactory.builder(this.clientAdapter)
.setEmbeddedValueResolver(value -> (value.equals("${baseUrl}") ? "/base" : value))
.build();
HttpServiceProxyFactory proxyFactory = new HttpServiceProxyFactory(this.client);
proxyFactory.setEmbeddedValueResolver(value -> (value.equals("${baseUrl}") ? "/base" : value));
proxyFactory.afterPropertiesSet();
MethodLevelAnnotatedService service = proxyFactory.createClient(TypeAndMethodLevelAnnotatedService.class);
service.performGet();
HttpRequestValues requestValues = this.clientAdapter.getRequestValues();
HttpRequestValues requestValues = this.client.getRequestValues();
assertThat(requestValues.getHttpMethod()).isEqualTo(HttpMethod.GET);
assertThat(requestValues.getUriTemplate()).isEqualTo("/base");
assertThat(requestValues.getHeaders().getContentType()).isEqualTo(MediaType.APPLICATION_CBOR);
@@ -184,7 +192,7 @@ public class HttpServiceMethodTests {
service.performPost();
requestValues = this.clientAdapter.getRequestValues();
requestValues = this.client.getRequestValues();
assertThat(requestValues.getHttpMethod()).isEqualTo(HttpMethod.POST);
assertThat(requestValues.getUriTemplate()).isEqualTo("/base/url");
assertThat(requestValues.getHeaders().getContentType()).isEqualTo(MediaType.APPLICATION_JSON);
@@ -192,8 +200,8 @@ public class HttpServiceMethodTests {
}
private void verifyClientInvocation(String methodName, @Nullable ParameterizedTypeReference<?> expectedBodyType) {
assertThat((this.clientAdapter.getInvokedMethodName())).isEqualTo(methodName);
assertThat(this.clientAdapter.getBodyType()).isEqualTo(expectedBodyType);
assertThat((this.client.getInvokedMethodName())).isEqualTo(methodName);
assertThat(this.client.getBodyType()).isEqualTo(expectedBodyType);
}

View File

@@ -26,6 +26,7 @@ import java.util.Map;
import java.util.Optional;
import org.apache.groovy.util.Maps;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.core.MethodParameter;
@@ -53,11 +54,17 @@ class NamedValueArgumentResolverTests {
private final TestNamedValueArgumentResolver argumentResolver = new TestNamedValueArgumentResolver();
private final Service service =
HttpServiceProxyFactory.builder(this.client)
.addCustomResolver(this.argumentResolver)
.build()
.createClient(Service.class);
private Service service;
@BeforeEach
void setUp() throws Exception {
HttpServiceProxyFactory proxyFactory = new HttpServiceProxyFactory(this.client);
proxyFactory.addCustomArgumentResolver(this.argumentResolver);
proxyFactory.afterPropertiesSet();
this.service = proxyFactory.createClient(Service.class);
}
@Test

View File

@@ -16,6 +16,7 @@
package org.springframework.web.service.invoker;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.lang.Nullable;
@@ -35,7 +36,15 @@ class PathVariableArgumentResolverTests {
private final TestHttpClientAdapter client = new TestHttpClientAdapter();
private final Service service = HttpServiceProxyFactory.builder(this.client).build().createClient(Service.class);
private Service service;
@BeforeEach
void setUp() throws Exception {
HttpServiceProxyFactory proxyFactory = new HttpServiceProxyFactory(this.client);
proxyFactory.afterPropertiesSet();
this.service = proxyFactory.createClient(Service.class);
}
// Base class functionality should be tested in NamedValueArgumentResolverTests.

View File

@@ -16,6 +16,7 @@
package org.springframework.web.service.invoker;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.lang.Nullable;
@@ -34,7 +35,15 @@ class RequestAttributeArgumentResolverTests {
private final TestHttpClientAdapter client = new TestHttpClientAdapter();
private final Service service = HttpServiceProxyFactory.builder(this.client).build().createClient(Service.class);
private Service service;
@BeforeEach
void setUp() throws Exception {
HttpServiceProxyFactory proxyFactory = new HttpServiceProxyFactory(this.client);
proxyFactory.afterPropertiesSet();
this.service = proxyFactory.createClient(Service.class);
}
// Base class functionality should be tested in NamedValueArgumentResolverTests.

View File

@@ -18,6 +18,7 @@ package org.springframework.web.service.invoker;
import io.reactivex.rxjava3.core.Completable;
import io.reactivex.rxjava3.core.Single;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Mono;
@@ -39,7 +40,15 @@ public class RequestBodyArgumentResolverTests {
private final TestHttpClientAdapter client = new TestHttpClientAdapter();
private final Service service = HttpServiceProxyFactory.builder(this.client).build().createClient(Service.class);
private Service service;
@BeforeEach
void setUp() throws Exception {
HttpServiceProxyFactory proxyFactory = new HttpServiceProxyFactory(this.client);
proxyFactory.afterPropertiesSet();
this.service = proxyFactory.createClient(Service.class);
}
@Test

View File

@@ -18,6 +18,7 @@ package org.springframework.web.service.invoker;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.util.ObjectUtils;
@@ -37,7 +38,15 @@ class RequestHeaderArgumentResolverTests {
private final TestHttpClientAdapter client = new TestHttpClientAdapter();
private final Service service = HttpServiceProxyFactory.builder(this.client).build().createClient(Service.class);
private Service service;
@BeforeEach
void setUp() throws Exception {
HttpServiceProxyFactory proxyFactory = new HttpServiceProxyFactory(this.client);
proxyFactory.afterPropertiesSet();
this.service = proxyFactory.createClient(Service.class);
}
// Base class functionality should be tested in NamedValueArgumentResolverTests.

View File

@@ -16,6 +16,7 @@
package org.springframework.web.service.invoker;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.web.bind.annotation.RequestParam;
@@ -39,7 +40,15 @@ public class RequestParamArgumentResolverTests {
private final TestHttpClientAdapter client = new TestHttpClientAdapter();
private final Service service = HttpServiceProxyFactory.builder(this.client).build().createClient(Service.class);
private Service service;
@BeforeEach
void setUp() throws Exception {
HttpServiceProxyFactory proxyFactory = new HttpServiceProxyFactory(this.client);
proxyFactory.afterPropertiesSet();
this.service = proxyFactory.createClient(Service.class);
}
// Base class functionality should be tested in NamedValueArgumentResolverTests.

View File

@@ -18,6 +18,7 @@ package org.springframework.web.service.invoker;
import java.net.URI;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.web.service.annotation.GetExchange;
@@ -34,7 +35,15 @@ public class UrlArgumentResolverTests {
private final TestHttpClientAdapter client = new TestHttpClientAdapter();
private final Service service = HttpServiceProxyFactory.builder(this.client).build().createClient(Service.class);
private Service service;
@BeforeEach
void setUp() throws Exception {
HttpServiceProxyFactory proxyFactory = new HttpServiceProxyFactory(this.client);
proxyFactory.afterPropertiesSet();
this.service = proxyFactory.createClient(Service.class);
}
@Test

View File

@@ -65,7 +65,7 @@ public class WebClientHttpServiceProxyTests {
@Test
void greeting() {
void greeting() throws Exception {
prepareResponse(response ->
response.setHeader("Content-Type", "text/plain").setBody("Hello Spring!"));
@@ -77,7 +77,7 @@ public class WebClientHttpServiceProxyTests {
}
@Test
void greetingWithRequestAttribute() {
void greetingWithRequestAttribute() throws Exception {
Map<String, Object> attributes = new HashMap<>();
@@ -100,14 +100,15 @@ public class WebClientHttpServiceProxyTests {
assertThat(attributes).containsEntry("myAttribute", "myAttributeValue");
}
private TestHttpService initHttpService() {
private TestHttpService initHttpService() throws Exception {
WebClient webClient = WebClient.builder().baseUrl(this.server.url("/").toString()).build();
return initHttpService(webClient);
}
private TestHttpService initHttpService(WebClient webClient) {
WebClientAdapter clientAdapter = new WebClientAdapter(webClient);
HttpServiceProxyFactory proxyFactory = HttpServiceProxyFactory.builder(clientAdapter).build();
private TestHttpService initHttpService(WebClient webClient) throws Exception {
WebClientAdapter client = new WebClientAdapter(webClient);
HttpServiceProxyFactory proxyFactory = new HttpServiceProxyFactory(client);
proxyFactory.afterPropertiesSet();
return proxyFactory.createClient(TestHttpService.class);
}