Extracted AbstractJsonHttpMessageConverter from GsonHttpMessageConverter

Generic type resolution algorithm in GenericTypeResolver shared between Jackson and Gson.

Issue: SPR-15381
This commit is contained in:
Juergen Hoeller
2017-04-10 15:36:29 +02:00
parent ea98ee820a
commit e5fdd4cd1d
12 changed files with 337 additions and 659 deletions

View File

@@ -17,9 +17,7 @@
package org.springframework.http.codec.json;
import java.lang.annotation.Annotation;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Collections;
@@ -32,14 +30,14 @@ import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.type.TypeFactory;
import org.springframework.core.GenericTypeResolver;
import org.springframework.core.MethodParameter;
import org.springframework.core.ResolvableType;
import org.springframework.util.Assert;
import org.springframework.util.MimeType;
import org.springframework.util.MimeTypeUtils;
/**
* Base class providing support methods for Jackson 2 encoding and decoding.
* Base class providing support methods for Jackson 2.9 encoding and decoding.
*
* @author Sebastien Deleuze
* @author Rossen Stoyanchev
@@ -62,99 +60,26 @@ public abstract class Jackson2CodecSupport {
new MimeType("application", "*+json", StandardCharsets.UTF_8));
protected final ObjectMapper mapper;
protected final ObjectMapper objectMapper;
/**
* Constructor with a Jackson {@link ObjectMapper} to use.
*/
protected Jackson2CodecSupport(ObjectMapper mapper) {
Assert.notNull(mapper, "ObjectMapper must not be null");
this.mapper = mapper;
protected Jackson2CodecSupport(ObjectMapper objectMapper) {
Assert.notNull(objectMapper, "ObjectMapper must not be null");
this.objectMapper = objectMapper;
}
protected boolean supportsMimeType(MimeType mimeType) {
return mimeType == null ||
JSON_MIME_TYPES.stream().anyMatch(m -> m.isCompatibleWith(mimeType));
return (mimeType == null ||
JSON_MIME_TYPES.stream().anyMatch(m -> m.isCompatibleWith(mimeType)));
}
/**
* Return the Jackson {@link JavaType} for the specified type and context class.
* <p>The default implementation returns {@code typeFactory.constructType(type, contextClass)},
* but this can be overridden in subclasses, to allow for custom generic collection handling.
* For instance:
* <pre class="code">
* protected JavaType getJavaType(Type type) {
* if (type instanceof Class && List.class.isAssignableFrom((Class)type)) {
* return TypeFactory.collectionType(ArrayList.class, MyBean.class);
* } else {
* return super.getJavaType(type);
* }
* }
* </pre>
* @param type the generic type to return the Jackson JavaType 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 Jackson JavaType
*/
protected JavaType getJavaType(Type type, Class<?> contextClass) {
TypeFactory typeFactory = this.mapper.getTypeFactory();
if (contextClass != null) {
ResolvableType resolvedType = ResolvableType.forType(type);
if (type instanceof TypeVariable) {
ResolvableType resolvedTypeVariable = resolveVariable(
(TypeVariable<?>) type, ResolvableType.forClass(contextClass));
if (resolvedTypeVariable != ResolvableType.NONE) {
return typeFactory.constructType(resolvedTypeVariable.resolve());
}
}
else if (type instanceof ParameterizedType && resolvedType.hasUnresolvableGenerics()) {
ParameterizedType parameterizedType = (ParameterizedType) type;
Class<?>[] generics = new Class<?>[parameterizedType.getActualTypeArguments().length];
Type[] typeArguments = parameterizedType.getActualTypeArguments();
for (int i = 0; i < typeArguments.length; i++) {
Type typeArgument = typeArguments[i];
if (typeArgument instanceof TypeVariable) {
ResolvableType resolvedTypeArgument = resolveVariable(
(TypeVariable<?>) typeArgument, ResolvableType.forClass(contextClass));
if (resolvedTypeArgument != ResolvableType.NONE) {
generics[i] = resolvedTypeArgument.resolve();
}
else {
generics[i] = ResolvableType.forType(typeArgument).resolve();
}
}
else {
generics[i] = ResolvableType.forType(typeArgument).resolve();
}
}
return typeFactory.constructType(ResolvableType.
forClassWithGenerics(resolvedType.getRawClass(), generics).getType());
}
}
return typeFactory.constructType(type);
}
private ResolvableType resolveVariable(TypeVariable<?> typeVariable, ResolvableType contextType) {
ResolvableType resolvedType;
if (contextType.hasGenerics()) {
resolvedType = ResolvableType.forType(typeVariable, contextType);
if (resolvedType.resolve() != null) {
return resolvedType;
}
}
resolvedType = resolveVariable(typeVariable, contextType.getSuperType());
if (resolvedType.resolve() != null) {
return resolvedType;
}
for (ResolvableType ifc : contextType.getInterfaces()) {
resolvedType = resolveVariable(typeVariable, ifc);
if (resolvedType.resolve() != null) {
return resolvedType;
}
}
return ResolvableType.NONE;
TypeFactory typeFactory = this.objectMapper.getTypeFactory();
return typeFactory.constructType(GenericTypeResolver.resolveType(type, contextClass));
}
protected Map<String, Object> getHints(ResolvableType actualType) {
@@ -169,7 +94,7 @@ public abstract class Jackson2CodecSupport {
}
protected Optional<MethodParameter> getParameter(ResolvableType type) {
return Optional.ofNullable (type.getSource() instanceof MethodParameter ?
return Optional.ofNullable(type.getSource() instanceof MethodParameter ?
(MethodParameter) type.getSource() : null);
}

View File

@@ -41,7 +41,7 @@ import org.springframework.util.Assert;
import org.springframework.util.MimeType;
/**
* Decode a byte stream into JSON and convert to Object's with Jackson 2.6+.
* Decode a byte stream into JSON and convert to Object's with Jackson 2.9.
*
* @author Sebastien Deleuze
* @author Rossen Stoyanchev
@@ -66,10 +66,10 @@ public class Jackson2JsonDecoder extends Jackson2CodecSupport implements HttpMes
@Override
public boolean canDecode(ResolvableType elementType, MimeType mimeType) {
JavaType javaType = this.mapper.getTypeFactory().constructType(elementType.getType());
JavaType javaType = this.objectMapper.getTypeFactory().constructType(elementType.getType());
// Skip String (CharSequenceDecoder + "*/*" comes after)
return !CharSequence.class.isAssignableFrom(elementType.resolve(Object.class)) &&
this.mapper.canDeserialize(javaType) && supportsMimeType(mimeType);
return (!CharSequence.class.isAssignableFrom(elementType.resolve(Object.class)) &&
this.objectMapper.canDeserialize(javaType) && supportsMimeType(mimeType));
}
@@ -102,9 +102,9 @@ public class Jackson2JsonDecoder extends Jackson2CodecSupport implements HttpMes
JavaType javaType = getJavaType(elementType.getType(), contextClass);
Class<?> jsonView = (Class<?>) hints.get(Jackson2CodecSupport.JSON_VIEW_HINT);
ObjectReader reader = jsonView != null ?
this.mapper.readerWithView(jsonView).forType(javaType) :
this.mapper.readerFor(javaType);
ObjectReader reader = (jsonView != null ?
this.objectMapper.readerWithView(jsonView).forType(javaType) :
this.objectMapper.readerFor(javaType));
return objectDecoder.decode(inputStream, elementType, mimeType, hints)
.map(dataBuffer -> {

View File

@@ -52,7 +52,7 @@ import org.springframework.util.MimeType;
/**
* Encode from an {@code Object} stream to a byte stream of JSON objects,
* using Jackson 2.6+.
* using Jackson 2.9.
*
* @author Sebastien Deleuze
* @author Arjen Poutsma
@@ -105,7 +105,7 @@ public class Jackson2JsonEncoder extends Jackson2CodecSupport implements HttpMes
@Override
public boolean canEncode(ResolvableType elementType, MimeType mimeType) {
Class<?> clazz = elementType.getRawClass();
return this.mapper.canSerialize(clazz) && supportsMimeType(mimeType);
return (this.objectMapper.canSerialize(clazz) && supportsMimeType(mimeType));
}
@Override
@@ -137,14 +137,15 @@ public class Jackson2JsonEncoder extends Jackson2CodecSupport implements HttpMes
private DataBuffer encodeValue(Object value, MimeType mimeType, DataBufferFactory bufferFactory,
ResolvableType elementType, Map<String, Object> hints) {
TypeFactory typeFactory = this.mapper.getTypeFactory();
TypeFactory typeFactory = this.objectMapper.getTypeFactory();
JavaType javaType = typeFactory.constructType(elementType.getType());
if (elementType.isInstance(value)) {
javaType = getJavaType(elementType.getType(), null);
}
Class<?> jsonView = (Class<?>) hints.get(Jackson2CodecSupport.JSON_VIEW_HINT);
ObjectWriter writer = jsonView != null ? this.mapper.writerWithView(jsonView): this.mapper.writer();
ObjectWriter writer = (jsonView != null ?
this.objectMapper.writerWithView(jsonView) : this.objectMapper.writer());
if (javaType != null && javaType.isContainerType()) {
writer = writer.forType(javaType);

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2015 the original author or authors.
* Copyright 2002-2017 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.
@@ -58,14 +58,19 @@ public abstract class AbstractGenericHttpMessageConverter<T> extends AbstractHtt
}
@Override
protected boolean supports(Class<?> clazz) {
return true;
}
@Override
public boolean canRead(Type type, Class<?> contextClass, MediaType mediaType) {
return canRead(contextClass, mediaType);
return canRead(mediaType);
}
@Override
public boolean canWrite(Type type, Class<?> clazz, MediaType mediaType) {
return canWrite(clazz, mediaType);
return canWrite(mediaType);
}
/**
@@ -102,7 +107,6 @@ public abstract class AbstractGenericHttpMessageConverter<T> extends AbstractHtt
}
}
@Override
protected void writeInternal(T t, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
@@ -113,7 +117,7 @@ public abstract class AbstractGenericHttpMessageConverter<T> extends AbstractHtt
/**
* 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 type the type of object to write (may be {@code null})
* @param outputMessage the HTTP output message to write to
* @throws IOException in case of I/O errors
* @throws HttpMessageNotWritableException in case of conversion errors

View File

@@ -49,31 +49,6 @@ public class ResourceRegionHttpMessageConverter extends AbstractGenericHttpMessa
}
@Override
protected boolean supports(Class<?> clazz) {
// should not be called as we override canRead/canWrite
return false;
}
@Override
public boolean canRead(Type type, Class<?> contextClass, MediaType mediaType) {
return false;
}
@Override
public Object read(Type type, Class<?> contextClass, HttpInputMessage inputMessage)
throws IOException, HttpMessageNotReadableException {
return null;
}
@Override
protected ResourceRegion readInternal(Class<?> clazz, HttpInputMessage inputMessage)
throws IOException, HttpMessageNotReadableException {
return null;
}
@Override
@SuppressWarnings("unchecked")
protected MediaType getDefaultContentType(Object object) {
@@ -90,6 +65,30 @@ public class ResourceRegionHttpMessageConverter extends AbstractGenericHttpMessa
return MediaTypeFactory.getMediaType(resource).orElse(MediaType.APPLICATION_OCTET_STREAM);
}
@Override
public boolean canRead(Class<?> clazz, MediaType mediaType) {
return false;
}
@Override
public boolean canRead(Type type, Class<?> contextClass, MediaType mediaType) {
return false;
}
@Override
public Object read(Type type, Class<?> contextClass, HttpInputMessage inputMessage)
throws IOException, HttpMessageNotReadableException {
throw new UnsupportedOperationException();
}
@Override
protected ResourceRegion readInternal(Class<?> clazz, HttpInputMessage inputMessage)
throws IOException, HttpMessageNotReadableException {
throw new UnsupportedOperationException();
}
@Override
public boolean canWrite(Class<?> clazz, MediaType mediaType) {
return canWrite(clazz, null, mediaType);
@@ -138,6 +137,7 @@ public class ResourceRegionHttpMessageConverter extends AbstractGenericHttpMessa
}
}
protected void writeResourceRegion(ResourceRegion region, HttpOutputMessage outputMessage) throws IOException {
Assert.notNull(region, "ResourceRegion must not be null");
HttpHeaders responseHeaders = outputMessage.getHeaders();
@@ -195,8 +195,6 @@ public class ResourceRegionHttpMessageConverter extends AbstractGenericHttpMessa
print(out, "--" + boundaryString + "--");
}
private static void println(OutputStream os) throws IOException {
os.write('\r');
os.write('\n');

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2016 the original author or authors.
* Copyright 2002-2017 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.
@@ -17,9 +17,7 @@
package org.springframework.http.converter.json;
import java.io.IOException;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.atomic.AtomicReference;
@@ -40,7 +38,7 @@ import com.fasterxml.jackson.databind.exc.InvalidDefinitionException;
import com.fasterxml.jackson.databind.ser.FilterProvider;
import com.fasterxml.jackson.databind.type.TypeFactory;
import org.springframework.core.ResolvableType;
import org.springframework.core.GenericTypeResolver;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.HttpStatus;
@@ -56,7 +54,7 @@ import org.springframework.util.TypeUtils;
* Abstract base class for Jackson based and content type independent
* {@link HttpMessageConverter} implementations.
*
* <p>Compatible with Jackson 2.6 and higher, as of Spring 4.3.
* <p>Compatible with Jackson 2.9 and higher, as of Spring 5.0.
*
* @author Arjen Poutsma
* @author Keith Donald
@@ -204,12 +202,6 @@ public abstract class AbstractJackson2HttpMessageConverter extends AbstractGener
}
}
@Override
protected boolean supports(Class<?> clazz) {
// should not be called, since we override canRead/Write instead
throw new UnsupportedOperationException();
}
@Override
protected Object readInternal(Class<?> clazz, HttpInputMessage inputMessage)
throws IOException, HttpMessageNotReadableException {
@@ -238,10 +230,11 @@ public abstract class AbstractJackson2HttpMessageConverter extends AbstractGener
return this.objectMapper.readValue(inputMessage.getBody(), javaType);
}
catch (InvalidDefinitionException ex) {
throw new HttpMessageNotReadableException("Could not read document: " + ex.getMessage(), ex, HttpStatus.INTERNAL_SERVER_ERROR);
throw new HttpMessageNotReadableException(
"Could not map JSON to target object of " + javaType, ex, HttpStatus.INTERNAL_SERVER_ERROR);
}
catch (IOException ex) {
throw new HttpMessageNotReadableException("Could not read document: " + ex.getMessage(), ex);
throw new HttpMessageNotReadableException("Could not read JSON document: " + ex.getMessage(), ex);
}
}
@@ -293,7 +286,7 @@ public abstract class AbstractJackson2HttpMessageConverter extends AbstractGener
}
catch (JsonProcessingException ex) {
throw new HttpMessageNotWritableException("Could not write content: " + ex.getMessage(), ex);
throw new HttpMessageNotWritableException("Could not write JSON document: " + ex.getMessage(), ex);
}
}
@@ -315,18 +308,6 @@ public abstract class AbstractJackson2HttpMessageConverter extends AbstractGener
/**
* Return the Jackson {@link JavaType} for the specified type and context class.
* <p>The default implementation returns {@code typeFactory.constructType(type, contextClass)},
* but this can be overridden in subclasses, to allow for custom generic collection handling.
* For instance:
* <pre class="code">
* protected JavaType getJavaType(Type type) {
* if (type instanceof Class && List.class.isAssignableFrom((Class)type)) {
* return TypeFactory.collectionType(ArrayList.class, MyBean.class);
* } else {
* return super.getJavaType(type);
* }
* }
* </pre>
* @param type the generic type to return the Jackson JavaType 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})
@@ -334,65 +315,7 @@ public abstract class AbstractJackson2HttpMessageConverter extends AbstractGener
*/
protected JavaType getJavaType(Type type, Class<?> contextClass) {
TypeFactory typeFactory = this.objectMapper.getTypeFactory();
if (contextClass != null) {
ResolvableType resolvedType = ResolvableType.forType(type);
if (type instanceof TypeVariable) {
ResolvableType resolvedTypeVariable = resolveVariable(
(TypeVariable<?>) type, ResolvableType.forClass(contextClass));
if (resolvedTypeVariable != ResolvableType.NONE) {
return typeFactory.constructType(resolvedTypeVariable.resolve());
}
}
else if (type instanceof ParameterizedType && resolvedType.hasUnresolvableGenerics()) {
ParameterizedType parameterizedType = (ParameterizedType) type;
Class<?>[] generics = new Class<?>[parameterizedType.getActualTypeArguments().length];
Type[] typeArguments = parameterizedType.getActualTypeArguments();
for (int i = 0; i < typeArguments.length; i++) {
Type typeArgument = typeArguments[i];
if (typeArgument instanceof TypeVariable) {
ResolvableType resolvedTypeArgument = resolveVariable(
(TypeVariable<?>) typeArgument, ResolvableType.forClass(contextClass));
if (resolvedTypeArgument != ResolvableType.NONE) {
generics[i] = resolvedTypeArgument.resolve();
}
else {
generics[i] = ResolvableType.forType(typeArgument).resolve();
}
}
else {
generics[i] = ResolvableType.forType(typeArgument).resolve();
}
}
return typeFactory.constructType(ResolvableType.
forClassWithGenerics(resolvedType.getRawClass(), generics).getType());
}
}
return typeFactory.constructType(type);
}
private ResolvableType resolveVariable(TypeVariable<?> typeVariable, ResolvableType contextType) {
ResolvableType resolvedType;
if (contextType.hasGenerics()) {
resolvedType = ResolvableType.forType(typeVariable, contextType);
if (resolvedType.resolve() != null) {
return resolvedType;
}
}
ResolvableType superType = contextType.getSuperType();
if (superType != ResolvableType.NONE) {
resolvedType = resolveVariable(typeVariable, superType);
if (resolvedType.resolve() != null) {
return resolvedType;
}
}
for (ResolvableType ifc : contextType.getInterfaces()) {
resolvedType = resolveVariable(typeVariable, ifc);
if (resolvedType.resolve() != null) {
return resolvedType;
}
}
return ResolvableType.NONE;
return typeFactory.constructType(GenericTypeResolver.resolveType(type, contextClass));
}
/**

View File

@@ -0,0 +1,161 @@
/*
* Copyright 2002-2017 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.json;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.Writer;
import java.lang.reflect.Type;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import org.springframework.core.GenericTypeResolver;
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.AbstractGenericHttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
/**
* Common base class for plain JSON converters, e.g. Gson and JSON-B.
*
* <p>Note that the Jackson converters have a dedicated class hierarchy
* due to their multi-format support.
*
* @author Juergen Hoeller
* @since 5.0
* @see GsonHttpMessageConverter
* @see JsonbHttpMessageConverter
* @see #readInternal(Type, Reader)
* @see #writeInternal(Object, Type, Writer)
*/
public abstract class AbstractJsonHttpMessageConverter extends AbstractGenericHttpMessageConverter<Object> {
public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
private String jsonPrefix;
public AbstractJsonHttpMessageConverter() {
super(MediaType.APPLICATION_JSON, new MediaType("application", "*+json"));
setDefaultCharset(DEFAULT_CHARSET);
}
/**
* Specify a custom prefix to use for JSON output. Default is none.
* @see #setPrefixJson
*/
public void setJsonPrefix(String jsonPrefix) {
this.jsonPrefix = jsonPrefix;
}
/**
* Indicate whether the JSON output by this view should be prefixed with ")]}', ".
* Default is {@code false}.
* <p>Prefixing the JSON string in this manner is used to help prevent JSON
* Hijacking. The prefix renders the string syntactically invalid as a script
* so that it cannot be hijacked.
* This prefix should be stripped before parsing the string as JSON.
* @see #setJsonPrefix
*/
public void setPrefixJson(boolean prefixJson) {
this.jsonPrefix = (prefixJson ? ")]}', " : null);
}
@Override
public final Object read(Type type, Class<?> contextClass, HttpInputMessage inputMessage)
throws IOException, HttpMessageNotReadableException {
return readResolved(GenericTypeResolver.resolveType(type, contextClass), inputMessage);
}
@Override
protected final Object readInternal(Class<?> clazz, HttpInputMessage inputMessage)
throws IOException, HttpMessageNotReadableException {
return readResolved(clazz, inputMessage);
}
private Object readResolved(Type resolvedType, HttpInputMessage inputMessage)
throws IOException, HttpMessageNotReadableException {
Reader reader = getReader(inputMessage);
try {
return readInternal(resolvedType, reader);
}
catch (Exception ex) {
throw new HttpMessageNotReadableException("Could not read JSON document: " + ex.getMessage(), ex);
}
}
@Override
protected final void writeInternal(Object o, Type type, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
Writer writer = getWriter(outputMessage);
if (this.jsonPrefix != null) {
writer.append(this.jsonPrefix);
}
try {
writeInternal(o, type, writer);
}
catch (Exception ex) {
throw new HttpMessageNotWritableException("Could not write JSON document: " + ex.getMessage(), ex);
}
writer.close();
}
/**
* Template method that reads the JSON-bound object from the given {@link Reader}.
* @param resolvedType the resolved generic type
* @param reader the {@code} Reader to use
* @return the JSON-bound object
* @throws Exception in case of read/parse failures
*/
protected abstract Object readInternal(Type resolvedType, Reader reader) throws Exception;
/**
* Template method that writes the JSON-bound object to the given {@link Writer}.
* @param o the object to write to the output message
* @param type the type of object to write (may be {@code null})
* @param writer the {@code} Writer to use
* @throws Exception in case of write failures
*/
protected abstract void writeInternal(Object o, Type type, Writer writer) throws Exception;
private static Reader getReader(HttpInputMessage inputMessage) throws IOException {
return new InputStreamReader(inputMessage.getBody(), getCharset(inputMessage.getHeaders()));
}
private static Writer getWriter(HttpOutputMessage outputMessage) throws IOException {
return new OutputStreamWriter(outputMessage.getBody(), getCharset(outputMessage.getHeaders()));
}
private static Charset getCharset(HttpHeaders headers) {
Charset charset = (headers.getContentType() != null ? headers.getContentType().getCharset() : null);
return (charset != null ? charset : DEFAULT_CHARSET);
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2016 the original author or authors.
* Copyright 2002-2017 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.
@@ -16,33 +16,18 @@
package org.springframework.http.converter.json;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.Writer;
import java.lang.reflect.Type;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import com.google.gson.Gson;
import com.google.gson.JsonIOException;
import com.google.gson.JsonParseException;
import com.google.gson.reflect.TypeToken;
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.AbstractGenericHttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
import org.springframework.util.Assert;
/**
* Implementation of {@link org.springframework.http.converter.HttpMessageConverter}
* that can read and write JSON using the
* <a href="https://code.google.com/p/google-gson/">Google Gson</a> library's
* {@link Gson} class.
* <a href="https://code.google.com/p/google-gson/">Google Gson</a> library.
*
* <p>This converter can be used to bind to typed beans or untyped {@code HashMap}s.
* By default, it supports {@code application/json} and {@code application/*+json} with
@@ -51,37 +36,43 @@ import org.springframework.util.Assert;
* <p>Tested against Gson 2.6; compatible with Gson 2.0 and higher.
*
* @author Roy Clarkson
* @author Juergen Hoeller
* @since 4.1
* @see com.google.gson.Gson
* @see com.google.gson.GsonBuilder
* @see #setGson
* @see #setSupportedMediaTypes
*/
public class GsonHttpMessageConverter extends AbstractGenericHttpMessageConverter<Object> {
public class GsonHttpMessageConverter extends AbstractJsonHttpMessageConverter {
public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
private Gson gson = new Gson();
private String jsonPrefix;
private Gson gson;
/**
* Construct a new {@code GsonHttpMessageConverter}.
* Construct a new {@code GsonHttpMessageConverter} with default configuration.
*/
public GsonHttpMessageConverter() {
super(MediaType.APPLICATION_JSON, new MediaType("application", "*+json"));
this.setDefaultCharset(DEFAULT_CHARSET);
this(new Gson());
}
/**
* Construct a new {@code GsonHttpMessageConverter} with the given delegate.
* @param gson the Gson instance to use
* @since 5.0
*/
public GsonHttpMessageConverter(Gson gson) {
setGson(gson);
}
/**
* Set the {@code Gson} instance to use.
* If not set, a default {@link Gson#Gson() Gson} instance is used.
* If not set, a default {@link Gson#Gson() Gson} instance will be used.
* <p>Setting a custom-configured {@code Gson} is one way to take further
* control of the JSON serialization process.
* @see #GsonHttpMessageConverter(Gson)
*/
public void setGson(Gson gson) {
Assert.notNull(gson, "'gson' is required");
Assert.notNull(gson, "A Gson instance is required");
this.gson = gson;
}
@@ -92,119 +83,19 @@ public class GsonHttpMessageConverter extends AbstractGenericHttpMessageConverte
return this.gson;
}
/**
* Specify a custom prefix to use for JSON output. Default is none.
* @see #setPrefixJson
*/
public void setJsonPrefix(String jsonPrefix) {
this.jsonPrefix = jsonPrefix;
}
/**
* Indicate whether the JSON output by this view should be prefixed with ")]}', ".
* Default is {@code false}.
* <p>Prefixing the JSON string in this manner is used to help prevent JSON
* Hijacking. The prefix renders the string syntactically invalid as a script
* so that it cannot be hijacked.
* This prefix should be stripped before parsing the string as JSON.
* @see #setJsonPrefix
*/
public void setPrefixJson(boolean prefixJson) {
this.jsonPrefix = (prefixJson ? ")]}', " : null);
}
@Override
public boolean canRead(Class<?> clazz, MediaType mediaType) {
return canRead(mediaType);
protected Object readInternal(Type resolvedType, Reader reader) throws Exception {
return getGson().fromJson(reader, resolvedType);
}
@Override
public boolean canWrite(Class<?> clazz, MediaType mediaType) {
return canWrite(mediaType);
}
@Override
protected boolean supports(Class<?> clazz) {
// should not be called, since we override canRead/Write instead
throw new UnsupportedOperationException();
}
@Override
protected Object readInternal(Class<?> clazz, HttpInputMessage inputMessage)
throws IOException, HttpMessageNotReadableException {
TypeToken<?> token = getTypeToken(clazz);
return readTypeToken(token, inputMessage);
}
@Override
public Object read(Type type, Class<?> contextClass, HttpInputMessage inputMessage)
throws IOException, HttpMessageNotReadableException {
TypeToken<?> token = getTypeToken(type);
return readTypeToken(token, inputMessage);
}
/**
* Return the Gson {@link TypeToken} for the specified type.
* <p>The default implementation returns {@code TypeToken.get(type)}, but
* this can be overridden in subclasses to allow for custom generic
* collection handling. For instance:
* <pre class="code">
* protected TypeToken<?> getTypeToken(Type type) {
* if (type instanceof Class && List.class.isAssignableFrom((Class<?>) type)) {
* return new TypeToken<ArrayList<MyBean>>() {};
* }
* else {
* return super.getTypeToken(type);
* }
* }
* </pre>
* @param type the type for which to return the TypeToken
* @return the type token
*/
protected TypeToken<?> getTypeToken(Type type) {
return TypeToken.get(type);
}
private Object readTypeToken(TypeToken<?> token, HttpInputMessage inputMessage) throws IOException {
Reader json = new InputStreamReader(inputMessage.getBody(), getCharset(inputMessage.getHeaders()));
try {
return this.gson.fromJson(json, token.getType());
protected void writeInternal(Object o, Type type, Writer writer) throws Exception {
if (type != null) {
getGson().toJson(o, type, writer);
}
catch (JsonParseException ex) {
throw new HttpMessageNotReadableException("Could not read JSON: " + ex.getMessage(), ex);
}
}
private Charset getCharset(HttpHeaders headers) {
if (headers == null || headers.getContentType() == null || headers.getContentType().getCharset() == null) {
return DEFAULT_CHARSET;
}
return headers.getContentType().getCharset();
}
@Override
protected void writeInternal(Object o, Type type, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
Charset charset = getCharset(outputMessage.getHeaders());
OutputStreamWriter writer = new OutputStreamWriter(outputMessage.getBody(), charset);
try {
if (this.jsonPrefix != null) {
writer.append(this.jsonPrefix);
}
if (type != null) {
this.gson.toJson(o, type, writer);
}
else {
this.gson.toJson(o, writer);
}
writer.close();
}
catch (JsonIOException ex) {
throw new HttpMessageNotWritableException("Could not write JSON: " + ex.getMessage(), ex);
else {
getGson().toJson(o, writer);
}
}