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 6e8bcdbf747..ff93da66639 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 @@ -16,6 +16,7 @@ package org.springframework.web.service.invoker; +import java.lang.reflect.Method; import java.net.URI; import java.util.Collections; import java.util.HashMap; @@ -176,6 +177,9 @@ public class HttpRequestValues { } + /** + * Return a builder for {@link HttpRequestValues}. + */ public static Builder builder() { return new Builder(); } @@ -209,6 +213,28 @@ public class HttpRequestValues { } + /** + * A contract that allows further customization of {@link HttpRequestValues} + * in addition to those added by argument resolvers. + *

Use {@link HttpServiceProxyFactory.Builder#httpRequestValuesProcessor(Processor)} + * to add such a processor. + * @since 7.0 + */ + public interface Processor { + + /** + * Invoked after argument resolvers have been called, and before the + * {@link HttpRequestValues} is built. + * @param method the {@code @HttpExchange} method + * @param arguments the raw argument values to the method + * @param builder the builder to add request values too; the builder + * also exposes method {@link Metadata} from the {@code HttpExchange} method. + */ + void process(Method method, @Nullable Object[] arguments, Builder builder); + + } + + /** * Builder for {@link HttpRequestValues}. */ @@ -238,6 +264,9 @@ public class HttpRequestValues { private @Nullable Object bodyValue; + protected Builder() { + } + /** * Set the HTTP method for the request. */ 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 b66610c4566..a07ef8d76a3 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 @@ -77,6 +77,8 @@ final class HttpServiceMethod { private final List argumentResolvers; + private final HttpRequestValues.Processor requestValuesProcessor; + private final HttpRequestValuesInitializer requestValuesInitializer; private final ResponseFunction responseFunction; @@ -84,11 +86,13 @@ final class HttpServiceMethod { HttpServiceMethod( Method method, Class containingClass, List argumentResolvers, - HttpExchangeAdapter adapter, @Nullable StringValueResolver embeddedValueResolver) { + HttpRequestValues.Processor valuesProcessor, HttpExchangeAdapter adapter, + @Nullable StringValueResolver embeddedValueResolver) { this.method = method; this.parameters = initMethodParameters(method); this.argumentResolvers = argumentResolvers; + this.requestValuesProcessor = valuesProcessor; boolean isReactorAdapter = (REACTOR_PRESENT && adapter instanceof ReactorHttpExchangeAdapter); @@ -129,6 +133,7 @@ final class HttpServiceMethod { public @Nullable Object invoke(@Nullable Object[] arguments) { HttpRequestValues.Builder requestValues = this.requestValuesInitializer.initializeRequestValuesBuilder(); applyArguments(requestValues, arguments); + this.requestValuesProcessor.process(this.method, arguments, requestValues); return this.responseFunction.execute(requestValues.build()); } 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 3bff90f04a2..ab492b6d370 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 @@ -58,15 +58,19 @@ public final class HttpServiceProxyFactory { private final List argumentResolvers; + private final HttpRequestValues.Processor requestValuesProcessor; + private final @Nullable StringValueResolver embeddedValueResolver; private HttpServiceProxyFactory( HttpExchangeAdapter exchangeAdapter, List argumentResolvers, + List requestValuesProcessor, @Nullable StringValueResolver embeddedValueResolver) { this.exchangeAdapter = exchangeAdapter; this.argumentResolvers = argumentResolvers; + this.requestValuesProcessor = new CompositeHttpRequestValuesProcessor(requestValuesProcessor); this.embeddedValueResolver = embeddedValueResolver; } @@ -97,7 +101,8 @@ public final class HttpServiceProxyFactory { "No argument resolvers: afterPropertiesSet was not called"); return new HttpServiceMethod( - method, serviceType, this.argumentResolvers, this.exchangeAdapter, this.embeddedValueResolver); + method, serviceType, this.argumentResolvers, this.requestValuesProcessor, + this.exchangeAdapter, this.embeddedValueResolver); } @@ -126,6 +131,8 @@ public final class HttpServiceProxyFactory { private final List customArgumentResolvers = new ArrayList<>(); + private final List requestValuesProcessors = new ArrayList<>(); + private @Nullable ConversionService conversionService; private @Nullable StringValueResolver embeddedValueResolver; @@ -154,6 +161,18 @@ public final class HttpServiceProxyFactory { return this; } + /** + * Register an {@link HttpRequestValues} processor that can further + * customize request values based on the method and all arguments. + * @param processor the processor to add + * @return this same builder instance + * @since 7.0 + */ + public Builder httpRequestValuesProcessor(HttpRequestValues.Processor processor) { + this.requestValuesProcessors.add(processor); + return this; + } + /** * Set the {@link ConversionService} to use where input values need to * be formatted as Strings. @@ -183,7 +202,8 @@ public final class HttpServiceProxyFactory { Assert.notNull(this.exchangeAdapter, "HttpClientAdapter is required"); return new HttpServiceProxyFactory( - this.exchangeAdapter, initArgumentResolvers(), this.embeddedValueResolver); + this.exchangeAdapter, initArgumentResolvers(), this.requestValuesProcessors, + this.embeddedValueResolver); } @SuppressWarnings({"DataFlowIssue", "NullAway"}) @@ -251,7 +271,21 @@ public final class HttpServiceProxyFactory { System.arraycopy(args, 0, functionArgs, 0, args.length - 1); return functionArgs; } + } + + + /** + * Processor that delegates to a list of other processors. + */ + private record CompositeHttpRequestValuesProcessor(List processors) + implements HttpRequestValues.Processor { + @Override + public void process(Method method, @Nullable Object[] arguments, HttpRequestValues.Builder builder) { + for (HttpRequestValues.Processor processor : this.processors) { + processor.process(method, arguments, builder); + } + } } } diff --git a/spring-web/src/main/java/org/springframework/web/service/invoker/ReactiveHttpRequestValues.java b/spring-web/src/main/java/org/springframework/web/service/invoker/ReactiveHttpRequestValues.java index e2bf12b22d8..4783edb0641 100644 --- a/spring-web/src/main/java/org/springframework/web/service/invoker/ReactiveHttpRequestValues.java +++ b/spring-web/src/main/java/org/springframework/web/service/invoker/ReactiveHttpRequestValues.java @@ -94,6 +94,9 @@ public final class ReactiveHttpRequestValues extends HttpRequestValues { private @Nullable ParameterizedTypeReference bodyElementType; + private Builder() { + } + @Override public Builder setHttpMethod(HttpMethod httpMethod) { super.setHttpMethod(httpMethod); 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 24ccd974811..11eb5be9943 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 @@ -198,12 +198,12 @@ class HttpServiceMethodTests { @Test void typeAndMethodAnnotatedService() { - HttpServiceProxyFactory proxyFactory = HttpServiceProxyFactory.builder() - .exchangeAdapter(this.client) - .embeddedValueResolver(value -> (value.equals("${baseUrl}") ? "/base" : value)) - .build(); - MethodLevelAnnotatedService service = proxyFactory.createClient(TypeAndMethodLevelAnnotatedService.class); + MethodLevelAnnotatedService service = HttpServiceProxyFactory.builder() + .exchangeAdapter(this.client) + .embeddedValueResolver(value -> (value.equals("${baseUrl}") ? "/base" : value)) + .build() + .createClient(TypeAndMethodLevelAnnotatedService.class); service.performGet(); @@ -222,6 +222,19 @@ class HttpServiceMethodTests { assertThat(requestValues.getHeaders().getAccept()).containsOnly(MediaType.APPLICATION_JSON); } + @Test + void httpRequestValuesProcessor() { + + HttpServiceProxyFactory.builder() + .exchangeAdapter(this.client) + .httpRequestValuesProcessor((m, a, builder) -> builder.addAttribute("foo", "a")) + .build() + .createClient(Service.class) + .execute(); + + assertThat(this.client.getRequestValues().getAttributes().get("foo")).isEqualTo("a"); + } + @Test // gh-32049 void multipleAnnotationsAtClassLevel() { Class serviceInterface = MultipleClassLevelAnnotationsService.class;