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;