Fix issue with @RequestBody args that are type vars
The change to support generic @RequestBody arguments introduced in 3.2 M2 also introduced a regression in reading arguments that are type variables. This change fixes the issue. Issue: SPR-9964
This commit is contained in:
@@ -27,6 +27,7 @@ import org.springframework.http.MediaType;
|
||||
* request into a target object of a specified generic type.
|
||||
*
|
||||
* @author Arjen Poutsma
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 3.2
|
||||
*
|
||||
* @see org.springframework.core.ParameterizedTypeReference
|
||||
@@ -36,11 +37,13 @@ public interface GenericHttpMessageConverter<T> extends HttpMessageConverter<T>
|
||||
/**
|
||||
* Indicates whether the given type can be read by this converter.
|
||||
* @param type the type to test for readability
|
||||
* @param contextClass a context class for the target type, for example a class
|
||||
* in which the target type appears in a method signature, can be {@code null}
|
||||
* @param mediaType the media type to read, can be {@code null} if not specified.
|
||||
* Typically the value of a {@code Content-Type} header.
|
||||
* @return {@code true} if readable; {@code false} otherwise
|
||||
*/
|
||||
boolean canRead(Type type, MediaType mediaType);
|
||||
boolean canRead(Type type, Class<?> contextClass, MediaType mediaType);
|
||||
|
||||
/**
|
||||
* Read an object of the given type form the given input message, and returns it.
|
||||
@@ -48,12 +51,14 @@ public interface GenericHttpMessageConverter<T> extends HttpMessageConverter<T>
|
||||
* been passed to the {@link #canRead canRead} method of this interface,
|
||||
* which must have returned {@code true}.
|
||||
* @param type the type of the target object
|
||||
* @param contextClass a context class for the target type, for example a class
|
||||
* in which the target type appears in a method signature, can be {@code null}
|
||||
* @param inputMessage the HTTP input message to read from
|
||||
* @return the converted object
|
||||
* @throws IOException in case of I/O errors
|
||||
* @throws HttpMessageNotReadableException in case of conversion errors
|
||||
*/
|
||||
T read(Type type, HttpInputMessage inputMessage)
|
||||
T read(Type type, Class<?> contextClass, HttpInputMessage inputMessage)
|
||||
throws IOException, HttpMessageNotReadableException;
|
||||
|
||||
}
|
||||
|
||||
@@ -128,11 +128,11 @@ public class MappingJackson2HttpMessageConverter extends AbstractHttpMessageConv
|
||||
|
||||
@Override
|
||||
public boolean canRead(Class<?> clazz, MediaType mediaType) {
|
||||
return canRead((Type) clazz, mediaType);
|
||||
return canRead((Type) clazz, null, mediaType);
|
||||
}
|
||||
|
||||
public boolean canRead(Type type, MediaType mediaType) {
|
||||
JavaType javaType = getJavaType(type);
|
||||
public boolean canRead(Type type, Class<?> contextClass, MediaType mediaType) {
|
||||
JavaType javaType = getJavaType(type, contextClass);
|
||||
return (this.objectMapper.canDeserialize(javaType) && canRead(mediaType));
|
||||
}
|
||||
|
||||
@@ -151,14 +151,14 @@ public class MappingJackson2HttpMessageConverter extends AbstractHttpMessageConv
|
||||
protected Object readInternal(Class<?> clazz, HttpInputMessage inputMessage)
|
||||
throws IOException, HttpMessageNotReadableException {
|
||||
|
||||
JavaType javaType = getJavaType(clazz);
|
||||
JavaType javaType = getJavaType(clazz, null);
|
||||
return readJavaType(javaType, inputMessage);
|
||||
}
|
||||
|
||||
public Object read(Type type, HttpInputMessage inputMessage)
|
||||
public Object read(Type type, Class<?> contextClass, HttpInputMessage inputMessage)
|
||||
throws IOException, HttpMessageNotReadableException {
|
||||
|
||||
JavaType javaType = getJavaType(type);
|
||||
JavaType javaType = getJavaType(type, contextClass);
|
||||
return readJavaType(javaType, inputMessage);
|
||||
}
|
||||
|
||||
@@ -197,10 +197,10 @@ public class MappingJackson2HttpMessageConverter extends AbstractHttpMessageConv
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return the Jackson {@link JavaType} for the specified type.
|
||||
* <p>The default implementation returns {@link ObjectMapper#constructType(java.lang.reflect.Type)},
|
||||
* Return the Jackson {@link JavaType} for the specified type and context class.
|
||||
* <p>The default implementation returns {@link ObjectMapper#constructType(java.lang.reflect.Type)}
|
||||
* or {@code ObjectMapper.getTypeFactory().constructType(type, contextClass)},
|
||||
* but this can be overridden in subclasses, to allow for custom generic collection handling.
|
||||
* For instance:
|
||||
* <pre class="code">
|
||||
@@ -213,10 +213,15 @@ public class MappingJackson2HttpMessageConverter extends AbstractHttpMessageConv
|
||||
* }
|
||||
* </pre>
|
||||
* @param type the type to return the java type for
|
||||
* @param contextClass a context class for the target type, for example a class
|
||||
* in which the target type appears in a method signature, can be {@code null}
|
||||
* signature, can be {@code null}
|
||||
* @return the java type
|
||||
*/
|
||||
protected JavaType getJavaType(Type type) {
|
||||
return this.objectMapper.constructType(type);
|
||||
protected JavaType getJavaType(Type type, Class<?> contextClass) {
|
||||
return (contextClass != null) ?
|
||||
this.objectMapper.getTypeFactory().constructType(type, contextClass) :
|
||||
this.objectMapper.constructType(type);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -128,11 +128,11 @@ public class MappingJacksonHttpMessageConverter extends AbstractHttpMessageConve
|
||||
|
||||
@Override
|
||||
public boolean canRead(Class<?> clazz, MediaType mediaType) {
|
||||
return canRead((Type) clazz, mediaType);
|
||||
return canRead((Type) clazz, null, mediaType);
|
||||
}
|
||||
|
||||
public boolean canRead(Type type, MediaType mediaType) {
|
||||
JavaType javaType = getJavaType(type);
|
||||
public boolean canRead(Type type, Class<?> contextClass, MediaType mediaType) {
|
||||
JavaType javaType = getJavaType(type, contextClass);
|
||||
return (this.objectMapper.canDeserialize(javaType) && canRead(mediaType));
|
||||
}
|
||||
|
||||
@@ -151,14 +151,14 @@ public class MappingJacksonHttpMessageConverter extends AbstractHttpMessageConve
|
||||
protected Object readInternal(Class<?> clazz, HttpInputMessage inputMessage)
|
||||
throws IOException, HttpMessageNotReadableException {
|
||||
|
||||
JavaType javaType = getJavaType(clazz);
|
||||
JavaType javaType = getJavaType(clazz, null);
|
||||
return readJavaType(javaType, inputMessage);
|
||||
}
|
||||
|
||||
public Object read(Type type, HttpInputMessage inputMessage)
|
||||
public Object read(Type type, Class<?> contextClass, HttpInputMessage inputMessage)
|
||||
throws IOException, HttpMessageNotReadableException {
|
||||
|
||||
JavaType javaType = getJavaType(type);
|
||||
JavaType javaType = getJavaType(type, contextClass);
|
||||
return readJavaType(javaType, inputMessage);
|
||||
}
|
||||
|
||||
@@ -196,10 +196,10 @@ public class MappingJacksonHttpMessageConverter extends AbstractHttpMessageConve
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return the Jackson {@link JavaType} for the specified type.
|
||||
* <p>The default implementation returns {@link TypeFactory#type(java.lang.reflect.Type)},
|
||||
* Return the Jackson {@link JavaType} for the specified type and context class.
|
||||
* <p>The default implementation returns {@link TypeFactory#type(java.lang.reflect.Type)}
|
||||
* or {@code TypeFactory.type(type, TypeFactory.type(contextClass))},
|
||||
* but this can be overridden in subclasses, to allow for custom generic collection handling.
|
||||
* For instance:
|
||||
* <pre class="code">
|
||||
@@ -212,10 +212,14 @@ public class MappingJacksonHttpMessageConverter extends AbstractHttpMessageConve
|
||||
* }
|
||||
* </pre>
|
||||
* @param type the type to return the java type for
|
||||
* @param contextClass a context class for the target type, for example a class
|
||||
* in which the target type appears in a method signature, can be {@code null}
|
||||
* @return the java type
|
||||
*/
|
||||
protected JavaType getJavaType(Type type) {
|
||||
return TypeFactory.type(type);
|
||||
protected JavaType getJavaType(Type type, Class<?> contextClass) {
|
||||
return (contextClass != null) ?
|
||||
TypeFactory.type(type, TypeFactory.type(contextClass)) :
|
||||
TypeFactory.type(type);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -74,7 +74,7 @@ public class Jaxb2CollectionHttpMessageConverter<T extends Collection>
|
||||
* {@link Collection} where the generic type is a JAXB type annotated with
|
||||
* {@link XmlRootElement} or {@link XmlType}.
|
||||
*/
|
||||
public boolean canRead(Type type, MediaType mediaType) {
|
||||
public boolean canRead(Type type, Class<?> contextClass, MediaType mediaType) {
|
||||
if (!(type instanceof ParameterizedType)) {
|
||||
return false;
|
||||
}
|
||||
@@ -119,7 +119,9 @@ public class Jaxb2CollectionHttpMessageConverter<T extends Collection>
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
public T read(Type type, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
|
||||
public T read(Type type, Class<?> contextClass, HttpInputMessage inputMessage)
|
||||
throws IOException, HttpMessageNotReadableException {
|
||||
|
||||
ParameterizedType parameterizedType = (ParameterizedType) type;
|
||||
T result = createCollection((Class<?>) parameterizedType.getRawType());
|
||||
Class<?> elementClass = (Class<?>) parameterizedType.getActualTypeArguments()[0];
|
||||
|
||||
@@ -86,12 +86,12 @@ public class HttpMessageConverterExtractor<T> implements ResponseExtractor<T> {
|
||||
for (HttpMessageConverter messageConverter : this.messageConverters) {
|
||||
if (messageConverter instanceof GenericHttpMessageConverter) {
|
||||
GenericHttpMessageConverter genericMessageConverter = (GenericHttpMessageConverter) messageConverter;
|
||||
if (genericMessageConverter.canRead(this.responseType, contentType)) {
|
||||
if (genericMessageConverter.canRead(this.responseType, null, contentType)) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Reading [" + this.responseType + "] as \"" +
|
||||
contentType + "\" using [" + messageConverter + "]");
|
||||
}
|
||||
return (T) genericMessageConverter.read(this.responseType, response);
|
||||
return (T) genericMessageConverter.read(this.responseType, null, response);
|
||||
}
|
||||
}
|
||||
if (this.responseClass != null) {
|
||||
|
||||
@@ -553,20 +553,17 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat
|
||||
}
|
||||
|
||||
List<MediaType> allSupportedMediaTypes = new ArrayList<MediaType>();
|
||||
for (HttpMessageConverter<?> messageConverter : getMessageConverters()) {
|
||||
for (HttpMessageConverter<?> converter : getMessageConverters()) {
|
||||
if (responseClass != null) {
|
||||
if (messageConverter.canRead(responseClass, null)) {
|
||||
allSupportedMediaTypes
|
||||
.addAll(getSupportedMediaTypes(messageConverter));
|
||||
if (converter.canRead(responseClass, null)) {
|
||||
allSupportedMediaTypes.addAll(getSupportedMediaTypes(converter));
|
||||
}
|
||||
}
|
||||
else if (messageConverter instanceof GenericHttpMessageConverter) {
|
||||
else if (converter instanceof GenericHttpMessageConverter) {
|
||||
|
||||
GenericHttpMessageConverter genericMessageConverter =
|
||||
(GenericHttpMessageConverter) messageConverter;
|
||||
if (genericMessageConverter.canRead(responseType, null)) {
|
||||
allSupportedMediaTypes
|
||||
.addAll(getSupportedMediaTypes(messageConverter));
|
||||
GenericHttpMessageConverter genericConverter = (GenericHttpMessageConverter) converter;
|
||||
if (genericConverter.canRead(responseType, null, null)) {
|
||||
allSupportedMediaTypes.addAll(getSupportedMediaTypes(converter));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -51,12 +51,12 @@ public class MappingJackson2HttpMessageConverterTests extends AbstractMappingJac
|
||||
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter() {
|
||||
|
||||
@Override
|
||||
protected JavaType getJavaType(Type type) {
|
||||
protected JavaType getJavaType(Type type, Class<?> contextClass) {
|
||||
if (type instanceof Class && List.class.isAssignableFrom((Class<?>)type)) {
|
||||
return new ObjectMapper().getTypeFactory().constructCollectionType(ArrayList.class, MyBean.class);
|
||||
}
|
||||
else {
|
||||
return super.getJavaType(type);
|
||||
return super.getJavaType(type, contextClass);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -87,7 +87,7 @@ public class MappingJackson2HttpMessageConverterTests extends AbstractMappingJac
|
||||
inputMessage.getHeaders().setContentType(new MediaType("application", "json"));
|
||||
|
||||
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
|
||||
List<MyBean> results = (List<MyBean>) converter.read(beansList.getType(), inputMessage);
|
||||
List<MyBean> results = (List<MyBean>) converter.read(beansList.getType(), null, inputMessage);
|
||||
assertEquals(1, results.size());
|
||||
MyBean result = results.get(0);
|
||||
assertEquals("Foo", result.getString());
|
||||
|
||||
@@ -49,12 +49,12 @@ public class MappingJacksonHttpMessageConverterTests extends AbstractMappingJack
|
||||
public void readGenerics() throws IOException {
|
||||
MappingJacksonHttpMessageConverter converter = new MappingJacksonHttpMessageConverter() {
|
||||
@Override
|
||||
protected JavaType getJavaType(Type type) {
|
||||
protected JavaType getJavaType(Type type, Class<?> contextClass) {
|
||||
if (type instanceof Class && List.class.isAssignableFrom((Class<?>)type)) {
|
||||
return TypeFactory.collectionType(ArrayList.class, MyBean.class);
|
||||
}
|
||||
else {
|
||||
return super.getJavaType(type);
|
||||
return super.getJavaType(type, contextClass);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -85,7 +85,7 @@ public class MappingJacksonHttpMessageConverterTests extends AbstractMappingJack
|
||||
inputMessage.getHeaders().setContentType(new MediaType("application", "json"));
|
||||
|
||||
MappingJacksonHttpMessageConverter converter = new MappingJacksonHttpMessageConverter();
|
||||
List<MyBean> results = (List<MyBean>) converter.read(beansList.getType(), inputMessage);
|
||||
List<MyBean> results = (List<MyBean>) converter.read(beansList.getType(), null, inputMessage);
|
||||
assertEquals(1, results.size());
|
||||
MyBean result = results.get(0);
|
||||
assertEquals("Foo", result.getString());
|
||||
|
||||
@@ -63,9 +63,9 @@ public class Jaxb2CollectionHttpMessageConverterTests {
|
||||
|
||||
@Test
|
||||
public void canRead() throws Exception {
|
||||
assertTrue(converter.canRead(rootElementListType, null));
|
||||
assertTrue(converter.canRead(rootElementSetType, null));
|
||||
assertTrue(converter.canRead(typeSetType, null));
|
||||
assertTrue(converter.canRead(rootElementListType, null, null));
|
||||
assertTrue(converter.canRead(rootElementSetType, null, null));
|
||||
assertTrue(converter.canRead(typeSetType, null, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -74,7 +74,7 @@ public class Jaxb2CollectionHttpMessageConverterTests {
|
||||
String content = "<list><rootElement><type s=\"1\"/></rootElement><rootElement><type s=\"2\"/></rootElement></list>";
|
||||
MockHttpInputMessage inputMessage = new MockHttpInputMessage(content.getBytes("UTF-8"));
|
||||
|
||||
List<RootElement> result = (List<RootElement>) converter.read(rootElementListType, inputMessage);
|
||||
List<RootElement> result = (List<RootElement>) converter.read(rootElementListType, null, inputMessage);
|
||||
|
||||
assertEquals("Invalid result", 2, result.size());
|
||||
assertEquals("Invalid result", "1", result.get(0).type.s);
|
||||
@@ -87,7 +87,7 @@ public class Jaxb2CollectionHttpMessageConverterTests {
|
||||
String content = "<set><rootElement><type s=\"1\"/></rootElement><rootElement><type s=\"2\"/></rootElement></set>";
|
||||
MockHttpInputMessage inputMessage = new MockHttpInputMessage(content.getBytes("UTF-8"));
|
||||
|
||||
Set<RootElement> result = (Set<RootElement>) converter.read(rootElementSetType, inputMessage);
|
||||
Set<RootElement> result = (Set<RootElement>) converter.read(rootElementSetType, null, inputMessage);
|
||||
|
||||
assertEquals("Invalid result", 2, result.size());
|
||||
assertTrue("Invalid result", result.contains(new RootElement("1")));
|
||||
@@ -100,7 +100,7 @@ public class Jaxb2CollectionHttpMessageConverterTests {
|
||||
String content = "<list><foo s=\"1\"/><bar s=\"2\"/></list>";
|
||||
MockHttpInputMessage inputMessage = new MockHttpInputMessage(content.getBytes("UTF-8"));
|
||||
|
||||
List<TestType> result = (List<TestType>) converter.read(typeListType, inputMessage);
|
||||
List<TestType> result = (List<TestType>) converter.read(typeListType, null, inputMessage);
|
||||
|
||||
assertEquals("Invalid result", 2, result.size());
|
||||
assertEquals("Invalid result", "1", result.get(0).s);
|
||||
@@ -113,7 +113,7 @@ public class Jaxb2CollectionHttpMessageConverterTests {
|
||||
String content = "<set><foo s=\"1\"/><bar s=\"2\"/></set>";
|
||||
MockHttpInputMessage inputMessage = new MockHttpInputMessage(content.getBytes("UTF-8"));
|
||||
|
||||
Set<TestType> result = (Set<TestType>) converter.read(typeSetType, inputMessage);
|
||||
Set<TestType> result = (Set<TestType>) converter.read(typeSetType, null, inputMessage);
|
||||
|
||||
assertEquals("Invalid result", 2, result.size());
|
||||
assertTrue("Invalid result", result.contains(new TestType("1")));
|
||||
|
||||
@@ -176,8 +176,8 @@ public class HttpMessageConverterExtractorTests {
|
||||
|
||||
expect(response.getStatusCode()).andReturn(HttpStatus.OK);
|
||||
expect(response.getHeaders()).andReturn(responseHeaders).times(2);
|
||||
expect(converter.canRead(type, contentType)).andReturn(true);
|
||||
expect(converter.read(type, response)).andReturn(expected);
|
||||
expect(converter.canRead(type, null, contentType)).andReturn(true);
|
||||
expect(converter.read(type, null, response)).andReturn(expected);
|
||||
|
||||
replay(response, converter);
|
||||
Object result = extractor.extractData(response);
|
||||
|
||||
@@ -86,7 +86,7 @@ public class RestTemplateTests {
|
||||
|
||||
verifyMocks();
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void varArgsNullTemplateVariable() throws Exception {
|
||||
expect(requestFactory.createRequest(new URI("http://example.com/-foo"), HttpMethod.GET))
|
||||
@@ -481,7 +481,7 @@ public class RestTemplateTests {
|
||||
|
||||
verifyMocks();
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void postForEntityNull() throws Exception {
|
||||
MediaType textPlain = new MediaType("text", "plain");
|
||||
@@ -644,7 +644,7 @@ public class RestTemplateTests {
|
||||
template.setMessageConverters(Collections.<HttpMessageConverter<?>>singletonList(converter));
|
||||
|
||||
ParameterizedTypeReference<List<Integer>> intList = new ParameterizedTypeReference<List<Integer>>() {};
|
||||
expect(converter.canRead(intList.getType(), null)).andReturn(true);
|
||||
expect(converter.canRead(intList.getType(), null, null)).andReturn(true);
|
||||
expect(converter.getSupportedMediaTypes()).andReturn(Collections.singletonList(MediaType.TEXT_PLAIN));
|
||||
expect(requestFactory.createRequest(new URI("http://example.com"), HttpMethod.POST)).andReturn(this.request);
|
||||
HttpHeaders requestHeaders = new HttpHeaders();
|
||||
@@ -660,8 +660,8 @@ public class RestTemplateTests {
|
||||
expect(response.getStatusCode()).andReturn(HttpStatus.OK);
|
||||
expect(response.getHeaders()).andReturn(responseHeaders).times(3);
|
||||
List<Integer> expected = Collections.singletonList(42);
|
||||
expect(converter.canRead(intList.getType(), MediaType.TEXT_PLAIN)).andReturn(true);
|
||||
expect(converter.read(intList.getType(), response)).andReturn(expected);
|
||||
expect(converter.canRead(intList.getType(), null, MediaType.TEXT_PLAIN)).andReturn(true);
|
||||
expect(converter.read(intList.getType(), null, response)).andReturn(expected);
|
||||
expect(response.getStatusCode()).andReturn(HttpStatus.OK);
|
||||
response.close();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user