Add HttpServiceProxyFactory builder

See gh-28386
This commit is contained in:
rstoyanchev 2022-04-27 17:02:44 +01:00
parent 8a46e96875
commit b1384ddafa
5 changed files with 133 additions and 40 deletions

View File

@ -18,9 +18,11 @@ package org.springframework.web.service.invoker;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.time.Duration; import java.time.Duration;
import java.util.HashMap; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation; import org.aopalliance.intercept.MethodInvocation;
@ -29,62 +31,167 @@ import org.springframework.aop.framework.ProxyFactory;
import org.springframework.core.MethodIntrospector; import org.springframework.core.MethodIntrospector;
import org.springframework.core.ReactiveAdapterRegistry; import org.springframework.core.ReactiveAdapterRegistry;
import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.convert.ConversionService;
import org.springframework.format.support.DefaultFormattingConversionService;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.web.service.annotation.HttpExchange; import org.springframework.web.service.annotation.HttpExchange;
/** /**
* Factory to create a proxy for an HTTP service with {@link HttpExchange} methods. * Factory for creating a client proxy given an HTTP service interface with
* {@link HttpExchange @HttpExchange} methods.
* *
* @author Rossen Stoyanchev * @author Rossen Stoyanchev
* @since 6.0 * @since 6.0
*/ */
public class HttpServiceProxyFactory { public final class HttpServiceProxyFactory {
private final List<HttpServiceArgumentResolver> argumentResolvers;
private final HttpClientAdapter clientAdapter; private final HttpClientAdapter clientAdapter;
private final List<HttpServiceArgumentResolver> argumentResolvers;
private final ReactiveAdapterRegistry reactiveAdapterRegistry; private final ReactiveAdapterRegistry reactiveAdapterRegistry;
private final Duration blockTimeout; private final Duration blockTimeout;
public HttpServiceProxyFactory( private HttpServiceProxyFactory(
List<HttpServiceArgumentResolver> argumentResolvers, HttpClientAdapter clientAdapter, HttpClientAdapter clientAdapter, List<HttpServiceArgumentResolver> argumentResolvers,
ReactiveAdapterRegistry reactiveAdapterRegistry, Duration blockTimeout) { ReactiveAdapterRegistry reactiveAdapterRegistry, Duration blockTimeout) {
this.argumentResolvers = argumentResolvers;
this.clientAdapter = clientAdapter; this.clientAdapter = clientAdapter;
this.argumentResolvers = argumentResolvers;
this.reactiveAdapterRegistry = reactiveAdapterRegistry; this.reactiveAdapterRegistry = reactiveAdapterRegistry;
this.blockTimeout = blockTimeout; this.blockTimeout = blockTimeout;
} }
/** /**
* Create a proxy for executing requests to the given HTTP service. * Return a proxy that implements the given HTTP service interface to perform
* HTTP requests and retrieves responses through an HTTP client.
* @param serviceType the HTTP service to create a proxy for * @param serviceType the HTTP service to create a proxy for
* @param <S> the service type * @param <S> the HTTP service type
* @return the created proxy * @return the created proxy
*/ */
public <S> S createClient(Class<S> serviceType) { public <S> S createClient(Class<S> serviceType) {
List<HttpServiceMethod> methods = List<HttpServiceMethod> methods =
MethodIntrospector.selectMethods(serviceType, this::isHttpRequestMethod) MethodIntrospector.selectMethods(serviceType, this::isExchangeMethod)
.stream() .stream()
.map(method -> initServiceMethod(method, serviceType)) .map(method ->
new HttpServiceMethod(
method, serviceType, this.argumentResolvers,
this.clientAdapter, this.reactiveAdapterRegistry, this.blockTimeout))
.toList(); .toList();
return ProxyFactory.getProxy(serviceType, new HttpServiceMethodInterceptor(methods)); return ProxyFactory.getProxy(serviceType, new HttpServiceMethodInterceptor(methods));
} }
private boolean isHttpRequestMethod(Method method) { private boolean isExchangeMethod(Method method) {
return AnnotatedElementUtils.hasAnnotation(method, HttpExchange.class); return AnnotatedElementUtils.hasAnnotation(method, HttpExchange.class);
} }
private HttpServiceMethod initServiceMethod(Method method, Class<?> serviceType) {
return new HttpServiceMethod( /**
method, serviceType, this.argumentResolvers, * Return a builder for an {@link HttpServiceProxyFactory}.
this.clientAdapter, this.reactiveAdapterRegistry, this.blockTimeout); * @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;
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 {@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.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);
resolvers.add(new HttpMethodArgumentResolver());
resolvers.add(new PathVariableArgumentResolver(conversionService));
return resolvers;
}
} }
@ -93,10 +200,11 @@ public class HttpServiceProxyFactory {
*/ */
private static final class HttpServiceMethodInterceptor implements MethodInterceptor { private static final class HttpServiceMethodInterceptor implements MethodInterceptor {
private final Map<Method, HttpServiceMethod> httpServiceMethods = new HashMap<>(); private final Map<Method, HttpServiceMethod> httpServiceMethods;
private HttpServiceMethodInterceptor(List<HttpServiceMethod> methods) { private HttpServiceMethodInterceptor(List<HttpServiceMethod> methods) {
methods.forEach(serviceMethod -> this.httpServiceMethods.put(serviceMethod.getMethod(), serviceMethod)); this.httpServiceMethods = methods.stream()
.collect(Collectors.toMap(HttpServiceMethod::getMethod, Function.identity()));
} }
@Override @Override
@ -105,7 +213,6 @@ public class HttpServiceProxyFactory {
HttpServiceMethod httpServiceMethod = this.httpServiceMethods.get(method); HttpServiceMethod httpServiceMethod = this.httpServiceMethods.get(method);
return httpServiceMethod.invoke(invocation.getArguments()); return httpServiceMethod.invoke(invocation.getArguments());
} }
} }
} }

View File

@ -34,7 +34,7 @@ public class HttpMethodArgumentResolverTests {
private final TestHttpClientAdapter clientAdapter = new TestHttpClientAdapter(); private final TestHttpClientAdapter clientAdapter = new TestHttpClientAdapter();
private final Service service = this.clientAdapter.createService(Service.class, new HttpMethodArgumentResolver()); private final Service service = this.clientAdapter.createService(Service.class);
@Test @Test

View File

@ -21,7 +21,6 @@ import java.util.Optional;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.service.annotation.GetExchange; import org.springframework.web.service.annotation.GetExchange;
@ -39,8 +38,7 @@ class PathVariableArgumentResolverTests {
private final TestHttpClientAdapter clientAdapter = new TestHttpClientAdapter(); private final TestHttpClientAdapter clientAdapter = new TestHttpClientAdapter();
private final Service service = this.clientAdapter.createService( private final Service service = this.clientAdapter.createService(Service.class);
Service.class, new PathVariableArgumentResolver(new DefaultConversionService()));
@Test @Test

View File

@ -16,14 +16,10 @@
package org.springframework.web.service.invoker; package org.springframework.web.service.invoker;
import java.time.Duration;
import java.util.Arrays;
import reactor.core.publisher.Flux; import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import org.springframework.core.ParameterizedTypeReference; import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.ReactiveAdapterRegistry;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
@ -53,11 +49,8 @@ class TestHttpClientAdapter implements HttpClientAdapter {
/** /**
* Create the proxy for the give service type. * Create the proxy for the give service type.
*/ */
public <S> S createService(Class<S> serviceType, HttpServiceArgumentResolver... resolvers) { public <S> S createService(Class<S> serviceType) {
HttpServiceProxyFactory factory = HttpServiceProxyFactory.builder(this).build();
HttpServiceProxyFactory factory = new HttpServiceProxyFactory(
Arrays.asList(resolvers), this, ReactiveAdapterRegistry.getSharedInstance(), Duration.ofSeconds(5));
return factory.createClient(serviceType); return factory.createClient(serviceType);
} }

View File

@ -19,7 +19,6 @@ package org.springframework.web.reactive.function.client.support;
import java.io.IOException; import java.io.IOException;
import java.time.Duration; import java.time.Duration;
import java.util.Collections;
import java.util.function.Consumer; import java.util.function.Consumer;
import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockResponse;
@ -30,7 +29,6 @@ import org.junit.jupiter.api.Test;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import reactor.test.StepVerifier; import reactor.test.StepVerifier;
import org.springframework.core.ReactiveAdapterRegistry;
import org.springframework.http.client.reactive.ReactorClientHttpConnector; import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.service.annotation.GetExchange; import org.springframework.web.service.annotation.GetExchange;
@ -59,11 +57,8 @@ public class WebClientHttpServiceProxyTests {
.baseUrl(this.server.url("/").toString()) .baseUrl(this.server.url("/").toString())
.build(); .build();
WebClientAdapter webClientAdapter = new WebClientAdapter(webClient); WebClientAdapter clientAdapter = new WebClientAdapter(webClient);
HttpServiceProxyFactory proxyFactory = HttpServiceProxyFactory.builder(clientAdapter).build();
HttpServiceProxyFactory proxyFactory = new HttpServiceProxyFactory(
Collections.emptyList(), webClientAdapter, ReactiveAdapterRegistry.getSharedInstance(),
Duration.ofSeconds(5));
this.httpService = proxyFactory.createClient(TestHttpService.class); this.httpService = proxyFactory.createClient(TestHttpService.class);
} }