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();