From 3209cf5c7a03ded467cfc5380e586f32853790b3 Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Mon, 10 Jul 2023 16:03:11 +0100 Subject: [PATCH] Add Reactor classpath checks in argument resolvers HTTP interface client argument resolvers for RequestBody and RequestPart now handle reactive input conditionally. See gh-30117 --- .../service/invoker/HttpExchangeAdapter.java | 6 +- .../service/invoker/HttpRequestValues.java | 2 +- .../invoker/HttpServiceProxyFactory.java | 12 ++-- .../invoker/ReactorHttpExchangeAdapter.java | 4 +- .../invoker/RequestBodyArgumentResolver.java | 56 +++++++++++-------- .../invoker/RequestPartArgumentResolver.java | 48 +++++++++------- 6 files changed, 77 insertions(+), 51 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/web/service/invoker/HttpExchangeAdapter.java b/spring-web/src/main/java/org/springframework/web/service/invoker/HttpExchangeAdapter.java index 3333e27fbc7..8e78bbe6103 100644 --- a/spring-web/src/main/java/org/springframework/web/service/invoker/HttpExchangeAdapter.java +++ b/spring-web/src/main/java/org/springframework/web/service/invoker/HttpExchangeAdapter.java @@ -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. + * + *

For reactive clients, see {@link ReactorHttpExchangeAdapter}. * * @author Rossen Stoyanchev * @since 6.1 diff --git a/spring-web/src/main/java/org/springframework/web/service/invoker/HttpRequestValues.java b/spring-web/src/main/java/org/springframework/web/service/invoker/HttpRequestValues.java index 2e9178c787c..a58d639f8e2 100644 --- a/spring-web/src/main/java/org/springframework/web/service/invoker/HttpRequestValues.java +++ b/spring-web/src/main/java/org/springframework/web/service/invoker/HttpRequestValues.java @@ -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 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 6f45ec13d61..da1e67622f3 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 @@ -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()); diff --git a/spring-web/src/main/java/org/springframework/web/service/invoker/ReactorHttpExchangeAdapter.java b/spring-web/src/main/java/org/springframework/web/service/invoker/ReactorHttpExchangeAdapter.java index 47f08906ba1..466fda215e9 100644 --- a/spring-web/src/main/java/org/springframework/web/service/invoker/ReactorHttpExchangeAdapter.java +++ b/spring-web/src/main/java/org/springframework/web/service/invoker/ReactorHttpExchangeAdapter.java @@ -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 diff --git a/spring-web/src/main/java/org/springframework/web/service/invoker/RequestBodyArgumentResolver.java b/spring-web/src/main/java/org/springframework/web/service/invoker/RequestBodyArgumentResolver.java index 8a3738e4f9a..01246a3cf81 100644 --- a/spring-web/src/main/java/org/springframework/web/service/invoker/RequestBodyArgumentResolver.java +++ b/spring-web/src/main/java/org/springframework/web/service/invoker/RequestBodyArgumentResolver.java @@ -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; diff --git a/spring-web/src/main/java/org/springframework/web/service/invoker/RequestPartArgumentResolver.java b/spring-web/src/main/java/org/springframework/web/service/invoker/RequestPartArgumentResolver.java index 54807b11fe1..65a147e1d6f 100644 --- a/spring-web/src/main/java/org/springframework/web/service/invoker/RequestPartArgumentResolver.java +++ b/spring-web/src/main/java/org/springframework/web/service/invoker/RequestPartArgumentResolver.java @@ -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); } }