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:
Rossen Stoyanchev
2012-11-21 10:40:28 -05:00
parent 4181397c31
commit b7f7fae78a
14 changed files with 202 additions and 105 deletions

View File

@@ -21,16 +21,19 @@ import java.lang.reflect.Array;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.GenericTypeResolver;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.MediaType;
@@ -104,40 +107,44 @@ public abstract class AbstractMessageConverterMethodArgumentResolver implements
* @param <T> the expected type of the argument value to be created
* @param inputMessage the HTTP input message representing the current request
* @param methodParam the method argument
* @param paramType the type of the argument value to be created
* @param targetType the type of object to create, not necessarily the same as
* the method parameter type (e.g. for {@code HttpEntity<String>} method
* parameter the target type is String)
* @return the created method argument value
* @throws IOException if the reading from the request fails
* @throws HttpMediaTypeNotSupportedException if no suitable message converter is found
*/
@SuppressWarnings("unchecked")
protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage,
MethodParameter methodParam, Type paramType) throws IOException, HttpMediaTypeNotSupportedException {
MethodParameter methodParam, Type targetType) throws IOException, HttpMediaTypeNotSupportedException {
MediaType contentType = inputMessage.getHeaders().getContentType();
if (contentType == null) {
contentType = MediaType.APPLICATION_OCTET_STREAM;
}
Class<T> paramClass = getParamClass(paramType);
Class<?> contextClass = methodParam.getDeclaringClass();
Map<TypeVariable, Type> map = GenericTypeResolver.getTypeVariableMap(contextClass);
Class<T> targetClass = (Class<T>) GenericTypeResolver.resolveType(targetType, map);
for (HttpMessageConverter<?> messageConverter : this.messageConverters) {
if (messageConverter instanceof GenericHttpMessageConverter) {
GenericHttpMessageConverter genericMessageConverter = (GenericHttpMessageConverter) messageConverter;
if (genericMessageConverter.canRead(paramType, contentType)) {
for (HttpMessageConverter<?> converter : this.messageConverters) {
if (converter instanceof GenericHttpMessageConverter) {
GenericHttpMessageConverter genericConverter = (GenericHttpMessageConverter) converter;
if (genericConverter.canRead(targetType, contextClass, contentType)) {
if (logger.isDebugEnabled()) {
logger.debug("Reading [" + paramType + "] as \"" +
contentType + "\" using [" + messageConverter + "]");
logger.debug("Reading [" + targetType + "] as \"" +
contentType + "\" using [" + converter + "]");
}
return (T) genericMessageConverter.read(paramType, inputMessage);
return (T) genericConverter.read(targetType, contextClass, inputMessage);
}
}
if (paramClass != null) {
if (messageConverter.canRead(paramClass, contentType)) {
if (targetClass != null) {
if (converter.canRead(targetClass, contentType)) {
if (logger.isDebugEnabled()) {
logger.debug("Reading [" + paramClass.getName() + "] as \"" + contentType + "\" using [" +
messageConverter + "]");
logger.debug("Reading [" + targetClass.getName() + "] as \"" +
contentType + "\" using [" + converter + "]");
}
return ((HttpMessageConverter<T>) messageConverter).read(paramClass, inputMessage);
return ((HttpMessageConverter<T>) converter).read(targetClass, inputMessage);
}
}
}
@@ -145,26 +152,6 @@ public abstract class AbstractMessageConverterMethodArgumentResolver implements
throw new HttpMediaTypeNotSupportedException(contentType, allSupportedMediaTypes);
}
private Class getParamClass(Type paramType) {
if (paramType instanceof Class) {
return (Class) paramType;
}
else if (paramType instanceof GenericArrayType) {
Type componentType = ((GenericArrayType) paramType).getGenericComponentType();
if (componentType instanceof Class) {
// Surely, there should be a nicer way to determine the array type
return Array.newInstance((Class<?>) componentType, 0).getClass();
}
}
else if (paramType instanceof ParameterizedType) {
ParameterizedType parameterizedType = (ParameterizedType) paramType;
if (parameterizedType.getRawType() instanceof Class) {
return (Class) parameterizedType.getRawType();
}
}
return null;
}
/**
* Creates a new {@link HttpInputMessage} from the given {@link NativeWebRequest}.
*

View File

@@ -18,6 +18,7 @@ package org.springframework.web.servlet.mvc.method.annotation;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
@@ -26,6 +27,7 @@ import org.junit.Before;
import org.junit.Test;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpEntity;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.mock.web.MockHttpServletRequest;
@@ -35,6 +37,7 @@ import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.method.support.ModelAndViewContainer;
/**
@@ -58,6 +61,8 @@ public class HttpEntityMethodProcessorTests {
private MockHttpServletRequest servletRequest;
private WebDataBinderFactory binderFactory;
@Before
public void setUp() throws Exception {
@@ -70,6 +75,8 @@ public class HttpEntityMethodProcessorTests {
servletRequest = new MockHttpServletRequest();
servletResponse = new MockHttpServletResponse();
webRequest = new ServletWebRequest(servletRequest, servletResponse);
binderFactory = new ValidatingBinderFactory();
}
@Test
@@ -84,7 +91,7 @@ public class HttpEntityMethodProcessorTests {
@SuppressWarnings("unchecked")
HttpEntity<SimpleBean> result = (HttpEntity<SimpleBean>) processor.resolveArgument(
paramSimpleBean, mavContainer, webRequest, new ValidatingBinderFactory());
paramSimpleBean, mavContainer, webRequest, binderFactory);
assertNotNull(result);
assertEquals("Jad", result.getBody().getName());
@@ -102,27 +109,68 @@ public class HttpEntityMethodProcessorTests {
@SuppressWarnings("unchecked")
HttpEntity<List<SimpleBean>> result = (HttpEntity<List<SimpleBean>>) processor.resolveArgument(
paramList, mavContainer, webRequest, new ValidatingBinderFactory());
paramList, mavContainer, webRequest, binderFactory);
assertNotNull(result);
assertEquals("Jad", result.getBody().get(0).getName());
assertEquals("Robert", result.getBody().get(1).getName());
}
@Test
public void resolveArgumentTypeVariable() throws Exception {
Method method = MySimpleParameterizedController.class.getMethod("handleDto", HttpEntity.class);
HandlerMethod handlerMethod = new HandlerMethod(new MySimpleParameterizedController(), method);
MethodParameter methodParam = handlerMethod.getMethodParameters()[0];
String content = "{\"name\" : \"Jad\"}";
this.servletRequest.setContent(content.getBytes("UTF-8"));
this.servletRequest.setContentType(MediaType.APPLICATION_JSON_VALUE);
List<HttpMessageConverter<?>> converters = new ArrayList<HttpMessageConverter<?>>();
converters.add(new MappingJackson2HttpMessageConverter());
HttpEntityMethodProcessor processor = new HttpEntityMethodProcessor(converters);
@SuppressWarnings("unchecked")
HttpEntity<SimpleBean> result = (HttpEntity<SimpleBean>) processor.resolveArgument(methodParam, mavContainer, webRequest, binderFactory);
assertNotNull(result);
assertEquals("Jad", result.getBody().getName());
}
public void handle(HttpEntity<List<SimpleBean>> arg1, HttpEntity<SimpleBean> arg2) {
}
private static abstract class MyParameterizedController<DTO extends Identifiable> {
@SuppressWarnings("unused")
public void handleDto(HttpEntity<DTO> dto) {}
}
private static class SimpleBean {
private static class MySimpleParameterizedController extends MyParameterizedController<SimpleBean> { }
private interface Identifiable extends Serializable {
public Long getId();
public void setId(Long id);
}
@SuppressWarnings({ "serial" })
private static class SimpleBean implements Identifiable {
private Long id;
private String name;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
@SuppressWarnings("unused")
public void setName(String name) {
this.name = name;
}

View File

@@ -19,9 +19,9 @@ package org.springframework.web.servlet.mvc.method.annotation;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.junit.Before;
@@ -42,6 +42,7 @@ import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.method.support.ModelAndViewContainer;
/**
@@ -68,6 +69,9 @@ public class RequestResponseBodyMethodProcessorTests {
private MockHttpServletResponse servletResponse;
private ValidatingBinderFactory binderFactory;
@Before
public void setUp() throws Exception {
@@ -85,8 +89,9 @@ public class RequestResponseBodyMethodProcessorTests {
servletRequest = new MockHttpServletRequest();
servletResponse = new MockHttpServletResponse();
webRequest = new ServletWebRequest(servletRequest, servletResponse);
}
this.binderFactory = new ValidatingBinderFactory();
}
@Test
public void resolveArgumentParameterizedType() throws Exception {
@@ -100,7 +105,7 @@ public class RequestResponseBodyMethodProcessorTests {
@SuppressWarnings("unchecked")
List<SimpleBean> result = (List<SimpleBean>) processor.resolveArgument(
paramGenericList, mavContainer, webRequest, new ValidatingBinderFactory());
paramGenericList, mavContainer, webRequest, binderFactory);
assertNotNull(result);
assertEquals("Jad", result.get(0).getName());
@@ -119,7 +124,7 @@ public class RequestResponseBodyMethodProcessorTests {
@SuppressWarnings("unchecked")
MultiValueMap<String, String> result = (MultiValueMap<String, String>) processor.resolveArgument(
paramMultiValueMap, mavContainer, webRequest, new ValidatingBinderFactory());
paramMultiValueMap, mavContainer, webRequest, binderFactory);
assertNotNull(result);
assertEquals("apple", result.getFirst("fruit"));
@@ -137,7 +142,7 @@ public class RequestResponseBodyMethodProcessorTests {
RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor(converters);
SimpleBean result = (SimpleBean) processor.resolveArgument(
paramSimpleBean, mavContainer, webRequest, new ValidatingBinderFactory());
paramSimpleBean, mavContainer, webRequest, binderFactory);
assertNotNull(result);
assertEquals("Jad", result.getName());
@@ -154,12 +159,35 @@ public class RequestResponseBodyMethodProcessorTests {
RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor(converters);
String result = (String) processor.resolveArgument(
paramString, mavContainer, webRequest, new ValidatingBinderFactory());
paramString, mavContainer, webRequest, binderFactory);
assertNotNull(result);
assertEquals("foobarbaz", result);
}
// SPR-9964
@Test
public void resolveArgumentTypeVariable() throws Exception {
Method method = MySimpleParameterizedController.class.getMethod("handleDto", Identifiable.class);
HandlerMethod handlerMethod = new HandlerMethod(new MySimpleParameterizedController(), method);
MethodParameter methodParam = handlerMethod.getMethodParameters()[0];
String content = "{\"name\" : \"Jad\"}";
this.servletRequest.setContent(content.getBytes("UTF-8"));
this.servletRequest.setContentType(MediaType.APPLICATION_JSON_VALUE);
List<HttpMessageConverter<?>> converters = new ArrayList<HttpMessageConverter<?>>();
converters.add(new MappingJackson2HttpMessageConverter());
RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor(converters);
SimpleBean result = (SimpleBean) processor.resolveArgument(methodParam, mavContainer, webRequest, binderFactory);
assertNotNull(result);
assertEquals("Jad", result.getName());
}
// SPR-9160
@Test
@@ -213,15 +241,36 @@ public class RequestResponseBodyMethodProcessorTests {
return null;
}
private static class SimpleBean {
private static abstract class MyParameterizedController<DTO extends Identifiable> {
@SuppressWarnings("unused")
public void handleDto(@RequestBody DTO dto) {}
}
private static class MySimpleParameterizedController extends MyParameterizedController<SimpleBean> { }
private interface Identifiable extends Serializable {
public Long getId();
public void setId(Long id);
}
@SuppressWarnings({ "serial" })
private static class SimpleBean implements Identifiable {
private Long id;
private String name;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
@SuppressWarnings("unused")
public void setName(String name) {
this.name = name;
}