Browse Source

HTTP Service proxy sets body type

Closes gh-34793
pull/35405/head
rstoyanchev 8 months ago
parent
commit
c48ff357dc
  1. 15
      spring-web/src/main/java/org/springframework/web/client/support/RestClientAdapter.java
  2. 15
      spring-web/src/main/java/org/springframework/web/client/support/RestTemplateAdapter.java
  3. 32
      spring-web/src/main/java/org/springframework/web/service/invoker/HttpRequestValues.java
  4. 19
      spring-web/src/main/java/org/springframework/web/service/invoker/RequestBodyArgumentResolver.java
  5. 33
      spring-web/src/test/java/org/springframework/web/client/support/RestClientAdapterTests.java
  6. 8
      spring-web/src/test/java/org/springframework/web/service/invoker/RequestBodyArgumentResolverTests.java
  7. 20
      spring-webflux/src/main/java/org/springframework/web/reactive/function/client/support/WebClientAdapter.java
  8. 39
      spring-webflux/src/test/java/org/springframework/web/reactive/function/client/support/WebClientAdapterTests.java

15
spring-web/src/main/java/org/springframework/web/client/support/RestClientAdapter.java

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -85,7 +85,8 @@ public final class RestClientAdapter implements HttpExchangeAdapter { @@ -85,7 +85,8 @@ public final class RestClientAdapter implements HttpExchangeAdapter {
return newRequest(values).retrieve().toEntity(bodyType);
}
private RestClient.RequestBodySpec newRequest(HttpRequestValues values) {
@SuppressWarnings("unchecked")
private <B> RestClient.RequestBodySpec newRequest(HttpRequestValues values) {
HttpMethod httpMethod = values.getHttpMethod();
Assert.notNull(httpMethod, "HttpMethod is required");
@ -123,8 +124,14 @@ public final class RestClientAdapter implements HttpExchangeAdapter { @@ -123,8 +124,14 @@ public final class RestClientAdapter implements HttpExchangeAdapter {
bodySpec.attributes(attributes -> attributes.putAll(values.getAttributes()));
if (values.getBodyValue() != null) {
bodySpec.body(values.getBodyValue());
B body = (B) values.getBodyValue();
if (body != null) {
if (values.getBodyValueType() != null) {
bodySpec.body(body, (ParameterizedTypeReference<? super B>) values.getBodyValueType());
}
else {
bodySpec.body(body);
}
}
return bodySpec;

15
spring-web/src/main/java/org/springframework/web/client/support/RestTemplateAdapter.java

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -86,7 +86,7 @@ public final class RestTemplateAdapter implements HttpExchangeAdapter { @@ -86,7 +86,7 @@ public final class RestTemplateAdapter implements HttpExchangeAdapter {
return this.restTemplate.exchange(newRequest(values), bodyType);
}
private RequestEntity<?> newRequest(HttpRequestValues values) {
private <B> RequestEntity<?> newRequest(HttpRequestValues values) {
HttpMethod httpMethod = values.getHttpMethod();
Assert.notNull(httpMethod, "HttpMethod is required");
@ -120,11 +120,16 @@ public final class RestTemplateAdapter implements HttpExchangeAdapter { @@ -120,11 +120,16 @@ public final class RestTemplateAdapter implements HttpExchangeAdapter {
builder.header(HttpHeaders.COOKIE, String.join("; ", cookies));
}
if (values.getBodyValue() != null) {
return builder.body(values.getBodyValue());
Object body = values.getBodyValue();
if (body == null) {
return builder.build();
}
return builder.build();
if (values.getBodyValueType() != null) {
return builder.body(body, values.getBodyValueType().getType());
}
return builder.body(body);
}

32
spring-web/src/main/java/org/springframework/web/service/invoker/HttpRequestValues.java

@ -23,6 +23,7 @@ import java.util.LinkedHashMap; @@ -23,6 +23,7 @@ import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
@ -75,6 +76,9 @@ public class HttpRequestValues { @@ -75,6 +76,9 @@ public class HttpRequestValues {
@Nullable
private final Object bodyValue;
@Nullable
private ParameterizedTypeReference<?> bodyValueType;
/**
* Construct {@link HttpRequestValues}.
@ -177,6 +181,15 @@ public class HttpRequestValues { @@ -177,6 +181,15 @@ public class HttpRequestValues {
return this.bodyValue;
}
/**
* Return the type for the {@linkplain #getBodyValue() body value}.
* @since 6.2.7
*/
@Nullable
public ParameterizedTypeReference<?> getBodyValueType() {
return this.bodyValueType;
}
public static Builder builder() {
return new Builder();
@ -253,6 +266,9 @@ public class HttpRequestValues { @@ -253,6 +266,9 @@ public class HttpRequestValues {
@Nullable
private Object bodyValue;
@Nullable
private ParameterizedTypeReference<?> bodyValueType;
/**
* Set the HTTP method for the request.
*/
@ -389,6 +405,15 @@ public class HttpRequestValues { @@ -389,6 +405,15 @@ public class HttpRequestValues {
this.bodyValue = bodyValue;
}
/**
* Variant of {@link #setBodyValue(Object)} with the body type.
* @since 6.2.7
*/
public void setBodyValue(@Nullable Object bodyValue, @Nullable ParameterizedTypeReference<?> valueType) {
setBodyValue(bodyValue);
this.bodyValueType = valueType;
}
// Implementation of {@link Metadata} methods
@ -465,9 +490,14 @@ public class HttpRequestValues { @@ -465,9 +490,14 @@ public class HttpRequestValues {
Map<String, Object> attributes = (this.attributes != null ?
new HashMap<>(this.attributes) : Collections.emptyMap());
return createRequestValues(
HttpRequestValues requestValues = createRequestValues(
this.httpMethod, uri, uriBuilderFactory, uriTemplate, uriVars,
headers, cookies, attributes, bodyValue);
// In 6.2.x only, temporarily work around protected methods
requestValues.bodyValueType = this.bodyValueType;
return requestValues;
}
protected boolean hasParts() {

19
spring-web/src/main/java/org/springframework/web/service/invoker/RequestBodyArgumentResolver.java

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -83,15 +83,16 @@ public class RequestBodyArgumentResolver implements HttpServiceArgumentResolver @@ -83,15 +83,16 @@ public class RequestBodyArgumentResolver implements HttpServiceArgumentResolver
if (this.reactiveAdapterRegistry != null) {
ReactiveAdapter adapter = this.reactiveAdapterRegistry.getAdapter(parameter.getParameterType());
if (adapter != null) {
MethodParameter nestedParameter = parameter.nested();
MethodParameter nestedParam = parameter.nested();
String message = "Async type for @RequestBody should produce value(s)";
Assert.isTrue(!adapter.isNoValue(), message);
Assert.isTrue(nestedParameter.getNestedParameterType() != Void.class, message);
Assert.isTrue(nestedParam.getNestedParameterType() != Void.class, message);
if (requestValues instanceof ReactiveHttpRequestValues.Builder reactiveRequestValues) {
reactiveRequestValues.setBodyPublisher(
adapter.toPublisher(argument), asParameterizedTypeRef(nestedParameter));
if (requestValues instanceof ReactiveHttpRequestValues.Builder rrv) {
rrv.setBodyPublisher(
adapter.toPublisher(argument),
ParameterizedTypeReference.forType(nestedParam.getNestedGenericParameterType()));
}
else {
throw new IllegalStateException(
@ -103,12 +104,8 @@ public class RequestBodyArgumentResolver implements HttpServiceArgumentResolver @@ -103,12 +104,8 @@ public class RequestBodyArgumentResolver implements HttpServiceArgumentResolver
}
// Not a reactive type
requestValues.setBodyValue(argument);
requestValues.setBodyValue(argument, ParameterizedTypeReference.forType(parameter.getGenericParameterType()));
return true;
}
private static ParameterizedTypeReference<Object> asParameterizedTypeRef(MethodParameter nestedParam) {
return ParameterizedTypeReference.forType(nestedParam.getNestedGenericParameterType());
}
}

33
spring-web/src/test/java/org/springframework/web/client/support/RestClientAdapterTests.java

@ -22,7 +22,9 @@ import java.lang.annotation.Retention; @@ -22,7 +22,9 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.net.URI;
import java.util.LinkedHashSet;
import java.util.Optional;
import java.util.Set;
import java.util.function.BiFunction;
import java.util.stream.Stream;
@ -267,6 +269,19 @@ class RestClientAdapterTests { @@ -267,6 +269,19 @@ class RestClientAdapterTests {
assertThat(this.anotherServer.getRequestCount()).isEqualTo(0);
}
@ParameterizedAdapterTest // gh-34793
void postSet(MockWebServer server, Service service) throws InterruptedException {
Set<Person> persons = new LinkedHashSet<>();
persons.add(new Person("John"));
persons.add(new Person("Richard"));
service.postPersonSet(persons);
RecordedRequest request = server.takeRequest();
assertThat(request.getMethod()).isEqualTo("POST");
assertThat(request.getPath()).isEqualTo("/persons");
assertThat(request.getBody().readUtf8()).isEqualTo("[{\"name\":\"John\"},{\"name\":\"Richard\"}]");
}
private static MockWebServer anotherServer() {
MockWebServer server = new MockWebServer();
@ -297,6 +312,9 @@ class RestClientAdapterTests { @@ -297,6 +312,9 @@ class RestClientAdapterTests {
@PostExchange
void postMultipart(MultipartFile file, @RequestPart String anotherPart);
@PostExchange(url = "/persons", contentType = MediaType.APPLICATION_JSON_VALUE)
void postPersonSet(@RequestBody Set<Person> set);
@PutExchange
void putWithCookies(@CookieValue String firstCookie, @CookieValue String secondCookie);
@ -315,4 +333,19 @@ class RestClientAdapterTests { @@ -315,4 +333,19 @@ class RestClientAdapterTests {
ResponseEntity<String> getWithIgnoredUriBuilderFactory(URI uri, UriBuilderFactory uriBuilderFactory);
}
static final class Person {
private final String name;
Person(String name) {
this.name = name;
}
public String getName() {
return this.name;
}
}
}

8
spring-web/src/test/java/org/springframework/web/service/invoker/RequestBodyArgumentResolverTests.java

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -54,6 +54,7 @@ class RequestBodyArgumentResolverTests { @@ -54,6 +54,7 @@ class RequestBodyArgumentResolverTests {
this.service.execute(body);
assertThat(getBodyValue()).isEqualTo(body);
assertThat(getBodyValueType()).isEqualTo(new ParameterizedTypeReference<String>() {});
assertThat(getPublisherBody()).isNull();
}
@ -173,6 +174,11 @@ class RequestBodyArgumentResolverTests { @@ -173,6 +174,11 @@ class RequestBodyArgumentResolverTests {
return getReactiveRequestValues().getBodyValue();
}
@Nullable
private ParameterizedTypeReference<?> getBodyValueType() {
return getReactiveRequestValues().getBodyValueType();
}
@Nullable
private Publisher<?> getPublisherBody() {
return getReactiveRequestValues().getBodyPublisher();

20
spring-webflux/src/main/java/org/springframework/web/reactive/function/client/support/WebClientAdapter.java

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -98,8 +98,8 @@ public final class WebClientAdapter extends AbstractReactorHttpExchangeAdapter { @@ -98,8 +98,8 @@ public final class WebClientAdapter extends AbstractReactorHttpExchangeAdapter {
return newRequest(requestValues).retrieve().toEntityFlux(bodyType);
}
@SuppressWarnings("ReactiveStreamsUnusedPublisher")
private WebClient.RequestBodySpec newRequest(HttpRequestValues values) {
@SuppressWarnings({"ReactiveStreamsUnusedPublisher", "unchecked"})
private <B> WebClient.RequestBodySpec newRequest(HttpRequestValues values) {
HttpMethod httpMethod = values.getHttpMethod();
Assert.notNull(httpMethod, "HttpMethod is required");
@ -130,12 +130,18 @@ public final class WebClientAdapter extends AbstractReactorHttpExchangeAdapter { @@ -130,12 +130,18 @@ public final class WebClientAdapter extends AbstractReactorHttpExchangeAdapter {
bodySpec.attributes(attributes -> attributes.putAll(values.getAttributes()));
if (values.getBodyValue() != null) {
bodySpec.bodyValue(values.getBodyValue());
if (values.getBodyValueType() != null) {
B body = (B) values.getBodyValue();
bodySpec.bodyValue(body, (ParameterizedTypeReference<B>) values.getBodyValueType());
}
else {
bodySpec.bodyValue(values.getBodyValue());
}
}
else if (values instanceof ReactiveHttpRequestValues reactiveRequestValues) {
Publisher<?> body = reactiveRequestValues.getBodyPublisher();
else if (values instanceof ReactiveHttpRequestValues rhrv) {
Publisher<?> body = rhrv.getBodyPublisher();
if (body != null) {
ParameterizedTypeReference<?> elementType = reactiveRequestValues.getBodyPublisherElementType();
ParameterizedTypeReference<?> elementType = rhrv.getBodyPublisherElementType();
Assert.notNull(elementType, "Publisher body element type is required");
bodySpec.body(body, elementType);
}

39
spring-webflux/src/test/java/org/springframework/web/reactive/function/client/support/WebClientAdapterTests.java

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -21,7 +21,9 @@ import java.io.IOException; @@ -21,7 +21,9 @@ import java.io.IOException;
import java.net.URI;
import java.time.Duration;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
import okhttp3.mockwebserver.MockResponse;
@ -39,6 +41,7 @@ import org.springframework.util.LinkedMultiValueMap; @@ -39,6 +41,7 @@ import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestAttribute;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.multipart.MultipartFile;
@ -168,6 +171,22 @@ class WebClientAdapterTests { @@ -168,6 +171,22 @@ class WebClientAdapterTests {
"Content-Type: text/plain;charset=UTF-8", "Content-Length: 5", "test2");
}
@Test // gh-34793
void postSet() throws InterruptedException {
prepareResponse(response -> response.setResponseCode(201));
Set<Person> persons = new LinkedHashSet<>();
persons.add(new Person("John"));
persons.add(new Person("Richard"));
initService().postPersonSet(persons);
RecordedRequest request = server.takeRequest();
assertThat(request.getMethod()).isEqualTo("POST");
assertThat(request.getPath()).isEqualTo("/persons");
assertThat(request.getBody().readUtf8()).isEqualTo("[{\"name\":\"John\"},{\"name\":\"Richard\"}]");
}
@Test
void uriBuilderFactory() throws Exception {
String ignoredResponseBody = "hello";
@ -251,6 +270,9 @@ class WebClientAdapterTests { @@ -251,6 +270,9 @@ class WebClientAdapterTests {
@PostExchange
void postMultipart(MultipartFile file, @RequestPart String anotherPart);
@PostExchange("/persons")
void postPersonSet(@RequestBody Set<Person> set);
@GetExchange("/greeting")
String getWithUriBuilderFactory(UriBuilderFactory uriBuilderFactory);
@ -263,4 +285,19 @@ class WebClientAdapterTests { @@ -263,4 +285,19 @@ class WebClientAdapterTests {
}
static final class Person {
private final String name;
Person(String name) {
this.name = name;
}
public String getName() {
return this.name;
}
}
}

Loading…
Cancel
Save