From 2a2fba6a37666b41205565b7d1a804373a6635d2 Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Thu, 19 May 2022 20:39:12 +0100 Subject: [PATCH] Resolve placeholders in HttpExchange#url Closes gh-28492 --- .../service/invoker/HttpServiceMethod.java | 20 +++++++++---- .../invoker/HttpServiceProxyFactory.java | 30 ++++++++++++++++--- .../invoker/HttpServiceMethodTests.java | 8 +++-- 3 files changed, 46 insertions(+), 12 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/web/service/invoker/HttpServiceMethod.java b/spring-web/src/main/java/org/springframework/web/service/invoker/HttpServiceMethod.java index b6307045b8..3b33f5a8af 100644 --- a/spring-web/src/main/java/org/springframework/web/service/invoker/HttpServiceMethod.java +++ b/spring-web/src/main/java/org/springframework/web/service/invoker/HttpServiceMethod.java @@ -42,6 +42,7 @@ import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; +import org.springframework.util.StringValueResolver; import org.springframework.web.service.annotation.HttpExchange; /** @@ -67,13 +68,13 @@ final class HttpServiceMethod { HttpServiceMethod( Method method, Class containingClass, List argumentResolvers, - HttpClientAdapter client, ReactiveAdapterRegistry reactiveRegistry, - Duration blockTimeout) { + HttpClientAdapter client, @Nullable StringValueResolver embeddedValueResolver, + ReactiveAdapterRegistry reactiveRegistry, Duration blockTimeout) { this.method = method; this.parameters = initMethodParameters(method); this.argumentResolvers = argumentResolvers; - this.requestValuesInitializer = HttpRequestValuesInitializer.create(method, containingClass); + this.requestValuesInitializer = HttpRequestValuesInitializer.create(method, containingClass, embeddedValueResolver); this.responseFunction = ResponseFunction.create(client, method, reactiveRegistry, blockTimeout); } @@ -161,7 +162,8 @@ final class HttpServiceMethod { /** * Introspect the method and create the request factory for it. */ - public static HttpRequestValuesInitializer create(Method method, Class containingClass) { + public static HttpRequestValuesInitializer create( + Method method, Class containingClass, @Nullable StringValueResolver embeddedValueResolver) { HttpExchange annot1 = AnnotatedElementUtils.findMergedAnnotation(containingClass, HttpExchange.class); HttpExchange annot2 = AnnotatedElementUtils.findMergedAnnotation(method, HttpExchange.class); @@ -169,7 +171,7 @@ final class HttpServiceMethod { Assert.notNull(annot2, "Expected HttpRequest annotation"); HttpMethod httpMethod = initHttpMethod(annot1, annot2); - String url = initUrl(annot1, annot2); + String url = initUrl(annot1, annot2, embeddedValueResolver); MediaType contentType = initContentType(annot1, annot2); List acceptableMediaTypes = initAccept(annot1, annot2); @@ -194,11 +196,17 @@ final class HttpServiceMethod { } @Nullable - private static String initUrl(@Nullable HttpExchange typeAnnot, HttpExchange annot) { + private static String initUrl( + @Nullable HttpExchange typeAnnot, HttpExchange annot, @Nullable StringValueResolver embeddedValueResolver) { String url1 = (typeAnnot != null ? typeAnnot.url() : null); String url2 = annot.url(); + if (embeddedValueResolver != null) { + url1 = (url1 != null ? embeddedValueResolver.resolveStringValue(url1) : null); + url2 = embeddedValueResolver.resolveStringValue(url2); + } + boolean hasUrl1 = StringUtils.hasText(url1); boolean hasUrl2 = StringUtils.hasText(url2); 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 0ec408e185..f8dcb081d5 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 @@ -35,6 +35,7 @@ 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.util.StringValueResolver; import org.springframework.web.service.annotation.HttpExchange; /** @@ -50,6 +51,9 @@ public final class HttpServiceProxyFactory { private final List argumentResolvers; + @Nullable + private final StringValueResolver embeddedValueResolver; + private final ReactiveAdapterRegistry reactiveAdapterRegistry; private final Duration blockTimeout; @@ -57,10 +61,12 @@ public final class HttpServiceProxyFactory { private HttpServiceProxyFactory( HttpClientAdapter clientAdapter, List argumentResolvers, - ReactiveAdapterRegistry reactiveAdapterRegistry, Duration blockTimeout) { + @Nullable StringValueResolver embeddedValueResolver, ReactiveAdapterRegistry reactiveAdapterRegistry, + Duration blockTimeout) { this.clientAdapter = clientAdapter; this.argumentResolvers = argumentResolvers; + this.embeddedValueResolver = embeddedValueResolver; this.reactiveAdapterRegistry = reactiveAdapterRegistry; this.blockTimeout = blockTimeout; } @@ -80,8 +86,8 @@ public final class HttpServiceProxyFactory { .stream() .map(method -> new HttpServiceMethod( - method, serviceType, this.argumentResolvers, - this.clientAdapter, this.reactiveAdapterRegistry, this.blockTimeout)) + method, serviceType, this.argumentResolvers, this.clientAdapter, + this.embeddedValueResolver, this.reactiveAdapterRegistry, this.blockTimeout)) .toList(); return ProxyFactory.getProxy(serviceType, new HttpServiceMethodInterceptor(methods)); @@ -114,6 +120,9 @@ public final class HttpServiceProxyFactory { @Nullable private ConversionService conversionService; + @Nullable + private StringValueResolver embeddedValueResolver; + private ReactiveAdapterRegistry reactiveAdapterRegistry = ReactiveAdapterRegistry.getSharedInstance(); private Duration blockTimeout = Duration.ofSeconds(5); @@ -144,6 +153,18 @@ public final class HttpServiceProxyFactory { 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. @@ -176,7 +197,8 @@ public final class HttpServiceProxyFactory { List resolvers = initArgumentResolvers(conversionService); return new HttpServiceProxyFactory( - this.clientAdapter, resolvers, this.reactiveAdapterRegistry, this.blockTimeout); + this.clientAdapter, resolvers, this.embeddedValueResolver, this.reactiveAdapterRegistry, + this.blockTimeout); } private ConversionService initConversionService() { diff --git a/spring-web/src/test/java/org/springframework/web/service/invoker/HttpServiceMethodTests.java b/spring-web/src/test/java/org/springframework/web/service/invoker/HttpServiceMethodTests.java index 2802a1ed01..5046471001 100644 --- a/spring-web/src/test/java/org/springframework/web/service/invoker/HttpServiceMethodTests.java +++ b/spring-web/src/test/java/org/springframework/web/service/invoker/HttpServiceMethodTests.java @@ -168,7 +168,11 @@ public class HttpServiceMethodTests { @Test void typeAndMethodAnnotatedService() { - MethodLevelAnnotatedService service = this.proxyFactory.createClient(TypeAndMethodLevelAnnotatedService.class); + HttpServiceProxyFactory proxyFactory = HttpServiceProxyFactory.builder(this.clientAdapter) + .setEmbeddedValueResolver(value -> (value.equals("${baseUrl}") ? "/base" : value)) + .build(); + + MethodLevelAnnotatedService service = proxyFactory.createClient(TypeAndMethodLevelAnnotatedService.class); service.performGet(); @@ -281,7 +285,7 @@ public class HttpServiceMethodTests { @SuppressWarnings("unused") - @HttpExchange(url = "/base", contentType = APPLICATION_CBOR_VALUE, accept = APPLICATION_CBOR_VALUE) + @HttpExchange(url = "${baseUrl}", contentType = APPLICATION_CBOR_VALUE, accept = APPLICATION_CBOR_VALUE) private interface TypeAndMethodLevelAnnotatedService extends MethodLevelAnnotatedService { }