Skip to content

Commit 3329abf

Browse files
committed
Allow to specify request body type in RestTemplate
This commit allows to specify the request body type in order to serialize generic types with a GenericHttpMessageConverter if needed. Issue: SPR-13154
1 parent 7b1fcfc commit 3329abf

File tree

5 files changed

+165
-12
lines changed

5 files changed

+165
-12
lines changed

spring-web/src/main/java/org/springframework/http/RequestEntity.java

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2015 the original author or authors.
2+
* Copyright 2002-2016 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,6 +16,7 @@
1616

1717
package org.springframework.http;
1818

19+
import java.lang.reflect.Type;
1920
import java.net.URI;
2021
import java.nio.charset.Charset;
2122
import java.util.Arrays;
@@ -54,6 +55,7 @@
5455
* </pre>
5556
*
5657
* @author Arjen Poutsma
58+
* @author Sebastien Deleuze
5759
* @since 4.1
5860
* @see #getMethod()
5961
* @see #getUrl()
@@ -64,6 +66,8 @@ public class RequestEntity<T> extends HttpEntity<T> {
6466

6567
private final URI url;
6668

69+
private final Type type;
70+
6771

6872
/**
6973
* Constructor with method and URL but without body nor headers.
@@ -81,7 +85,19 @@ public RequestEntity(HttpMethod method, URI url) {
8185
* @param url the URL
8286
*/
8387
public RequestEntity(T body, HttpMethod method, URI url) {
84-
this(body, null, method, url);
88+
this(body, null, method, url, null);
89+
}
90+
91+
/**
92+
* Constructor with method, URL, body and type but without headers.
93+
* @param body the body
94+
* @param method the method
95+
* @param url the URL
96+
* @param type the type used for generic type resolution
97+
* @since 4.3
98+
*/
99+
public RequestEntity(T body, HttpMethod method, URI url, Type type) {
100+
this(body, null, method, url, type);
85101
}
86102

87103
/**
@@ -91,7 +107,7 @@ public RequestEntity(T body, HttpMethod method, URI url) {
91107
* @param url the URL
92108
*/
93109
public RequestEntity(MultiValueMap<String, String> headers, HttpMethod method, URI url) {
94-
this(null, headers, method, url);
110+
this(null, headers, method, url, null);
95111
}
96112

97113
/**
@@ -102,9 +118,23 @@ public RequestEntity(MultiValueMap<String, String> headers, HttpMethod method, U
102118
* @param url the URL
103119
*/
104120
public RequestEntity(T body, MultiValueMap<String, String> headers, HttpMethod method, URI url) {
121+
this(body, headers, method, url, null);
122+
}
123+
124+
/**
125+
* Constructor with method, URL, headers, body and type.
126+
* @param body the body
127+
* @param headers the headers
128+
* @param method the method
129+
* @param url the URL
130+
* @param type the type used for generic type resolution
131+
* @since 4.3
132+
*/
133+
public RequestEntity(T body, MultiValueMap<String, String> headers, HttpMethod method, URI url, Type type) {
105134
super(body, headers);
106135
this.method = method;
107136
this.url = url;
137+
this.type = type;
108138
}
109139

110140

@@ -124,6 +154,14 @@ public URI getUrl() {
124154
return this.url;
125155
}
126156

157+
/**
158+
* Return the type of the request's body.
159+
* @return the request's body type
160+
* @since 4.3
161+
*/
162+
public Type getType() {
163+
return (this.type == null && this.getBody() != null ? this.getBody().getClass() : this.type );
164+
}
127165

128166
@Override
129167
public boolean equals(Object other) {
@@ -327,6 +365,16 @@ public interface BodyBuilder extends HeadersBuilder<BodyBuilder> {
327365
* @return the built request entity
328366
*/
329367
<T> RequestEntity<T> body(T body);
368+
369+
/**
370+
* Set the body and type of the request entity and build the RequestEntity.
371+
* @param <T> the type of the body
372+
* @param body the body of the request entity
373+
* @param type the type of the body, useful for generic type resolution
374+
* @return the built request entity
375+
* @since 4.3
376+
*/
377+
<T> RequestEntity<T> body(T body, Type type);
330378
}
331379

332380

@@ -396,6 +444,11 @@ public RequestEntity<Void> build() {
396444
public <T> RequestEntity<T> body(T body) {
397445
return new RequestEntity<T>(body, this.headers, this.method, this.url);
398446
}
447+
448+
@Override
449+
public <T> RequestEntity<T> body(T body, Type type) {
450+
return new RequestEntity<T>(body, this.headers, this.method, this.url, type);
451+
}
399452
}
400453

401454
}

spring-web/src/main/java/org/springframework/web/client/RestTemplate.java

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -782,11 +782,34 @@ public void doWithRequest(ClientHttpRequest httpRequest) throws IOException {
782782
}
783783
else {
784784
Object requestBody = this.requestEntity.getBody();
785-
Class<?> requestType = requestBody.getClass();
785+
Class<?> requestBodyClass = requestBody.getClass();
786+
Type requestBodyType = (this.requestEntity instanceof RequestEntity ?
787+
((RequestEntity<?>)this.requestEntity).getType() : requestBodyClass);
786788
HttpHeaders requestHeaders = this.requestEntity.getHeaders();
787789
MediaType requestContentType = requestHeaders.getContentType();
788790
for (HttpMessageConverter<?> messageConverter : getMessageConverters()) {
789-
if (messageConverter.canWrite(requestType, requestContentType)) {
791+
if (messageConverter instanceof GenericHttpMessageConverter) {
792+
GenericHttpMessageConverter<Object> genericMessageConverter = (GenericHttpMessageConverter<Object>) messageConverter;
793+
if (genericMessageConverter.canWrite(requestBodyType, requestBodyClass, requestContentType)) {
794+
if (!requestHeaders.isEmpty()) {
795+
httpRequest.getHeaders().putAll(requestHeaders);
796+
}
797+
if (logger.isDebugEnabled()) {
798+
if (requestContentType != null) {
799+
logger.debug("Writing [" + requestBody + "] as \"" + requestContentType +
800+
"\" using [" + messageConverter + "]");
801+
}
802+
else {
803+
logger.debug("Writing [" + requestBody + "] using [" + messageConverter + "]");
804+
}
805+
806+
}
807+
genericMessageConverter.write(
808+
requestBody, requestBodyType, requestContentType, httpRequest);
809+
return;
810+
}
811+
}
812+
else if (messageConverter.canWrite(requestBodyClass, requestContentType)) {
790813
if (!requestHeaders.isEmpty()) {
791814
httpRequest.getHeaders().putAll(requestHeaders);
792815
}
@@ -806,7 +829,7 @@ public void doWithRequest(ClientHttpRequest httpRequest) throws IOException {
806829
}
807830
}
808831
String message = "Could not write request: no suitable HttpMessageConverter found for request type [" +
809-
requestType.getName() + "]";
832+
requestBodyClass.getName() + "]";
810833
if (requestContentType != null) {
811834
message += " and content type [" + requestContentType + "]";
812835
}

spring-web/src/test/java/org/springframework/http/RequestEntityTests.java

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2015 the original author or authors.
2+
* Copyright 2002-2016 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -19,11 +19,14 @@
1919
import java.net.URI;
2020
import java.net.URISyntaxException;
2121
import java.nio.charset.Charset;
22+
import java.util.Arrays;
2223
import java.util.HashMap;
24+
import java.util.List;
2325
import java.util.Map;
2426

2527
import org.junit.Test;
2628

29+
import org.springframework.core.ParameterizedTypeReference;
2730
import org.springframework.web.util.UriTemplate;
2831

2932
import static org.junit.Assert.*;
@@ -148,4 +151,14 @@ public void methods() throws URISyntaxException {
148151

149152
}
150153

154+
@Test // SPR-13154
155+
public void types() throws URISyntaxException {
156+
URI url = new URI("http://example.com");
157+
List<String> body = Arrays.asList("foo", "bar");
158+
ParameterizedTypeReference<?> typeReference = new ParameterizedTypeReference<List<String>>() {};
159+
160+
RequestEntity<?> entity = RequestEntity.post(url).body(body, typeReference.getType());
161+
assertEquals(typeReference.getType(), entity.getType());
162+
}
163+
151164
}

spring-web/src/test/java/org/springframework/web/client/RestTemplateIntegrationTests.java

Lines changed: 67 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2015 the original author or authors.
2+
* Copyright 2002-2016 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -20,18 +20,24 @@
2020
import java.net.URI;
2121
import java.net.URISyntaxException;
2222
import java.nio.charset.Charset;
23+
import java.util.ArrayList;
2324
import java.util.EnumSet;
25+
import java.util.List;
2426
import java.util.Set;
2527

28+
import com.fasterxml.jackson.annotation.JsonTypeInfo;
29+
import com.fasterxml.jackson.annotation.JsonTypeName;
2630
import org.junit.Test;
2731

32+
import org.springframework.core.ParameterizedTypeReference;
2833
import org.springframework.core.io.ClassPathResource;
2934
import org.springframework.core.io.Resource;
3035
import org.springframework.http.HttpEntity;
3136
import org.springframework.http.HttpHeaders;
3237
import org.springframework.http.HttpMethod;
3338
import org.springframework.http.HttpStatus;
3439
import org.springframework.http.MediaType;
40+
import org.springframework.http.RequestEntity;
3541
import org.springframework.http.ResponseEntity;
3642
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
3743
import org.springframework.http.converter.json.MappingJacksonValue;
@@ -215,7 +221,7 @@ public void jsonPostForObject() throws URISyntaxException {
215221
bean.setWith2("with");
216222
bean.setWithout("without");
217223
HttpEntity<MySampleBean> entity = new HttpEntity<MySampleBean>(bean, entityHeaders);
218-
String s = template.postForObject(baseUrl + "/jsonpost", entity, String.class, "post");
224+
String s = template.postForObject(baseUrl + "/jsonpost", entity, String.class);
219225
assertTrue(s.contains("\"with1\":\"with\""));
220226
assertTrue(s.contains("\"with2\":\"with\""));
221227
assertTrue(s.contains("\"without\":\"without\""));
@@ -229,7 +235,7 @@ public void jsonPostForObjectWithJacksonView() throws URISyntaxException {
229235
MappingJacksonValue jacksonValue = new MappingJacksonValue(bean);
230236
jacksonValue.setSerializationView(MyJacksonView1.class);
231237
HttpEntity<MappingJacksonValue> entity = new HttpEntity<MappingJacksonValue>(jacksonValue, entityHeaders);
232-
String s = template.postForObject(baseUrl + "/jsonpost", entity, String.class, "post");
238+
String s = template.postForObject(baseUrl + "/jsonpost", entity, String.class);
233239
assertTrue(s.contains("\"with1\":\"with\""));
234240
assertFalse(s.contains("\"with2\":\"with\""));
235241
assertFalse(s.contains("\"without\":\"without\""));
@@ -243,6 +249,21 @@ public void serverPort() {
243249
assertEquals("Invalid content", helloWorld, s);
244250
}
245251

252+
@Test // SPR-13154
253+
public void jsonPostForObjectWithJacksonTypeInfoList() throws URISyntaxException {
254+
List<ParentClass> list = new ArrayList<>();
255+
list.add(new Foo("foo"));
256+
list.add(new Bar("bar"));
257+
ParameterizedTypeReference<?> typeReference = new ParameterizedTypeReference<List<ParentClass>>() {};
258+
RequestEntity<List<ParentClass>> entity = RequestEntity
259+
.post(new URI(baseUrl + "/jsonpost"))
260+
.contentType(new MediaType("application", "json", Charset.forName("UTF-8")))
261+
.body(list, typeReference.getType());
262+
String content = template.exchange(entity, String.class).getBody();
263+
assertTrue(content.contains("\"type\":\"foo\""));
264+
assertTrue(content.contains("\"type\":\"bar\""));
265+
}
266+
246267
public interface MyJacksonView1 {};
247268
public interface MyJacksonView2 {};
248269

@@ -290,4 +311,47 @@ public void setWithout(String without) {
290311
}
291312
}
292313

314+
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type")
315+
public static class ParentClass {
316+
317+
private String parentProperty;
318+
319+
public ParentClass() {
320+
}
321+
322+
public ParentClass(String parentProperty) {
323+
this.parentProperty = parentProperty;
324+
}
325+
326+
public String getParentProperty() {
327+
return parentProperty;
328+
}
329+
330+
public void setParentProperty(String parentProperty) {
331+
this.parentProperty = parentProperty;
332+
}
333+
}
334+
335+
@JsonTypeName("foo")
336+
public static class Foo extends ParentClass {
337+
338+
public Foo() {
339+
}
340+
341+
public Foo(String parentProperty) {
342+
super(parentProperty);
343+
}
344+
}
345+
346+
@JsonTypeName("bar")
347+
public static class Bar extends ParentClass {
348+
349+
public Bar() {
350+
}
351+
352+
public Bar(String parentProperty) {
353+
super(parentProperty);
354+
}
355+
}
356+
293357
}

spring-web/src/test/java/org/springframework/web/client/RestTemplateTests.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -699,9 +699,9 @@ public void exchangeParameterizedType() throws Exception {
699699
given(requestFactory.createRequest(new URI("http://example.com"), HttpMethod.POST)).willReturn(this.request);
700700
HttpHeaders requestHeaders = new HttpHeaders();
701701
given(this.request.getHeaders()).willReturn(requestHeaders);
702-
given(converter.canWrite(String.class, null)).willReturn(true);
702+
given(converter.canWrite(String.class, String.class, null)).willReturn(true);
703703
String requestBody = "Hello World";
704-
converter.write(requestBody, null, this.request);
704+
converter.write(requestBody, String.class, null, this.request);
705705
given(this.request.execute()).willReturn(response);
706706
given(errorHandler.hasError(response)).willReturn(false);
707707
List<Integer> expected = Collections.singletonList(42);

0 commit comments

Comments
 (0)