diff --git a/spring-web/src/main/java/org/springframework/web/service/invoker/HttpServiceProxyFactory.java b/spring-web/src/main/java/org/springframework/web/service/invoker/HttpServiceProxyFactory.java index 1ffc1051495..1ab49e2cdb9 100644 --- a/spring-web/src/main/java/org/springframework/web/service/invoker/HttpServiceProxyFactory.java +++ b/spring-web/src/main/java/org/springframework/web/service/invoker/HttpServiceProxyFactory.java @@ -18,9 +18,11 @@ package org.springframework.web.service.invoker; import java.lang.reflect.Method; import java.time.Duration; -import java.util.HashMap; +import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; @@ -29,62 +31,167 @@ import org.springframework.aop.framework.ProxyFactory; import org.springframework.core.MethodIntrospector; import org.springframework.core.ReactiveAdapterRegistry; 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; /** - * 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 * @since 6.0 */ -public class HttpServiceProxyFactory { - - private final List argumentResolvers; +public final class HttpServiceProxyFactory { private final HttpClientAdapter clientAdapter; + private final List argumentResolvers; + private final ReactiveAdapterRegistry reactiveAdapterRegistry; private final Duration blockTimeout; - public HttpServiceProxyFactory( - List argumentResolvers, HttpClientAdapter clientAdapter, + private HttpServiceProxyFactory( + HttpClientAdapter clientAdapter, List argumentResolvers, ReactiveAdapterRegistry reactiveAdapterRegistry, Duration blockTimeout) { - this.argumentResolvers = argumentResolvers; this.clientAdapter = clientAdapter; + this.argumentResolvers = argumentResolvers; this.reactiveAdapterRegistry = reactiveAdapterRegistry; 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 the service type + * @param the HTTP service type * @return the created proxy */ public S createClient(Class serviceType) { List methods = - MethodIntrospector.selectMethods(serviceType, this::isHttpRequestMethod) + MethodIntrospector.selectMethods(serviceType, this::isExchangeMethod) .stream() - .map(method -> initServiceMethod(method, serviceType)) + .map(method -> + new HttpServiceMethod( + method, serviceType, this.argumentResolvers, + this.clientAdapter, this.reactiveAdapterRegistry, this.blockTimeout)) .toList(); return ProxyFactory.getProxy(serviceType, new HttpServiceMethodInterceptor(methods)); } - private boolean isHttpRequestMethod(Method method) { + private boolean isExchangeMethod(Method method) { return AnnotatedElementUtils.hasAnnotation(method, HttpExchange.class); } - private HttpServiceMethod initServiceMethod(Method method, Class serviceType) { - return new HttpServiceMethod( - method, serviceType, this.argumentResolvers, - this.clientAdapter, this.reactiveAdapterRegistry, this.blockTimeout); + + /** + * 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 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. + *

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. + *

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. + *

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 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 initArgumentResolvers(ConversionService conversionService) { + List 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 final Map httpServiceMethods = new HashMap<>(); + private final Map httpServiceMethods; private HttpServiceMethodInterceptor(List methods) { - methods.forEach(serviceMethod -> this.httpServiceMethods.put(serviceMethod.getMethod(), serviceMethod)); + this.httpServiceMethods = methods.stream() + .collect(Collectors.toMap(HttpServiceMethod::getMethod, Function.identity())); } @Override @@ -105,7 +213,6 @@ public class HttpServiceProxyFactory { HttpServiceMethod httpServiceMethod = this.httpServiceMethods.get(method); return httpServiceMethod.invoke(invocation.getArguments()); } - } } diff --git a/spring-web/src/test/java/org/springframework/web/service/invoker/HttpMethodArgumentResolverTests.java b/spring-web/src/test/java/org/springframework/web/service/invoker/HttpMethodArgumentResolverTests.java index a40db1d3ce3..58dda0306a7 100644 --- a/spring-web/src/test/java/org/springframework/web/service/invoker/HttpMethodArgumentResolverTests.java +++ b/spring-web/src/test/java/org/springframework/web/service/invoker/HttpMethodArgumentResolverTests.java @@ -34,7 +34,7 @@ public class HttpMethodArgumentResolverTests { 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 diff --git a/spring-web/src/test/java/org/springframework/web/service/invoker/PathVariableArgumentResolverTests.java b/spring-web/src/test/java/org/springframework/web/service/invoker/PathVariableArgumentResolverTests.java index a050b7606a9..8db7873a553 100644 --- a/spring-web/src/test/java/org/springframework/web/service/invoker/PathVariableArgumentResolverTests.java +++ b/spring-web/src/test/java/org/springframework/web/service/invoker/PathVariableArgumentResolverTests.java @@ -21,7 +21,6 @@ import java.util.Optional; import org.junit.jupiter.api.Test; -import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.lang.Nullable; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.service.annotation.GetExchange; @@ -39,8 +38,7 @@ class PathVariableArgumentResolverTests { private final TestHttpClientAdapter clientAdapter = new TestHttpClientAdapter(); - private final Service service = this.clientAdapter.createService( - Service.class, new PathVariableArgumentResolver(new DefaultConversionService())); + private final Service service = this.clientAdapter.createService(Service.class); @Test diff --git a/spring-web/src/test/java/org/springframework/web/service/invoker/TestHttpClientAdapter.java b/spring-web/src/test/java/org/springframework/web/service/invoker/TestHttpClientAdapter.java index 06bff5e3c8f..69094897523 100644 --- a/spring-web/src/test/java/org/springframework/web/service/invoker/TestHttpClientAdapter.java +++ b/spring-web/src/test/java/org/springframework/web/service/invoker/TestHttpClientAdapter.java @@ -16,14 +16,10 @@ package org.springframework.web.service.invoker; -import java.time.Duration; -import java.util.Arrays; - import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import org.springframework.core.ParameterizedTypeReference; -import org.springframework.core.ReactiveAdapterRegistry; import org.springframework.http.HttpHeaders; import org.springframework.http.ResponseEntity; import org.springframework.lang.Nullable; @@ -53,11 +49,8 @@ class TestHttpClientAdapter implements HttpClientAdapter { /** * Create the proxy for the give service type. */ - public S createService(Class serviceType, HttpServiceArgumentResolver... resolvers) { - - HttpServiceProxyFactory factory = new HttpServiceProxyFactory( - Arrays.asList(resolvers), this, ReactiveAdapterRegistry.getSharedInstance(), Duration.ofSeconds(5)); - + public S createService(Class serviceType) { + HttpServiceProxyFactory factory = HttpServiceProxyFactory.builder(this).build(); return factory.createClient(serviceType); } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/support/WebClientHttpServiceProxyTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/support/WebClientHttpServiceProxyTests.java index 44a33ca138f..8010bb4ac61 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/support/WebClientHttpServiceProxyTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/support/WebClientHttpServiceProxyTests.java @@ -19,7 +19,6 @@ package org.springframework.web.reactive.function.client.support; import java.io.IOException; import java.time.Duration; -import java.util.Collections; import java.util.function.Consumer; import okhttp3.mockwebserver.MockResponse; @@ -30,7 +29,6 @@ import org.junit.jupiter.api.Test; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; -import org.springframework.core.ReactiveAdapterRegistry; import org.springframework.http.client.reactive.ReactorClientHttpConnector; import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.service.annotation.GetExchange; @@ -59,11 +57,8 @@ public class WebClientHttpServiceProxyTests { .baseUrl(this.server.url("/").toString()) .build(); - WebClientAdapter webClientAdapter = new WebClientAdapter(webClient); - - HttpServiceProxyFactory proxyFactory = new HttpServiceProxyFactory( - Collections.emptyList(), webClientAdapter, ReactiveAdapterRegistry.getSharedInstance(), - Duration.ofSeconds(5)); + WebClientAdapter clientAdapter = new WebClientAdapter(webClient); + HttpServiceProxyFactory proxyFactory = HttpServiceProxyFactory.builder(clientAdapter).build(); this.httpService = proxyFactory.createClient(TestHttpService.class); }