Skip to content

Commit 31a5434

Browse files
committed
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
1 parent 04a7ed5 commit 31a5434

File tree

12 files changed

+442
-76
lines changed

12 files changed

+442
-76
lines changed
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
/*
2+
* Copyright 2002-2015 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.http.converter;
18+
19+
import java.io.IOException;
20+
import java.io.OutputStream;
21+
import java.lang.reflect.Type;
22+
23+
import org.springframework.http.HttpHeaders;
24+
import org.springframework.http.HttpOutputMessage;
25+
import org.springframework.http.MediaType;
26+
import org.springframework.http.StreamingHttpOutputMessage;
27+
28+
/**
29+
* Abstract base class for most {@link GenericHttpMessageConverter} implementations.
30+
*
31+
* @author Sebastien Deleuze
32+
* @since 4.2
33+
*/
34+
public abstract class AbstractGenericHttpMessageConverter<T> extends AbstractHttpMessageConverter<T>
35+
implements GenericHttpMessageConverter<T> {
36+
37+
/**
38+
* Construct an {@code AbstractGenericHttpMessageConverter} with no supported media types.
39+
* @see #setSupportedMediaTypes
40+
*/
41+
protected AbstractGenericHttpMessageConverter() {
42+
}
43+
44+
/**
45+
* Construct an {@code AbstractGenericHttpMessageConverter} with one supported media type.
46+
* @param supportedMediaType the supported media type
47+
*/
48+
protected AbstractGenericHttpMessageConverter(MediaType supportedMediaType) {
49+
super(supportedMediaType);
50+
}
51+
52+
/**
53+
* Construct an {@code AbstractGenericHttpMessageConverter} with multiple supported media type.
54+
* @param supportedMediaTypes the supported media types
55+
*/
56+
protected AbstractGenericHttpMessageConverter(MediaType... supportedMediaTypes) {
57+
super(supportedMediaTypes);
58+
}
59+
60+
@Override
61+
public boolean canWrite(Class<?> contextClass, MediaType mediaType) {
62+
return canWrite(null, contextClass, mediaType);
63+
}
64+
65+
/**
66+
* This implementation sets the default headers by calling {@link #addDefaultHeaders},
67+
* and then calls {@link #writeInternal}.
68+
*/
69+
public final void write(final T t, final Type type, MediaType contentType, HttpOutputMessage outputMessage)
70+
throws IOException, HttpMessageNotWritableException {
71+
72+
final HttpHeaders headers = outputMessage.getHeaders();
73+
addDefaultHeaders(headers, t, contentType);
74+
75+
if (outputMessage instanceof StreamingHttpOutputMessage) {
76+
StreamingHttpOutputMessage streamingOutputMessage =
77+
(StreamingHttpOutputMessage) outputMessage;
78+
streamingOutputMessage.setBody(new StreamingHttpOutputMessage.Body() {
79+
@Override
80+
public void writeTo(final OutputStream outputStream) throws IOException {
81+
writeInternal(t, type, new HttpOutputMessage() {
82+
@Override
83+
public OutputStream getBody() throws IOException {
84+
return outputStream;
85+
}
86+
@Override
87+
public HttpHeaders getHeaders() {
88+
return headers;
89+
}
90+
});
91+
}
92+
});
93+
}
94+
else {
95+
writeInternal(t, type, outputMessage);
96+
outputMessage.getBody().flush();
97+
}
98+
}
99+
100+
101+
@Override
102+
protected void writeInternal(T t, HttpOutputMessage outputMessage)
103+
throws IOException, HttpMessageNotWritableException {
104+
writeInternal(t, null, outputMessage);
105+
}
106+
107+
/**
108+
* Abstract template method that writes the actual body. Invoked from {@link #write}.
109+
* @param t the object to write to the output message
110+
* @param type the type of object to write, can be {@code null} if not specified.
111+
* @param outputMessage the HTTP output message to write to
112+
* @throws IOException in case of I/O errors
113+
* @throws HttpMessageNotWritableException in case of conversion errors
114+
*/
115+
protected abstract void writeInternal(T t, Type type, HttpOutputMessage outputMessage)
116+
throws IOException, HttpMessageNotWritableException;
117+
118+
}

spring-web/src/main/java/org/springframework/http/converter/AbstractHttpMessageConverter.java

Lines changed: 33 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -160,34 +160,15 @@ public final T read(Class<? extends T> clazz, HttpInputMessage inputMessage) thr
160160
}
161161

162162
/**
163-
* This implementation delegates to {@link #getDefaultContentType(Object)} if a content
164-
* type was not provided, calls {@link #getContentLength}, and sets the corresponding headers
165-
* on the output message. It then calls {@link #writeInternal}.
163+
* This implementation sets the default headers by calling {@link #addDefaultHeaders},
164+
* and then calls {@link #writeInternal}.
166165
*/
167166
@Override
168167
public final void write(final T t, MediaType contentType, HttpOutputMessage outputMessage)
169168
throws IOException, HttpMessageNotWritableException {
170169

171170
final HttpHeaders headers = outputMessage.getHeaders();
172-
if (headers.getContentType() == null) {
173-
MediaType contentTypeToUse = contentType;
174-
if (contentType == null || contentType.isWildcardType() || contentType.isWildcardSubtype()) {
175-
contentTypeToUse = getDefaultContentType(t);
176-
}
177-
else if (MediaType.APPLICATION_OCTET_STREAM.equals(contentType)) {
178-
MediaType type = getDefaultContentType(t);
179-
contentTypeToUse = (type != null ? type : contentTypeToUse);
180-
}
181-
if (contentTypeToUse != null) {
182-
headers.setContentType(contentTypeToUse);
183-
}
184-
}
185-
if (headers.getContentLength() == -1) {
186-
Long contentLength = getContentLength(t, headers.getContentType());
187-
if (contentLength != null) {
188-
headers.setContentLength(contentLength);
189-
}
190-
}
171+
addDefaultHeaders(headers, t, contentType);
191172

192173
if (outputMessage instanceof StreamingHttpOutputMessage) {
193174
StreamingHttpOutputMessage streamingOutputMessage =
@@ -214,6 +195,36 @@ public HttpHeaders getHeaders() {
214195
}
215196
}
216197

198+
/**
199+
* Add default headers to the output message.
200+
* <p>This implementation delegates to {@link #getDefaultContentType(Object)} if a content
201+
* type was not provided, calls {@link #getContentLength}, and sets the corresponding headers
202+
* @since 4.2
203+
*/
204+
protected void addDefaultHeaders(final HttpHeaders headers, final T t, MediaType contentType)
205+
throws IOException{
206+
207+
if (headers.getContentType() == null) {
208+
MediaType contentTypeToUse = contentType;
209+
if (contentType == null || contentType.isWildcardType() || contentType.isWildcardSubtype()) {
210+
contentTypeToUse = getDefaultContentType(t);
211+
}
212+
else if (MediaType.APPLICATION_OCTET_STREAM.equals(contentType)) {
213+
MediaType mediaType = getDefaultContentType(t);
214+
contentTypeToUse = (mediaType != null ? mediaType : contentTypeToUse);
215+
}
216+
if (contentTypeToUse != null) {
217+
headers.setContentType(contentTypeToUse);
218+
}
219+
}
220+
if (headers.getContentLength() == -1) {
221+
Long contentLength = getContentLength(t, headers.getContentType());
222+
if (contentLength != null) {
223+
headers.setContentLength(contentLength);
224+
}
225+
}
226+
}
227+
217228
/**
218229
* Returns the default content type for the given type. Called when {@link #write}
219230
* is invoked without a specified content type parameter.

spring-web/src/main/java/org/springframework/http/converter/GenericHttpMessageConverter.java

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,17 @@
2020
import java.lang.reflect.Type;
2121

2222
import org.springframework.http.HttpInputMessage;
23+
import org.springframework.http.HttpOutputMessage;
2324
import org.springframework.http.MediaType;
2425

2526
/**
26-
* A specialization of {@link HttpMessageConverter} that can convert an HTTP
27-
* request into a target object of a specified generic type.
27+
* A specialization of {@link HttpMessageConverter} that can convert an HTTP request
28+
* into a target object of a specified generic type and a source object of a specified
29+
* generic type into an HTTP response.
2830
*
2931
* @author Arjen Poutsma
3032
* @author Rossen Stoyanchev
33+
* @author Sebastien Deleuze
3134
* @since 3.2
3235
* @see org.springframework.core.ParameterizedTypeReference
3336
*/
@@ -59,4 +62,34 @@ public interface GenericHttpMessageConverter<T> extends HttpMessageConverter<T>
5962
T read(Type type, Class<?> contextClass, HttpInputMessage inputMessage)
6063
throws IOException, HttpMessageNotReadableException;
6164

65+
/**
66+
* Indicates whether the given class can be written by this converter.
67+
* @param type the type to test for writability, can be {@code null} if not specified.
68+
* @param contextClass the class to test for writability
69+
* @param mediaType the media type to write, can be {@code null} if not specified.
70+
* Typically the value of an {@code Accept} header.
71+
* @return {@code true} if writable; {@code false} otherwise
72+
* @since 4.2
73+
*/
74+
boolean canWrite(Type type, Class<?> contextClass, MediaType mediaType);
75+
76+
/**
77+
* Write an given object to the given output message.
78+
* @param t the object to write to the output message. The type of this object must have previously been
79+
* passed to the {@link #canWrite canWrite} method of this interface, which must have returned {@code true}.
80+
* @param type the type of object to write. This type must have previously
81+
* been passed to the {@link #canWrite canWrite} method of this interface,
82+
* which must have returned {@code true}. Can be {@code null} if not specified.
83+
* @param contentType the content type to use when writing. May be {@code null} to indicate that the
84+
* default content type of the converter must be used. If not {@code null}, this media type must have
85+
* previously been passed to the {@link #canWrite canWrite} method of this interface, which must have
86+
* returned {@code true}.
87+
* @param outputMessage the message to write to
88+
* @throws IOException in case of I/O errors
89+
* @throws HttpMessageNotWritableException in case of conversion errors
90+
* @since 4.2
91+
*/
92+
void write(T t, Type type, MediaType contentType, HttpOutputMessage outputMessage)
93+
throws IOException, HttpMessageNotWritableException;
94+
6295
}

spring-web/src/main/java/org/springframework/http/converter/json/AbstractJackson2HttpMessageConverter.java

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,19 +27,21 @@
2727
import com.fasterxml.jackson.core.util.DefaultPrettyPrinter;
2828
import com.fasterxml.jackson.databind.JavaType;
2929
import com.fasterxml.jackson.databind.ObjectMapper;
30+
import com.fasterxml.jackson.databind.ObjectWriter;
3031
import com.fasterxml.jackson.databind.SerializationFeature;
3132
import com.fasterxml.jackson.databind.ser.FilterProvider;
3233

3334
import org.springframework.http.HttpInputMessage;
3435
import org.springframework.http.HttpOutputMessage;
3536
import org.springframework.http.MediaType;
36-
import org.springframework.http.converter.AbstractHttpMessageConverter;
37+
import org.springframework.http.converter.AbstractGenericHttpMessageConverter;
3738
import org.springframework.http.converter.GenericHttpMessageConverter;
3839
import org.springframework.http.converter.HttpMessageConverter;
3940
import org.springframework.http.converter.HttpMessageNotReadableException;
4041
import org.springframework.http.converter.HttpMessageNotWritableException;
4142
import org.springframework.util.Assert;
4243
import org.springframework.util.ClassUtils;
44+
import org.springframework.util.TypeUtils;
4345

4446
/**
4547
* Abstract base class for Jackson based and content type independent
@@ -54,7 +56,7 @@
5456
* @author Sebastien Deleuze
5557
* @since 4.1
5658
*/
57-
public abstract class AbstractJackson2HttpMessageConverter extends AbstractHttpMessageConverter<Object>
59+
public abstract class AbstractJackson2HttpMessageConverter extends AbstractGenericHttpMessageConverter<Object>
5860
implements GenericHttpMessageConverter<Object> {
5961

6062
public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
@@ -158,7 +160,7 @@ public boolean canRead(Type type, Class<?> contextClass, MediaType mediaType) {
158160
}
159161

160162
@Override
161-
public boolean canWrite(Class<?> clazz, MediaType mediaType) {
163+
public boolean canWrite(Type type, Class<?> clazz, MediaType mediaType) {
162164
if (!jackson23Available || !logger.isWarnEnabled()) {
163165
return (this.objectMapper.canSerialize(clazz) && canWrite(mediaType));
164166
}
@@ -218,31 +220,43 @@ private Object readJavaType(JavaType javaType, HttpInputMessage inputMessage) {
218220
}
219221

220222
@Override
221-
protected void writeInternal(Object object, HttpOutputMessage outputMessage)
223+
@SuppressWarnings("deprecation")
224+
protected void writeInternal(Object object, Type type, HttpOutputMessage outputMessage)
222225
throws IOException, HttpMessageNotWritableException {
223226

224227
JsonEncoding encoding = getJsonEncoding(outputMessage.getHeaders().getContentType());
225228
JsonGenerator generator = this.objectMapper.getFactory().createGenerator(outputMessage.getBody(), encoding);
226229
try {
227230
writePrefix(generator, object);
231+
228232
Class<?> serializationView = null;
229233
FilterProvider filters = null;
230234
Object value = object;
231-
if (value instanceof MappingJacksonValue) {
235+
JavaType javaType = null;
236+
if (type != null) {
237+
javaType = getJavaType(type, null);
238+
}
239+
if (object instanceof MappingJacksonValue) {
232240
MappingJacksonValue container = (MappingJacksonValue) object;
233241
value = container.getValue();
234242
serializationView = container.getSerializationView();
235243
filters = container.getFilters();
236244
}
245+
ObjectWriter objectWriter;
237246
if (serializationView != null) {
238-
this.objectMapper.writerWithView(serializationView).writeValue(generator, value);
247+
objectWriter = this.objectMapper.writerWithView(serializationView);
239248
}
240249
else if (filters != null) {
241-
this.objectMapper.writer(filters).writeValue(generator, value);
250+
objectWriter = this.objectMapper.writer(filters);
242251
}
243252
else {
244-
this.objectMapper.writeValue(generator, value);
253+
objectWriter = this.objectMapper.writer();
245254
}
255+
if (javaType != null && value != null && TypeUtils.isAssignable(type, value.getClass())) {
256+
objectWriter = objectWriter.withType(javaType);
257+
}
258+
objectWriter.writeValue(generator, value);
259+
246260
writeSuffix(generator, object);
247261
generator.flush();
248262

spring-web/src/main/java/org/springframework/http/converter/json/GsonHttpMessageConverter.java

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2014 the original author or authors.
2+
* Copyright 2002-2015 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.
@@ -32,7 +32,7 @@
3232
import org.springframework.http.HttpInputMessage;
3333
import org.springframework.http.HttpOutputMessage;
3434
import org.springframework.http.MediaType;
35-
import org.springframework.http.converter.AbstractHttpMessageConverter;
35+
import org.springframework.http.converter.AbstractGenericHttpMessageConverter;
3636
import org.springframework.http.converter.GenericHttpMessageConverter;
3737
import org.springframework.http.converter.HttpMessageNotReadableException;
3838
import org.springframework.http.converter.HttpMessageNotWritableException;
@@ -54,7 +54,7 @@
5454
* @see #setGson
5555
* @see #setSupportedMediaTypes
5656
*/
57-
public class GsonHttpMessageConverter extends AbstractHttpMessageConverter<Object>
57+
public class GsonHttpMessageConverter extends AbstractGenericHttpMessageConverter<Object>
5858
implements GenericHttpMessageConverter<Object> {
5959

6060
public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
@@ -125,7 +125,7 @@ public boolean canRead(Type type, Class<?> contextClass, MediaType mediaType) {
125125
}
126126

127127
@Override
128-
public boolean canWrite(Class<?> clazz, MediaType mediaType) {
128+
public boolean canWrite(Type type, Class<?> clazz, MediaType mediaType) {
129129
return canWrite(mediaType);
130130
}
131131

@@ -191,7 +191,7 @@ private Charset getCharset(HttpHeaders headers) {
191191
}
192192

193193
@Override
194-
protected void writeInternal(Object o, HttpOutputMessage outputMessage)
194+
protected void writeInternal(Object o, Type type, HttpOutputMessage outputMessage)
195195
throws IOException, HttpMessageNotWritableException {
196196

197197
Charset charset = getCharset(outputMessage.getHeaders());
@@ -200,7 +200,12 @@ protected void writeInternal(Object o, HttpOutputMessage outputMessage)
200200
if (this.jsonPrefix != null) {
201201
writer.append(this.jsonPrefix);
202202
}
203-
this.gson.toJson(o, writer);
203+
if (type != null) {
204+
this.gson.toJson(o, type, writer);
205+
}
206+
else {
207+
this.gson.toJson(o, writer);
208+
}
204209
writer.close();
205210
}
206211
catch (JsonIOException ex) {

0 commit comments

Comments
 (0)