Add Reactor classpath checks in argument resolvers

HTTP interface client argument resolvers for RequestBody and
RequestPart now handle reactive input conditionally.

See gh-30117
This commit is contained in:
rstoyanchev
2023-07-10 16:03:11 +01:00
parent 22376c2efa
commit 3209cf5c7a
6 changed files with 77 additions and 51 deletions

View File

@@ -22,8 +22,10 @@ import org.springframework.http.ResponseEntity;
import org.springframework.lang.Nullable;
/**
* Contract to abstract an underlying HTTP client and decouple it from the
* {@linkplain HttpServiceProxyFactory#createClient(Class) HTTP service proxy}.
* Contract to abstract an HTTP client from {@linkplain HttpServiceProxyFactory}
* and make it pluggable.
*
* <p>For reactive clients, see {@link ReactorHttpExchangeAdapter}.
*
* @author Rossen Stoyanchev
* @since 6.1

View File

@@ -43,7 +43,7 @@ import org.springframework.web.util.UriUtils;
* Container for HTTP request values extracted from an
* {@link org.springframework.web.service.annotation.HttpExchange @HttpExchange}-annotated
* method and argument values passed to it. This is then given to
* {@link HttpClientAdapter} to adapt to the underlying HTTP client.
* {@link HttpExchangeAdapter} to adapt to the underlying HTTP client.
*
* @author Rossen Stoyanchev
* @since 6.0

View File

@@ -53,6 +53,7 @@ import org.springframework.web.service.annotation.HttpExchange;
*
* @author Rossen Stoyanchev
* @since 6.0
* @see org.springframework.web.client.support.RestTemplateAdapter
* @see org.springframework.web.reactive.function.client.support.WebClientAdapter
*/
public final class HttpServiceProxyFactory {
@@ -107,6 +108,7 @@ public final class HttpServiceProxyFactory {
/**
* Return a builder that's initialized with the given client.
* @since 6.1
*/
public static Builder builderFor(HttpExchangeAdapter exchangeAdapter) {
return new Builder().exchangeAdapter(exchangeAdapter);
@@ -114,7 +116,8 @@ public final class HttpServiceProxyFactory {
/**
* Return a builder that's initialized with the given client.
* @deprecated in favor of {@link #builderFor(HttpExchangeAdapter)}
* @deprecated in favor of {@link #builderFor(HttpExchangeAdapter)};
* to be removed in 6.2.
*/
@SuppressWarnings("removal")
@Deprecated(since = "6.1", forRemoval = true)
@@ -164,7 +167,8 @@ public final class HttpServiceProxyFactory {
* Provide the HTTP client to perform requests through.
* @param clientAdapter a client adapted to {@link HttpClientAdapter}
* @return this same builder instance
* @deprecated in favor of {@link #exchangeAdapter(HttpExchangeAdapter)}
* @deprecated in favor of {@link #exchangeAdapter(HttpExchangeAdapter)};
* to be removed in 6.2
*/
@SuppressWarnings("removal")
@Deprecated(since = "6.1", forRemoval = true)
@@ -260,10 +264,10 @@ public final class HttpServiceProxyFactory {
// Annotation-based
resolvers.add(new RequestHeaderArgumentResolver(service));
resolvers.add(new RequestBodyArgumentResolver());
resolvers.add(new RequestBodyArgumentResolver(this.exchangeAdapter));
resolvers.add(new PathVariableArgumentResolver(service));
resolvers.add(new RequestParamArgumentResolver(service));
resolvers.add(new RequestPartArgumentResolver());
resolvers.add(new RequestPartArgumentResolver(this.exchangeAdapter));
resolvers.add(new CookieValueArgumentResolver(service));
if (this.exchangeAdapter.supportsRequestAttributes()) {
resolvers.add(new RequestAttributeArgumentResolver());

View File

@@ -28,8 +28,8 @@ import org.springframework.http.ResponseEntity;
import org.springframework.lang.Nullable;
/**
* Contract to abstract a Project Reactor, HTTP client to decouple it from the
* {@linkplain HttpServiceProxyFactory#createClient(Class) HTTP service proxy}.
* Contract to abstract a reactive, HTTP client from
* {@linkplain HttpServiceProxyFactory} and make it pluggable.
*
* @author Rossen Stoyanchev
* @since 6.1

View File

@@ -22,6 +22,7 @@ import org.springframework.core.ReactiveAdapter;
import org.springframework.core.ReactiveAdapterRegistry;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.web.bind.annotation.RequestBody;
/**
@@ -33,23 +34,28 @@ import org.springframework.web.bind.annotation.RequestBody;
*/
public class RequestBodyArgumentResolver implements HttpServiceArgumentResolver {
private static final boolean REACTOR_PRESENT =
ClassUtils.isPresent("reactor.core.publisher.Mono", RequestBodyArgumentResolver.class.getClassLoader());
@Nullable
private final ReactiveAdapterRegistry reactiveAdapterRegistry;
/**
* Default constructor that uses {@link ReactiveAdapterRegistry#getSharedInstance()}.
* Constructor with a {@link HttpExchangeAdapter}, for access to config settings.
* @since 6.1
*/
public RequestBodyArgumentResolver() {
this(ReactiveAdapterRegistry.getSharedInstance());
}
/**
* Constructor with a {@link ReactiveAdapterRegistry}.
*/
public RequestBodyArgumentResolver(ReactiveAdapterRegistry reactiveAdapterRegistry) {
Assert.notNull(reactiveAdapterRegistry, "ReactiveAdapterRegistry is required");
this.reactiveAdapterRegistry = reactiveAdapterRegistry;
public RequestBodyArgumentResolver(HttpExchangeAdapter exchangeAdapter) {
if (REACTOR_PRESENT) {
this.reactiveAdapterRegistry =
(exchangeAdapter instanceof ReactorHttpExchangeAdapter reactorAdapter ?
reactorAdapter.getReactiveAdapterRegistry() :
ReactiveAdapterRegistry.getSharedInstance());
}
else {
this.reactiveAdapterRegistry = null;
}
}
@@ -63,21 +69,25 @@ public class RequestBodyArgumentResolver implements HttpServiceArgumentResolver
}
if (argument != null) {
ReactiveAdapter reactiveAdapter = this.reactiveAdapterRegistry.getAdapter(parameter.getParameterType());
if (reactiveAdapter == null) {
requestValues.setBodyValue(argument);
}
else {
MethodParameter nestedParameter = parameter.nested();
if (this.reactiveAdapterRegistry != null) {
ReactiveAdapter adapter = this.reactiveAdapterRegistry.getAdapter(parameter.getParameterType());
if (adapter != null) {
MethodParameter nestedParameter = parameter.nested();
String message = "Async type for @RequestBody should produce value(s)";
Assert.isTrue(!reactiveAdapter.isNoValue(), message);
Assert.isTrue(nestedParameter.getNestedParameterType() != Void.class, message);
String message = "Async type for @RequestBody should produce value(s)";
Assert.isTrue(!adapter.isNoValue(), message);
Assert.isTrue(nestedParameter.getNestedParameterType() != Void.class, message);
requestValues.setBody(
reactiveAdapter.toPublisher(argument),
ParameterizedTypeReference.forType(nestedParameter.getNestedGenericParameterType()));
requestValues.setBody(
adapter.toPublisher(argument),
ParameterizedTypeReference.forType(nestedParameter.getNestedGenericParameterType()));
return true;
}
}
// Not a reactive type
requestValues.setBodyValue(argument);
}
return true;

View File

@@ -24,7 +24,9 @@ import org.springframework.core.ReactiveAdapterRegistry;
import org.springframework.core.ResolvableType;
import org.springframework.http.HttpEntity;
import org.springframework.http.codec.multipart.Part;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.web.bind.annotation.RequestPart;
/**
@@ -47,22 +49,28 @@ import org.springframework.web.bind.annotation.RequestPart;
*/
public class RequestPartArgumentResolver extends AbstractNamedValueArgumentResolver {
private static final boolean REACTOR_PRESENT =
ClassUtils.isPresent("reactor.core.publisher.Mono", RequestPartArgumentResolver.class.getClassLoader());
@Nullable
private final ReactiveAdapterRegistry reactiveAdapterRegistry;
/**
* Default constructor that uses {@link ReactiveAdapterRegistry#getSharedInstance()}.
* Constructor with a {@link HttpExchangeAdapter}, for access to config settings.
* @since 6.1
*/
public RequestPartArgumentResolver() {
this(ReactiveAdapterRegistry.getSharedInstance());
}
/**
* Constructor with a {@link ReactiveAdapterRegistry}.
*/
public RequestPartArgumentResolver(ReactiveAdapterRegistry reactiveAdapterRegistry) {
this.reactiveAdapterRegistry = reactiveAdapterRegistry;
public RequestPartArgumentResolver(HttpExchangeAdapter exchangeAdapter) {
if (REACTOR_PRESENT) {
this.reactiveAdapterRegistry =
(exchangeAdapter instanceof ReactorHttpExchangeAdapter reactorAdapter ?
reactorAdapter.getReactiveAdapterRegistry() :
ReactiveAdapterRegistry.getSharedInstance());
}
else {
this.reactiveAdapterRegistry = null;
}
}
@@ -77,16 +85,18 @@ public class RequestPartArgumentResolver extends AbstractNamedValueArgumentResol
protected void addRequestValue(
String name, Object value, MethodParameter parameter, HttpRequestValues.Builder requestValues) {
Class<?> type = parameter.getParameterType();
ReactiveAdapter adapter = this.reactiveAdapterRegistry.getAdapter(type);
if (adapter != null) {
Assert.isTrue(!adapter.isNoValue(), "Expected publisher that produces a value");
Publisher<?> publisher = adapter.toPublisher(value);
requestValues.addRequestPart(name, publisher, ResolvableType.forMethodParameter(parameter.nested()));
}
else {
requestValues.addRequestPart(name, value);
if (this.reactiveAdapterRegistry != null) {
Class<?> type = parameter.getParameterType();
ReactiveAdapter adapter = this.reactiveAdapterRegistry.getAdapter(type);
if (adapter != null) {
Assert.isTrue(!adapter.isNoValue(), "Expected publisher that produces a value");
Publisher<?> publisher = adapter.toPublisher(value);
requestValues.addRequestPart(name, publisher, ResolvableType.forMethodParameter(parameter.nested()));
return;
}
}
requestValues.addRequestPart(name, value);
}
}