Make @ResponseBody method return type available for message converters

This commit adds canWrite() and write() methods to the
GenericHttpMessageConverter interface. These are type aware variants
of the methods available in HttpMessageConverter, in order to keep
parametrized type information when serializing objects.

AbstractMessageConverterMethodProcessor now calls those type aware
methods when the message converter implements GenericHttpMessageConverter.

AbstractJackson2HttpMessageConverter and GsonHttpMessageConverter uses
these new methods to make @ResponseBody method return type available
for type resolution instead of just letting the JSON serializer trying
to guess the type to use from the object to serialize.

Issue: SPR-12811
This commit is contained in:
Sebastien Deleuze 2015-06-22 13:52:17 +02:00
parent 04a7ed5f91
commit 31a5434ea4
12 changed files with 442 additions and 76 deletions

View File

@ -0,0 +1,118 @@
/*
* Copyright 2002-2015 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.http.converter;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.reflect.Type;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.StreamingHttpOutputMessage;
/**
* Abstract base class for most {@link GenericHttpMessageConverter} implementations.
*
* @author Sebastien Deleuze
* @since 4.2
*/
public abstract class AbstractGenericHttpMessageConverter<T> extends AbstractHttpMessageConverter<T>
implements GenericHttpMessageConverter<T> {
/**
* Construct an {@code AbstractGenericHttpMessageConverter} with no supported media types.
* @see #setSupportedMediaTypes
*/
protected AbstractGenericHttpMessageConverter() {
}
/**
* Construct an {@code AbstractGenericHttpMessageConverter} with one supported media type.
* @param supportedMediaType the supported media type
*/
protected AbstractGenericHttpMessageConverter(MediaType supportedMediaType) {
super(supportedMediaType);
}
/**
* Construct an {@code AbstractGenericHttpMessageConverter} with multiple supported media type.
* @param supportedMediaTypes the supported media types
*/
protected AbstractGenericHttpMessageConverter(MediaType... supportedMediaTypes) {
super(supportedMediaTypes);
}
@Override
public boolean canWrite(Class<?> contextClass, MediaType mediaType) {
return canWrite(null, contextClass, mediaType);
}
/**
* This implementation sets the default headers by calling {@link #addDefaultHeaders},
* and then calls {@link #writeInternal}.
*/
public final void write(final T t, final Type type, MediaType contentType, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
final HttpHeaders headers = outputMessage.getHeaders();
addDefaultHeaders(headers, t, contentType);
if (outputMessage instanceof StreamingHttpOutputMessage) {
StreamingHttpOutputMessage streamingOutputMessage =
(StreamingHttpOutputMessage) outputMessage;
streamingOutputMessage.setBody(new StreamingHttpOutputMessage.Body() {
@Override
public void writeTo(final OutputStream outputStream) throws IOException {
writeInternal(t, type, new HttpOutputMessage() {
@Override
public OutputStream getBody() throws IOException {
return outputStream;
}
@Override
public HttpHeaders getHeaders() {
return headers;
}
});
}
});
}
else {
writeInternal(t, type, outputMessage);
outputMessage.getBody().flush();
}
}
@Override
protected void writeInternal(T t, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
writeInternal(t, null, outputMessage);
}
/**
* Abstract template method that writes the actual body. Invoked from {@link #write}.
* @param t the object to write to the output message
* @param type the type of object to write, can be {@code null} if not specified.
* @param outputMessage the HTTP output message to write to
* @throws IOException in case of I/O errors
* @throws HttpMessageNotWritableException in case of conversion errors
*/
protected abstract void writeInternal(T t, Type type, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException;
}

View File

@ -160,34 +160,15 @@ public abstract class AbstractHttpMessageConverter<T> implements HttpMessageConv
}
/**
* This implementation delegates to {@link #getDefaultContentType(Object)} if a content
* type was not provided, calls {@link #getContentLength}, and sets the corresponding headers
* on the output message. It then calls {@link #writeInternal}.
* This implementation sets the default headers by calling {@link #addDefaultHeaders},
* and then calls {@link #writeInternal}.
*/
@Override
public final void write(final T t, MediaType contentType, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
final HttpHeaders headers = outputMessage.getHeaders();
if (headers.getContentType() == null) {
MediaType contentTypeToUse = contentType;
if (contentType == null || contentType.isWildcardType() || contentType.isWildcardSubtype()) {
contentTypeToUse = getDefaultContentType(t);
}
else if (MediaType.APPLICATION_OCTET_STREAM.equals(contentType)) {
MediaType type = getDefaultContentType(t);
contentTypeToUse = (type != null ? type : contentTypeToUse);
}
if (contentTypeToUse != null) {
headers.setContentType(contentTypeToUse);
}
}
if (headers.getContentLength() == -1) {
Long contentLength = getContentLength(t, headers.getContentType());
if (contentLength != null) {
headers.setContentLength(contentLength);
}
}
addDefaultHeaders(headers, t, contentType);
if (outputMessage instanceof StreamingHttpOutputMessage) {
StreamingHttpOutputMessage streamingOutputMessage =
@ -214,6 +195,36 @@ public abstract class AbstractHttpMessageConverter<T> implements HttpMessageConv
}
}
/**
* Add default headers to the output message.
* <p>This implementation delegates to {@link #getDefaultContentType(Object)} if a content
* type was not provided, calls {@link #getContentLength}, and sets the corresponding headers
* @since 4.2
*/
protected void addDefaultHeaders(final HttpHeaders headers, final T t, MediaType contentType)
throws IOException{
if (headers.getContentType() == null) {
MediaType contentTypeToUse = contentType;
if (contentType == null || contentType.isWildcardType() || contentType.isWildcardSubtype()) {
contentTypeToUse = getDefaultContentType(t);
}
else if (MediaType.APPLICATION_OCTET_STREAM.equals(contentType)) {
MediaType mediaType = getDefaultContentType(t);
contentTypeToUse = (mediaType != null ? mediaType : contentTypeToUse);
}
if (contentTypeToUse != null) {
headers.setContentType(contentTypeToUse);
}
}
if (headers.getContentLength() == -1) {
Long contentLength = getContentLength(t, headers.getContentType());
if (contentLength != null) {
headers.setContentLength(contentLength);
}
}
}
/**
* Returns the default content type for the given type. Called when {@link #write}
* is invoked without a specified content type parameter.

View File

@ -20,14 +20,17 @@ import java.io.IOException;
import java.lang.reflect.Type;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
/**
* A specialization of {@link HttpMessageConverter} that can convert an HTTP
* request into a target object of a specified generic type.
* A specialization of {@link HttpMessageConverter} that can convert an HTTP request
* into a target object of a specified generic type and a source object of a specified
* generic type into an HTTP response.
*
* @author Arjen Poutsma
* @author Rossen Stoyanchev
* @author Sebastien Deleuze
* @since 3.2
* @see org.springframework.core.ParameterizedTypeReference
*/
@ -59,4 +62,34 @@ public interface GenericHttpMessageConverter<T> extends HttpMessageConverter<T>
T read(Type type, Class<?> contextClass, HttpInputMessage inputMessage)
throws IOException, HttpMessageNotReadableException;
/**
* Indicates whether the given class can be written by this converter.
* @param type the type to test for writability, can be {@code null} if not specified.
* @param contextClass the class to test for writability
* @param mediaType the media type to write, can be {@code null} if not specified.
* Typically the value of an {@code Accept} header.
* @return {@code true} if writable; {@code false} otherwise
* @since 4.2
*/
boolean canWrite(Type type, Class<?> contextClass, MediaType mediaType);
/**
* Write an given object to the given output message.
* @param t the object to write to the output message. The type of this object must have previously been
* passed to the {@link #canWrite canWrite} method of this interface, which must have returned {@code true}.
* @param type the type of object to write. This type must have previously
* been passed to the {@link #canWrite canWrite} method of this interface,
* which must have returned {@code true}. Can be {@code null} if not specified.
* @param contentType the content type to use when writing. May be {@code null} to indicate that the
* default content type of the converter must be used. If not {@code null}, this media type must have
* previously been passed to the {@link #canWrite canWrite} method of this interface, which must have
* returned {@code true}.
* @param outputMessage the message to write to
* @throws IOException in case of I/O errors
* @throws HttpMessageNotWritableException in case of conversion errors
* @since 4.2
*/
void write(T t, Type type, MediaType contentType, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException;
}

View File

@ -27,19 +27,21 @@ import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.util.DefaultPrettyPrinter;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectWriter;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.ser.FilterProvider;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.AbstractHttpMessageConverter;
import org.springframework.http.converter.AbstractGenericHttpMessageConverter;
import org.springframework.http.converter.GenericHttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.TypeUtils;
/**
* Abstract base class for Jackson based and content type independent
@ -54,7 +56,7 @@ import org.springframework.util.ClassUtils;
* @author Sebastien Deleuze
* @since 4.1
*/
public abstract class AbstractJackson2HttpMessageConverter extends AbstractHttpMessageConverter<Object>
public abstract class AbstractJackson2HttpMessageConverter extends AbstractGenericHttpMessageConverter<Object>
implements GenericHttpMessageConverter<Object> {
public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
@ -158,7 +160,7 @@ public abstract class AbstractJackson2HttpMessageConverter extends AbstractHttpM
}
@Override
public boolean canWrite(Class<?> clazz, MediaType mediaType) {
public boolean canWrite(Type type, Class<?> clazz, MediaType mediaType) {
if (!jackson23Available || !logger.isWarnEnabled()) {
return (this.objectMapper.canSerialize(clazz) && canWrite(mediaType));
}
@ -218,31 +220,43 @@ public abstract class AbstractJackson2HttpMessageConverter extends AbstractHttpM
}
@Override
protected void writeInternal(Object object, HttpOutputMessage outputMessage)
@SuppressWarnings("deprecation")
protected void writeInternal(Object object, Type type, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
JsonEncoding encoding = getJsonEncoding(outputMessage.getHeaders().getContentType());
JsonGenerator generator = this.objectMapper.getFactory().createGenerator(outputMessage.getBody(), encoding);
try {
writePrefix(generator, object);
Class<?> serializationView = null;
FilterProvider filters = null;
Object value = object;
if (value instanceof MappingJacksonValue) {
JavaType javaType = null;
if (type != null) {
javaType = getJavaType(type, null);
}
if (object instanceof MappingJacksonValue) {
MappingJacksonValue container = (MappingJacksonValue) object;
value = container.getValue();
serializationView = container.getSerializationView();
filters = container.getFilters();
}
ObjectWriter objectWriter;
if (serializationView != null) {
this.objectMapper.writerWithView(serializationView).writeValue(generator, value);
objectWriter = this.objectMapper.writerWithView(serializationView);
}
else if (filters != null) {
this.objectMapper.writer(filters).writeValue(generator, value);
objectWriter = this.objectMapper.writer(filters);
}
else {
this.objectMapper.writeValue(generator, value);
objectWriter = this.objectMapper.writer();
}
if (javaType != null && value != null && TypeUtils.isAssignable(type, value.getClass())) {
objectWriter = objectWriter.withType(javaType);
}
objectWriter.writeValue(generator, value);
writeSuffix(generator, object);
generator.flush();

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2014 the original author or authors.
* Copyright 2002-2015 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.
@ -32,7 +32,7 @@ import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.AbstractHttpMessageConverter;
import org.springframework.http.converter.AbstractGenericHttpMessageConverter;
import org.springframework.http.converter.GenericHttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
@ -54,7 +54,7 @@ import org.springframework.util.Assert;
* @see #setGson
* @see #setSupportedMediaTypes
*/
public class GsonHttpMessageConverter extends AbstractHttpMessageConverter<Object>
public class GsonHttpMessageConverter extends AbstractGenericHttpMessageConverter<Object>
implements GenericHttpMessageConverter<Object> {
public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
@ -125,7 +125,7 @@ public class GsonHttpMessageConverter extends AbstractHttpMessageConverter<Objec
}
@Override
public boolean canWrite(Class<?> clazz, MediaType mediaType) {
public boolean canWrite(Type type, Class<?> clazz, MediaType mediaType) {
return canWrite(mediaType);
}
@ -191,7 +191,7 @@ public class GsonHttpMessageConverter extends AbstractHttpMessageConverter<Objec
}
@Override
protected void writeInternal(Object o, HttpOutputMessage outputMessage)
protected void writeInternal(Object o, Type type, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
Charset charset = getCharset(outputMessage.getHeaders());
@ -200,7 +200,12 @@ public class GsonHttpMessageConverter extends AbstractHttpMessageConverter<Objec
if (this.jsonPrefix != null) {
writer.append(this.jsonPrefix);
}
this.gson.toJson(o, writer);
if (type != null) {
this.gson.toJson(o, type, writer);
}
else {
this.gson.toJson(o, writer);
}
writer.close();
}
catch (JsonIOException ex) {

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2014 the original author or authors.
* Copyright 2002-2015 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.
@ -40,10 +40,12 @@ import javax.xml.transform.Source;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.GenericHttpMessageConverter;
import org.springframework.http.converter.HttpMessageConversionException;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
/**
* An {@code HttpMessageConverter} that can read XML collections using JAXB2.
@ -112,6 +114,15 @@ public class Jaxb2CollectionHttpMessageConverter<T extends Collection>
return false;
}
/**
* Always returns {@code false} since Jaxb2CollectionHttpMessageConverter
* does not convert collections to XML.
*/
@Override
public boolean canWrite(Type type, Class<?> contextClass, MediaType mediaType) {
return false;
}
@Override
protected boolean supports(Class<?> clazz) {
// should not be called, since we override canRead/Write
@ -216,6 +227,12 @@ public class Jaxb2CollectionHttpMessageConverter<T extends Collection>
return event;
}
@Override
public void write(T t, Type type, MediaType contentType, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
throw new UnsupportedOperationException();
}
@Override
protected void writeToResult(T t, HttpHeaders headers, Result result) throws IOException {
throw new UnsupportedOperationException();

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2014 the original author or authors.
* Copyright 2002-2015 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.
@ -209,7 +209,7 @@ public class GsonHttpMessageConverterTests {
public void prefixJson() throws Exception {
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
this.converter.setPrefixJson(true);
this.converter.writeInternal("foo", outputMessage);
this.converter.writeInternal("foo", null, outputMessage);
assertEquals(")]}', \"foo\"", outputMessage.getBodyAsString(UTF8));
}
@ -217,7 +217,7 @@ public class GsonHttpMessageConverterTests {
public void prefixJsonCustom() throws Exception {
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
this.converter.setJsonPrefix(")))");
this.converter.writeInternal("foo", outputMessage);
this.converter.writeInternal("foo", null, outputMessage);
assertEquals(")))\"foo\"", outputMessage.getBodyAsString(UTF8));
}

View File

@ -221,7 +221,7 @@ public class MappingJackson2HttpMessageConverterTests {
bean.setName("Jason");
this.converter.setPrettyPrint(true);
this.converter.writeInternal(bean, outputMessage);
this.converter.writeInternal(bean, null, outputMessage);
String result = outputMessage.getBodyAsString(Charset.forName("UTF-8"));
assertEquals("{" + NEWLINE_SYSTEM_PROPERTY + " \"name\" : \"Jason\"" + NEWLINE_SYSTEM_PROPERTY + "}", result);
@ -231,7 +231,7 @@ public class MappingJackson2HttpMessageConverterTests {
public void prefixJson() throws Exception {
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
this.converter.setPrefixJson(true);
this.converter.writeInternal("foo", outputMessage);
this.converter.writeInternal("foo", null, outputMessage);
assertEquals(")]}', \"foo\"", outputMessage.getBodyAsString(Charset.forName("UTF-8")));
}
@ -240,7 +240,7 @@ public class MappingJackson2HttpMessageConverterTests {
public void prefixJsonCustom() throws Exception {
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
this.converter.setJsonPrefix(")))");
this.converter.writeInternal("foo", outputMessage);
this.converter.writeInternal("foo", null, outputMessage);
assertEquals(")))\"foo\"", outputMessage.getBodyAsString(Charset.forName("UTF-8")));
}
@ -255,7 +255,7 @@ public class MappingJackson2HttpMessageConverterTests {
MappingJacksonValue jacksonValue = new MappingJacksonValue(bean);
jacksonValue.setSerializationView(MyJacksonView1.class);
this.converter.writeInternal(jacksonValue, outputMessage);
this.converter.writeInternal(jacksonValue, null, outputMessage);
String result = outputMessage.getBodyAsString(Charset.forName("UTF-8"));
assertThat(result, containsString("\"withView1\":\"with\""));
@ -274,7 +274,7 @@ public class MappingJackson2HttpMessageConverterTests {
FilterProvider filters = new SimpleFilterProvider().addFilter("myJacksonFilter",
SimpleBeanPropertyFilter.serializeAllExcept("property2"));
jacksonValue.setFilters(filters);
this.converter.writeInternal(jacksonValue, outputMessage);
this.converter.writeInternal(jacksonValue, null, outputMessage);
String result = outputMessage.getBodyAsString(Charset.forName("UTF-8"));
assertThat(result, containsString("\"property1\":\"value\""));
@ -288,7 +288,7 @@ public class MappingJackson2HttpMessageConverterTests {
jacksonValue.setJsonpFunction("callback");
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
this.converter.writeInternal(jacksonValue, outputMessage);
this.converter.writeInternal(jacksonValue, null, outputMessage);
assertEquals("callback(\"foo\");", outputMessage.getBodyAsString(Charset.forName("UTF-8")));
}
@ -304,7 +304,7 @@ public class MappingJackson2HttpMessageConverterTests {
MappingJacksonValue jacksonValue = new MappingJacksonValue(bean);
jacksonValue.setSerializationView(MyJacksonView1.class);
jacksonValue.setJsonpFunction("callback");
this.converter.writeInternal(jacksonValue, outputMessage);
this.converter.writeInternal(jacksonValue, null, outputMessage);
String result = outputMessage.getBodyAsString(Charset.forName("UTF-8"));
assertThat(result, startsWith("callback("));

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2014 the original author or authors.
* Copyright 2002-2015 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.
@ -29,8 +29,8 @@ import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.MockHttpInputMessage;
import org.springframework.http.MockHttpOutputMessage;
import org.springframework.http.converter.AbstractHttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter;
import org.springframework.http.converter.json.MappingJacksonValue;
import static org.hamcrest.CoreMatchers.*;
@ -142,7 +142,7 @@ public class MappingJackson2XmlHttpMessageConverterTests {
private void writeInternal(Object object, HttpOutputMessage outputMessage)
throws NoSuchMethodException, InvocationTargetException,
IllegalAccessException {
Method method = AbstractJackson2HttpMessageConverter.class.getDeclaredMethod(
Method method = AbstractHttpMessageConverter.class.getDeclaredMethod(
"writeInternal", Object.class, HttpOutputMessage.class);
method.setAccessible(true);
method.invoke(this.converter, object, outputMessage);

View File

@ -17,6 +17,7 @@
package org.springframework.web.servlet.mvc.method.annotation;
import java.io.IOException;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashSet;
@ -27,8 +28,10 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.GenericHttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.http.server.ServletServerHttpResponse;
@ -158,7 +161,20 @@ public abstract class AbstractMessageConverterMethodProcessor extends AbstractMe
(Class<? extends HttpMessageConverter<?>>) messageConverter.getClass(),
inputMessage, outputMessage);
if (returnValue != null) {
((HttpMessageConverter<T>) messageConverter).write(returnValue, selectedMediaType, outputMessage);
if (messageConverter instanceof GenericHttpMessageConverter) {
Type type;
if (HttpEntity.class.isAssignableFrom(returnType.getParameterType())) {
returnType.increaseNestingLevel();
type = returnType.getNestedGenericParameterType();
}
else {
type = returnType.getGenericParameterType();
}
((GenericHttpMessageConverter<T>) messageConverter).write(returnValue, type, selectedMediaType, outputMessage);
}
else {
((HttpMessageConverter<T>) messageConverter).write(returnValue, selectedMediaType, outputMessage);
}
if (logger.isDebugEnabled()) {
logger.debug("Written [" + returnValue + "] as \"" +
selectedMediaType + "\" using [" + messageConverter + "]");

View File

@ -16,6 +16,8 @@
package org.springframework.web.servlet.mvc.method.annotation;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.JsonTypeName;
import static org.junit.Assert.*;
import java.io.Serializable;
@ -36,6 +38,8 @@ import org.springframework.mock.web.test.MockHttpServletRequest;
import org.springframework.mock.web.test.MockHttpServletResponse;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.ServletWebRequest;
@ -64,6 +68,8 @@ public class HttpEntityMethodProcessorTests {
private ServletWebRequest webRequest;
private MockHttpServletResponse servletResponse;
@Before
public void setUp() throws Exception {
@ -74,7 +80,8 @@ public class HttpEntityMethodProcessorTests {
mavContainer = new ModelAndViewContainer();
binderFactory = new ValidatingBinderFactory();
servletRequest = new MockHttpServletRequest();
webRequest = new ServletWebRequest(servletRequest, new MockHttpServletResponse());
servletResponse = new MockHttpServletResponse();
webRequest = new ServletWebRequest(servletRequest, servletResponse);
}
@Test
@ -153,6 +160,24 @@ public class HttpEntityMethodProcessorTests {
assertEquals("Jad", result.getBody().getName());
}
@Test // SPR-12811
public void jacksonTypeInfoList() throws Exception {
Method method = JacksonController.class.getMethod("handleList");
HandlerMethod handlerMethod = new HandlerMethod(new JacksonController(), method);
MethodParameter methodReturnType = handlerMethod.getReturnType();
List<HttpMessageConverter<?>> converters = new ArrayList<HttpMessageConverter<?>>();
converters.add(new MappingJackson2HttpMessageConverter());
HttpEntityMethodProcessor processor = new HttpEntityMethodProcessor(converters);
Object returnValue = new JacksonController().handleList();
processor.handleReturnValue(returnValue, methodReturnType, this.mavContainer, this.webRequest);
String content = this.servletResponse.getContentAsString();
assertTrue(content.contains("\"type\":\"foo\""));
assertTrue(content.contains("\"type\":\"bar\""));
}
@SuppressWarnings("unused")
public void handle(HttpEntity<List<SimpleBean>> arg1, HttpEntity<SimpleBean> arg2) {
@ -217,4 +242,59 @@ public class HttpEntityMethodProcessorTests {
}
}
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type")
private static class ParentClass {
private String parentProperty;
public ParentClass() {
}
public ParentClass(String parentProperty) {
this.parentProperty = parentProperty;
}
public String getParentProperty() {
return parentProperty;
}
public void setParentProperty(String parentProperty) {
this.parentProperty = parentProperty;
}
}
@JsonTypeName("foo")
private static class Foo extends ParentClass {
public Foo() {
}
public Foo(String parentProperty) {
super(parentProperty);
}
}
@JsonTypeName("bar")
private static class Bar extends ParentClass {
public Bar() {
}
public Bar(String parentProperty) {
super(parentProperty);
}
}
private static class JacksonController {
@RequestMapping
@ResponseBody
public HttpEntity<List<ParentClass>> handleList() {
List<ParentClass> list = new ArrayList<>();
list.add(new Foo("foo"));
list.add(new Bar("bar"));
return new HttpEntity<>(list);
}
}
}

View File

@ -27,6 +27,8 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.JsonTypeName;
import com.fasterxml.jackson.annotation.JsonView;
import org.junit.Before;
import org.junit.Test;
@ -349,8 +351,8 @@ public class RequestResponseBodyMethodProcessorTests {
@Test
public void jacksonJsonViewWithResponseBodyAndJsonMessageConverter() throws Exception {
Method method = JacksonViewController.class.getMethod("handleResponseBody");
HandlerMethod handlerMethod = new HandlerMethod(new JacksonViewController(), method);
Method method = JacksonController.class.getMethod("handleResponseBody");
HandlerMethod handlerMethod = new HandlerMethod(new JacksonController(), method);
MethodParameter methodReturnType = handlerMethod.getReturnType();
List<HttpMessageConverter<?>> converters = new ArrayList<HttpMessageConverter<?>>();
@ -359,7 +361,7 @@ public class RequestResponseBodyMethodProcessorTests {
RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor(
converters, null, Collections.singletonList(new JsonViewResponseBodyAdvice()));
Object returnValue = new JacksonViewController().handleResponseBody();
Object returnValue = new JacksonController().handleResponseBody();
processor.handleReturnValue(returnValue, methodReturnType, this.mavContainer, this.webRequest);
String content = this.servletResponse.getContentAsString();
@ -370,8 +372,8 @@ public class RequestResponseBodyMethodProcessorTests {
@Test
public void jacksonJsonViewWithResponseEntityAndJsonMessageConverter() throws Exception {
Method method = JacksonViewController.class.getMethod("handleResponseEntity");
HandlerMethod handlerMethod = new HandlerMethod(new JacksonViewController(), method);
Method method = JacksonController.class.getMethod("handleResponseEntity");
HandlerMethod handlerMethod = new HandlerMethod(new JacksonController(), method);
MethodParameter methodReturnType = handlerMethod.getReturnType();
List<HttpMessageConverter<?>> converters = new ArrayList<HttpMessageConverter<?>>();
@ -380,7 +382,7 @@ public class RequestResponseBodyMethodProcessorTests {
HttpEntityMethodProcessor processor = new HttpEntityMethodProcessor(
converters, null, Collections.singletonList(new JsonViewResponseBodyAdvice()));
Object returnValue = new JacksonViewController().handleResponseEntity();
Object returnValue = new JacksonController().handleResponseEntity();
processor.handleReturnValue(returnValue, methodReturnType, this.mavContainer, this.webRequest);
String content = this.servletResponse.getContentAsString();
@ -391,8 +393,8 @@ public class RequestResponseBodyMethodProcessorTests {
@Test // SPR-12149
public void jacksonJsonViewWithResponseBodyAndXmlMessageConverter() throws Exception {
Method method = JacksonViewController.class.getMethod("handleResponseBody");
HandlerMethod handlerMethod = new HandlerMethod(new JacksonViewController(), method);
Method method = JacksonController.class.getMethod("handleResponseBody");
HandlerMethod handlerMethod = new HandlerMethod(new JacksonController(), method);
MethodParameter methodReturnType = handlerMethod.getReturnType();
List<HttpMessageConverter<?>> converters = new ArrayList<HttpMessageConverter<?>>();
@ -401,7 +403,7 @@ public class RequestResponseBodyMethodProcessorTests {
RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor(
converters, null, Collections.singletonList(new JsonViewResponseBodyAdvice()));
Object returnValue = new JacksonViewController().handleResponseBody();
Object returnValue = new JacksonController().handleResponseBody();
processor.handleReturnValue(returnValue, methodReturnType, this.mavContainer, this.webRequest);
String content = this.servletResponse.getContentAsString();
@ -412,8 +414,8 @@ public class RequestResponseBodyMethodProcessorTests {
@Test // SPR-12149
public void jacksonJsonViewWithResponseEntityAndXmlMessageConverter() throws Exception {
Method method = JacksonViewController.class.getMethod("handleResponseEntity");
HandlerMethod handlerMethod = new HandlerMethod(new JacksonViewController(), method);
Method method = JacksonController.class.getMethod("handleResponseEntity");
HandlerMethod handlerMethod = new HandlerMethod(new JacksonController(), method);
MethodParameter methodReturnType = handlerMethod.getReturnType();
List<HttpMessageConverter<?>> converters = new ArrayList<HttpMessageConverter<?>>();
@ -422,7 +424,7 @@ public class RequestResponseBodyMethodProcessorTests {
HttpEntityMethodProcessor processor = new HttpEntityMethodProcessor(
converters, null, Collections.singletonList(new JsonViewResponseBodyAdvice()));
Object returnValue = new JacksonViewController().handleResponseEntity();
Object returnValue = new JacksonController().handleResponseEntity();
processor.handleReturnValue(returnValue, methodReturnType, this.mavContainer, this.webRequest);
String content = this.servletResponse.getContentAsString();
@ -437,8 +439,8 @@ public class RequestResponseBodyMethodProcessorTests {
this.servletRequest.setContent(content.getBytes("UTF-8"));
this.servletRequest.setContentType(MediaType.APPLICATION_JSON_VALUE);
Method method = JacksonViewController.class.getMethod("handleRequestBody", JacksonViewBean.class);
HandlerMethod handlerMethod = new HandlerMethod(new JacksonViewController(), method);
Method method = JacksonController.class.getMethod("handleRequestBody", JacksonViewBean.class);
HandlerMethod handlerMethod = new HandlerMethod(new JacksonController(), method);
MethodParameter methodParameter = handlerMethod.getMethodParameters()[0];
List<HttpMessageConverter<?>> converters = new ArrayList<HttpMessageConverter<?>>();
@ -463,8 +465,8 @@ public class RequestResponseBodyMethodProcessorTests {
this.servletRequest.setContent(content.getBytes("UTF-8"));
this.servletRequest.setContentType(MediaType.APPLICATION_JSON_VALUE);
Method method = JacksonViewController.class.getMethod("handleHttpEntity", HttpEntity.class);
HandlerMethod handlerMethod = new HandlerMethod(new JacksonViewController(), method);
Method method = JacksonController.class.getMethod("handleHttpEntity", HttpEntity.class);
HandlerMethod handlerMethod = new HandlerMethod(new JacksonController(), method);
MethodParameter methodParameter = handlerMethod.getMethodParameters()[0];
List<HttpMessageConverter<?>> converters = new ArrayList<HttpMessageConverter<?>>();
@ -490,8 +492,8 @@ public class RequestResponseBodyMethodProcessorTests {
this.servletRequest.setContent(content.getBytes("UTF-8"));
this.servletRequest.setContentType(MediaType.APPLICATION_XML_VALUE);
Method method = JacksonViewController.class.getMethod("handleRequestBody", JacksonViewBean.class);
HandlerMethod handlerMethod = new HandlerMethod(new JacksonViewController(), method);
Method method = JacksonController.class.getMethod("handleRequestBody", JacksonViewBean.class);
HandlerMethod handlerMethod = new HandlerMethod(new JacksonController(), method);
MethodParameter methodParameter = handlerMethod.getMethodParameters()[0];
List<HttpMessageConverter<?>> converters = new ArrayList<HttpMessageConverter<?>>();
@ -516,8 +518,8 @@ public class RequestResponseBodyMethodProcessorTests {
this.servletRequest.setContent(content.getBytes("UTF-8"));
this.servletRequest.setContentType(MediaType.APPLICATION_XML_VALUE);
Method method = JacksonViewController.class.getMethod("handleHttpEntity", HttpEntity.class);
HandlerMethod handlerMethod = new HandlerMethod(new JacksonViewController(), method);
Method method = JacksonController.class.getMethod("handleHttpEntity", HttpEntity.class);
HandlerMethod handlerMethod = new HandlerMethod(new JacksonController(), method);
MethodParameter methodParameter = handlerMethod.getMethodParameters()[0];
List<HttpMessageConverter<?>> converters = new ArrayList<HttpMessageConverter<?>>();
@ -537,6 +539,24 @@ public class RequestResponseBodyMethodProcessorTests {
assertNull(result.getBody().getWithoutView());
}
@Test // SPR-12811
public void jacksonTypeInfoList() throws Exception {
Method method = JacksonController.class.getMethod("handleList");
HandlerMethod handlerMethod = new HandlerMethod(new JacksonController(), method);
MethodParameter methodReturnType = handlerMethod.getReturnType();
List<HttpMessageConverter<?>> converters = new ArrayList<HttpMessageConverter<?>>();
converters.add(new MappingJackson2HttpMessageConverter());
RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor(converters);
Object returnValue = new JacksonController().handleList();
processor.handleReturnValue(returnValue, methodReturnType, this.mavContainer, this.webRequest);
String content = this.servletResponse.getContentAsString();
assertTrue(content.contains("\"type\":\"foo\""));
assertTrue(content.contains("\"type\":\"bar\""));
}
String handle(
@RequestBody List<SimpleBean> list,
@ -670,7 +690,50 @@ public class RequestResponseBodyMethodProcessorTests {
}
}
private static class JacksonViewController {
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type")
public static class ParentClass {
private String parentProperty;
public ParentClass() {
}
public ParentClass(String parentProperty) {
this.parentProperty = parentProperty;
}
public String getParentProperty() {
return parentProperty;
}
public void setParentProperty(String parentProperty) {
this.parentProperty = parentProperty;
}
}
@JsonTypeName("foo")
public static class Foo extends ParentClass {
public Foo() {
}
public Foo(String parentProperty) {
super(parentProperty);
}
}
@JsonTypeName("bar")
public static class Bar extends ParentClass {
public Bar() {
}
public Bar(String parentProperty) {
super(parentProperty);
}
}
private static class JacksonController {
@RequestMapping
@ResponseBody
@ -706,8 +769,17 @@ public class RequestResponseBodyMethodProcessorTests {
public JacksonViewBean handleHttpEntity(@JsonView(MyJacksonView1.class) HttpEntity<JacksonViewBean> entity) {
return entity.getBody();
}
}
@RequestMapping
@ResponseBody
public List<ParentClass> handleList() {
List<ParentClass> list = new ArrayList<>();
list.add(new Foo("foo"));
list.add(new Bar("bar"));
return list;
}
}
private static class EmptyRequestBodyAdvice implements RequestBodyAdvice {
@Override