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);
}