From e5fdd4cd1de76303842e47646213054d25242f4f Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Mon, 10 Apr 2017 15:36:29 +0200 Subject: [PATCH] Extracted AbstractJsonHttpMessageConverter from GsonHttpMessageConverter Generic type resolution algorithm in GenericTypeResolver shared between Jackson and Gson. Issue: SPR-15381 --- .../core/GenericTypeResolver.java | 245 +++++------------- .../core/BridgeMethodResolverTests.java | 32 +-- .../core/GenericTypeResolverTests.java | 66 +---- .../http/codec/json/Jackson2CodecSupport.java | 97 +------ .../http/codec/json/Jackson2JsonDecoder.java | 14 +- .../http/codec/json/Jackson2JsonEncoder.java | 9 +- .../AbstractGenericHttpMessageConverter.java | 14 +- .../ResourceRegionHttpMessageConverter.java | 52 ++-- .../AbstractJackson2HttpMessageConverter.java | 93 +------ .../AbstractJsonHttpMessageConverter.java | 161 ++++++++++++ .../json/GsonHttpMessageConverter.java | 167 +++--------- .../json/GsonHttpMessageConverterTests.java | 46 ++-- 12 files changed, 337 insertions(+), 659 deletions(-) create mode 100644 spring-web/src/main/java/org/springframework/http/converter/json/AbstractJsonHttpMessageConverter.java diff --git a/spring-core/src/main/java/org/springframework/core/GenericTypeResolver.java b/spring-core/src/main/java/org/springframework/core/GenericTypeResolver.java index a66d732c0f..7bb5961727 100644 --- a/spring-core/src/main/java/org/springframework/core/GenericTypeResolver.java +++ b/spring-core/src/main/java/org/springframework/core/GenericTypeResolver.java @@ -21,12 +21,8 @@ import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.lang.reflect.TypeVariable; import java.lang.reflect.WildcardType; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; import org.springframework.util.Assert; -import org.springframework.util.ConcurrentReferenceHashMap; /** * Helper class for resolving generic types against type variables. @@ -42,24 +38,6 @@ import org.springframework.util.ConcurrentReferenceHashMap; */ public abstract class GenericTypeResolver { - /** Cache from Class to TypeVariable Map */ - @SuppressWarnings("rawtypes") - private static final Map, Map> typeVariableCache = - new ConcurrentReferenceHashMap<>(); - - - /** - * Determine the target type for the given parameter specification. - * @param methodParameter the method parameter specification - * @return the corresponding generic parameter type - * @deprecated as of Spring 4.0, use {@link MethodParameter#getGenericParameterType()} - */ - @Deprecated - public static Type getTargetType(MethodParameter methodParameter) { - Assert.notNull(methodParameter, "MethodParameter must not be null"); - return methodParameter.getGenericParameterType(); - } - /** * Determine the target type for the given generic parameter type. * @param methodParameter the method parameter specification @@ -80,7 +58,6 @@ public abstract class GenericTypeResolver { * @param method the method to introspect * @param clazz the class to resolve type variables against * @return the corresponding generic parameter or return type - * @see #resolveReturnTypeForGenericMethod */ public static Class resolveReturnType(Method method, Class clazz) { Assert.notNull(method, "Method must not be null"); @@ -88,106 +65,6 @@ public abstract class GenericTypeResolver { return ResolvableType.forMethodReturnType(method, clazz).resolve(method.getReturnType()); } - /** - * Determine the target type for the generic return type of the given - * generic method, where formal type variables are declared on - * the given method itself. - *

For example, given a factory method with the following signature, - * if {@code resolveReturnTypeForGenericMethod()} is invoked with the reflected - * method for {@code creatProxy()} and an {@code Object[]} array containing - * {@code MyService.class}, {@code resolveReturnTypeForGenericMethod()} will - * infer that the target return type is {@code MyService}. - *

{@code public static  T createProxy(Class clazz)}
- *

Possible Return Values

- *
    - *
  • the target return type, if it can be inferred
  • - *
  • the {@linkplain Method#getReturnType() standard return type}, if - * the given {@code method} does not declare any {@linkplain - * Method#getTypeParameters() formal type variables}
  • - *
  • the {@linkplain Method#getReturnType() standard return type}, if the - * target return type cannot be inferred (e.g., due to type erasure)
  • - *
  • {@code null}, if the length of the given arguments array is shorter - * than the length of the {@linkplain - * Method#getGenericParameterTypes() formal argument list} for the given - * method
  • - *
- * @param method the method to introspect, never {@code null} - * @param args the arguments that will be supplied to the method when it is - * invoked (never {@code null}) - * @param classLoader the ClassLoader to resolve class names against, if necessary - * (may be {@code null}) - * @return the resolved target return type, the standard return type, or {@code null} - * @since 3.2.5 - * @see #resolveReturnType - */ - public static Class resolveReturnTypeForGenericMethod(Method method, Object[] args, ClassLoader classLoader) { - Assert.notNull(method, "Method must not be null"); - Assert.notNull(args, "Argument array must not be null"); - - TypeVariable[] declaredTypeVariables = method.getTypeParameters(); - Type genericReturnType = method.getGenericReturnType(); - Type[] methodArgumentTypes = method.getGenericParameterTypes(); - - // No declared type variables to inspect, so just return the standard return type. - if (declaredTypeVariables.length == 0) { - return method.getReturnType(); - } - - // The supplied argument list is too short for the method's signature, so - // return null, since such a method invocation would fail. - if (args.length < methodArgumentTypes.length) { - return null; - } - - // Ensure that the type variable (e.g., T) is declared directly on the method - // itself (e.g., via ), not on the enclosing class or interface. - boolean locallyDeclaredTypeVariableMatchesReturnType = false; - for (TypeVariable currentTypeVariable : declaredTypeVariables) { - if (currentTypeVariable.equals(genericReturnType)) { - locallyDeclaredTypeVariableMatchesReturnType = true; - break; - } - } - - if (locallyDeclaredTypeVariableMatchesReturnType) { - for (int i = 0; i < methodArgumentTypes.length; i++) { - Type currentMethodArgumentType = methodArgumentTypes[i]; - if (currentMethodArgumentType.equals(genericReturnType)) { - return args[i].getClass(); - } - if (currentMethodArgumentType instanceof ParameterizedType) { - ParameterizedType parameterizedType = (ParameterizedType) currentMethodArgumentType; - Type[] actualTypeArguments = parameterizedType.getActualTypeArguments(); - for (Type typeArg : actualTypeArguments) { - if (typeArg.equals(genericReturnType)) { - Object arg = args[i]; - if (arg instanceof Class) { - return (Class) arg; - } - else if (arg instanceof String && classLoader != null) { - try { - return classLoader.loadClass((String) arg); - } - catch (ClassNotFoundException ex) { - throw new IllegalStateException( - "Could not resolve specific class name argument [" + arg + "]", ex); - } - } - else { - // Consider adding logic to determine the class of the typeArg, if possible. - // For now, just fall back... - return method.getReturnType(); - } - } - } - } - } - } - - // Fall back... - return method.getReturnType(); - } - /** * Resolve the single type argument of the given generic interface against the given * target method which is assumed to return the given interface or an implementation @@ -248,81 +125,75 @@ public abstract class GenericTypeResolver { } /** - * Resolve the specified generic type against the given TypeVariable map. - * @param genericType the generic type to resolve - * @param map the TypeVariable Map to resolved against - * @return the type if it resolves to a Class, or {@code Object.class} otherwise - * @deprecated as of Spring 4.0 in favor of {@link ResolvableType} + * Resolve the given generic type against the given context class, + * substituting type variables as far as possible. + * @param genericType the (potentially) generic type + * @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 resolved type (possibly the given generic type as-is) + * @since 5.0 */ - @Deprecated - @SuppressWarnings("rawtypes") - public static Class resolveType(Type genericType, Map map) { - return ResolvableType.forType(genericType, new TypeVariableMapVariableResolver(map)).resolve(Object.class); - } - - /** - * Build a mapping of {@link TypeVariable#getName TypeVariable names} to - * {@link Class concrete classes} for the specified {@link Class}. Searches - * all super types, enclosing types and interfaces. - * @deprecated as of Spring 4.0 in favor of {@link ResolvableType} - */ - @Deprecated - @SuppressWarnings("rawtypes") - public static Map getTypeVariableMap(Class clazz) { - Map typeVariableMap = typeVariableCache.get(clazz); - if (typeVariableMap == null) { - typeVariableMap = new HashMap<>(); - buildTypeVariableMap(ResolvableType.forClass(clazz), typeVariableMap); - typeVariableCache.put(clazz, Collections.unmodifiableMap(typeVariableMap)); - } - return typeVariableMap; - } - - @SuppressWarnings("rawtypes") - private static void buildTypeVariableMap(ResolvableType type, Map typeVariableMap) { - if (type != ResolvableType.NONE) { - if (type.getType() instanceof ParameterizedType) { - TypeVariable[] variables = type.resolve().getTypeParameters(); - for (int i = 0; i < variables.length; i++) { - ResolvableType generic = type.getGeneric(i); - while (generic.getType() instanceof TypeVariable) { - generic = generic.resolveType(); - } - if (generic != ResolvableType.NONE) { - typeVariableMap.put(variables[i], generic.getType()); - } + public static Type resolveType(Type genericType, Class contextClass) { + if (contextClass != null) { + if (genericType instanceof TypeVariable) { + ResolvableType resolvedTypeVariable = resolveVariable( + (TypeVariable) genericType, ResolvableType.forClass(contextClass)); + if (resolvedTypeVariable != ResolvableType.NONE) { + return resolvedTypeVariable.resolve(); } } - buildTypeVariableMap(type.getSuperType(), typeVariableMap); - for (ResolvableType interfaceType : type.getInterfaces()) { - buildTypeVariableMap(interfaceType, typeVariableMap); - } - if (type.resolve().isMemberClass()) { - buildTypeVariableMap(ResolvableType.forClass(type.resolve().getEnclosingClass()), typeVariableMap); + else if (genericType instanceof ParameterizedType) { + ResolvableType resolvedType = ResolvableType.forType(genericType); + if (resolvedType.hasUnresolvableGenerics()) { + ParameterizedType parameterizedType = (ParameterizedType) genericType; + 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 ResolvableType.forClassWithGenerics(resolvedType.getRawClass(), generics).getType(); + } } } + return genericType; } - - @SuppressWarnings({"serial", "rawtypes"}) - private static class TypeVariableMapVariableResolver implements ResolvableType.VariableResolver { - - private final Map typeVariableMap; - - public TypeVariableMapVariableResolver(Map typeVariableMap) { - this.typeVariableMap = typeVariableMap; + private static ResolvableType resolveVariable(TypeVariable typeVariable, ResolvableType contextType) { + ResolvableType resolvedType; + if (contextType.hasGenerics()) { + resolvedType = ResolvableType.forType(typeVariable, contextType); + if (resolvedType.resolve() != null) { + return resolvedType; + } } - @Override - public ResolvableType resolveVariable(TypeVariable variable) { - Type type = this.typeVariableMap.get(variable); - return (type != null ? ResolvableType.forType(type) : null); + ResolvableType superType = contextType.getSuperType(); + if (superType != ResolvableType.NONE) { + resolvedType = resolveVariable(typeVariable, superType); + if (resolvedType.resolve() != null) { + return resolvedType; + } } - - @Override - public Object getSource() { - return this.typeVariableMap; + for (ResolvableType ifc : contextType.getInterfaces()) { + resolvedType = resolveVariable(typeVariable, ifc); + if (resolvedType.resolve() != null) { + return resolvedType; + } } + return ResolvableType.NONE; } } diff --git a/spring-core/src/test/java/org/springframework/core/BridgeMethodResolverTests.java b/spring-core/src/test/java/org/springframework/core/BridgeMethodResolverTests.java index 5be62b9bab..da0cbab1a8 100644 --- a/spring-core/src/test/java/org/springframework/core/BridgeMethodResolverTests.java +++ b/spring-core/src/test/java/org/springframework/core/BridgeMethodResolverTests.java @@ -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. @@ -18,8 +18,6 @@ package org.springframework.core; import java.io.Serializable; import java.lang.reflect.Method; -import java.lang.reflect.ParameterizedType; -import java.lang.reflect.Type; import java.lang.reflect.TypeVariable; import java.util.Collection; import java.util.Date; @@ -109,26 +107,6 @@ public class BridgeMethodResolverTests { assertFalse("Should not be bridge method", BridgeMethodResolver.isBridgeMethodFor(bridge, other, MyBar.class)); } - @Test - @Deprecated - public void testCreateTypeVariableMap() throws Exception { - Map typeVariableMap = GenericTypeResolver.getTypeVariableMap(MyBar.class); - TypeVariable barT = findTypeVariable(InterBar.class, "T"); - assertEquals(String.class, typeVariableMap.get(barT)); - - typeVariableMap = GenericTypeResolver.getTypeVariableMap(MyFoo.class); - TypeVariable fooT = findTypeVariable(Foo.class, "T"); - assertEquals(String.class, typeVariableMap.get(fooT)); - - typeVariableMap = GenericTypeResolver.getTypeVariableMap(ExtendsEnclosing.ExtendsEnclosed.ExtendsReallyDeepNow.class); - TypeVariable r = findTypeVariable(Enclosing.Enclosed.ReallyDeepNow.class, "R"); - TypeVariable s = findTypeVariable(Enclosing.Enclosed.class, "S"); - TypeVariable t = findTypeVariable(Enclosing.class, "T"); - assertEquals(Long.class, typeVariableMap.get(r)); - assertEquals(Integer.class, typeVariableMap.get(s)); - assertEquals(String.class, typeVariableMap.get(t)); - } - @Test public void testDoubleParameterization() throws Exception { Method objectBridge = MyBoo.class.getDeclaredMethod("foo", Object.class); @@ -228,14 +206,6 @@ public class BridgeMethodResolverTests { assertEquals(bridgedMethod, BridgeMethodResolver.findBridgedMethod(bridgeMethod)); } - @Test - @Deprecated - public void testSPR2454() throws Exception { - Map typeVariableMap = GenericTypeResolver.getTypeVariableMap(YourHomer.class); - TypeVariable variable = findTypeVariable(MyHomer.class, "L"); - assertEquals(AbstractBounded.class, ((ParameterizedType) typeVariableMap.get(variable)).getRawType()); - } - @Test public void testSPR2603() throws Exception { Method objectBridge = YourHomer.class.getDeclaredMethod("foo", Bounded.class); diff --git a/spring-core/src/test/java/org/springframework/core/GenericTypeResolverTests.java b/spring-core/src/test/java/org/springframework/core/GenericTypeResolverTests.java index 364bffb623..7d18f6b9a4 100644 --- a/spring-core/src/test/java/org/springframework/core/GenericTypeResolverTests.java +++ b/spring-core/src/test/java/org/springframework/core/GenericTypeResolverTests.java @@ -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,11 +17,7 @@ package org.springframework.core; import java.io.Serializable; -import java.lang.reflect.Method; -import java.lang.reflect.Type; -import java.lang.reflect.TypeVariable; import java.util.Collection; -import java.util.HashMap; import java.util.List; import java.util.Map; @@ -76,71 +72,11 @@ public class GenericTypeResolverTests { resolveReturnTypeArgument(findMethod(MyTypeWithMethods.class, "object"), MyInterfaceType.class)); } - @Test - @Deprecated - public void testResolveType() { - Method intMessageMethod = findMethod(MyTypeWithMethods.class, "readIntegerInputMessage", MyInterfaceType.class); - MethodParameter intMessageMethodParam = new MethodParameter(intMessageMethod, 0); - assertEquals(MyInterfaceType.class, - resolveType(intMessageMethodParam.getGenericParameterType(), new HashMap<>())); - - Method intArrMessageMethod = findMethod(MyTypeWithMethods.class, "readIntegerArrayInputMessage", - MyInterfaceType[].class); - MethodParameter intArrMessageMethodParam = new MethodParameter(intArrMessageMethod, 0); - assertEquals(MyInterfaceType[].class, - resolveType(intArrMessageMethodParam.getGenericParameterType(), new HashMap<>())); - - Method genericArrMessageMethod = findMethod(MySimpleTypeWithMethods.class, "readGenericArrayInputMessage", - Object[].class); - MethodParameter genericArrMessageMethodParam = new MethodParameter(genericArrMessageMethod, 0); - Map varMap = getTypeVariableMap(MySimpleTypeWithMethods.class); - assertEquals(Integer[].class, resolveType(genericArrMessageMethodParam.getGenericParameterType(), varMap)); - } - @Test public void testBoundParameterizedType() { assertEquals(B.class, resolveTypeArgument(TestImpl.class, TestIfc.class)); } - @Test - @Deprecated - public void testGetTypeVariableMap() throws Exception { - Map map; - - map = GenericTypeResolver.getTypeVariableMap(MySimpleInterfaceType.class); - assertThat(map.toString(), equalTo("{T=class java.lang.String}")); - - map = GenericTypeResolver.getTypeVariableMap(MyCollectionInterfaceType.class); - assertThat(map.toString(), equalTo("{T=java.util.Collection}")); - - map = GenericTypeResolver.getTypeVariableMap(MyCollectionSuperclassType.class); - assertThat(map.toString(), equalTo("{T=java.util.Collection}")); - - map = GenericTypeResolver.getTypeVariableMap(MySimpleTypeWithMethods.class); - assertThat(map.toString(), equalTo("{T=class java.lang.Integer}")); - - map = GenericTypeResolver.getTypeVariableMap(TopLevelClass.class); - assertThat(map.toString(), equalTo("{}")); - - map = GenericTypeResolver.getTypeVariableMap(TypedTopLevelClass.class); - assertThat(map.toString(), equalTo("{T=class java.lang.Integer}")); - - map = GenericTypeResolver.getTypeVariableMap(TypedTopLevelClass.TypedNested.class); - assertThat(map.size(), equalTo(2)); - Type t = null; - Type x = null; - for (Map.Entry entry : map.entrySet()) { - if (entry.getKey().toString().equals("T")) { - t = entry.getValue(); - } - else { - x = entry.getValue(); - } - } - assertThat(t, equalTo((Type) Integer.class)); - assertThat(x, equalTo((Type) Long.class)); - } - @Test public void getGenericsCannotBeResolved() throws Exception { // SPR-11030 diff --git a/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2CodecSupport.java b/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2CodecSupport.java index a8db4a930d..5bd836bb56 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2CodecSupport.java +++ b/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2CodecSupport.java @@ -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. - *

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: - *

-	 * 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);
-	 *   }
-	 * }
-	 * 
- * @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 getHints(ResolvableType actualType) { @@ -169,7 +94,7 @@ public abstract class Jackson2CodecSupport { } protected Optional getParameter(ResolvableType type) { - return Optional.ofNullable (type.getSource() instanceof MethodParameter ? + return Optional.ofNullable(type.getSource() instanceof MethodParameter ? (MethodParameter) type.getSource() : null); } diff --git a/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2JsonDecoder.java b/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2JsonDecoder.java index 9beda0e8a7..df3220195a 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2JsonDecoder.java +++ b/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2JsonDecoder.java @@ -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 -> { diff --git a/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2JsonEncoder.java b/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2JsonEncoder.java index 0675ca09e2..467d773ca0 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2JsonEncoder.java +++ b/spring-web/src/main/java/org/springframework/http/codec/json/Jackson2JsonEncoder.java @@ -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 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); diff --git a/spring-web/src/main/java/org/springframework/http/converter/AbstractGenericHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/AbstractGenericHttpMessageConverter.java index a52b032aca..ebe813e317 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/AbstractGenericHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/AbstractGenericHttpMessageConverter.java @@ -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 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 extends AbstractHtt } } - @Override protected void writeInternal(T t, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException { @@ -113,7 +117,7 @@ public abstract class AbstractGenericHttpMessageConverter 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 diff --git a/spring-web/src/main/java/org/springframework/http/converter/ResourceRegionHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/ResourceRegionHttpMessageConverter.java index 4e76c2a8ca..32eb4d3a7b 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/ResourceRegionHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/ResourceRegionHttpMessageConverter.java @@ -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'); diff --git a/spring-web/src/main/java/org/springframework/http/converter/json/AbstractJackson2HttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/json/AbstractJackson2HttpMessageConverter.java index a2dbcbda1a..2a85fb4e16 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/json/AbstractJackson2HttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/json/AbstractJackson2HttpMessageConverter.java @@ -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. * - *

Compatible with Jackson 2.6 and higher, as of Spring 4.3. + *

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. - *

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: - *

-	 * 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);
-	 *   }
-	 * }
-	 * 
* @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)); } /** diff --git a/spring-web/src/main/java/org/springframework/http/converter/json/AbstractJsonHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/json/AbstractJsonHttpMessageConverter.java new file mode 100644 index 0000000000..0e28fc8775 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/converter/json/AbstractJsonHttpMessageConverter.java @@ -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. + * + *

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 { + + 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}. + *

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); + } + +} diff --git a/spring-web/src/main/java/org/springframework/http/converter/json/GsonHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/json/GsonHttpMessageConverter.java index af688fb35b..c544795ba3 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/json/GsonHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/json/GsonHttpMessageConverter.java @@ -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 - * Google Gson library's - * {@link Gson} class. + * Google Gson library. * *

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; *

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 { +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. *

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}. - *

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. - *

The default implementation returns {@code TypeToken.get(type)}, but - * this can be overridden in subclasses to allow for custom generic - * collection handling. For instance: - *

-	 * protected TypeToken getTypeToken(Type type) {
-	 *   if (type instanceof Class && List.class.isAssignableFrom((Class) type)) {
-	 *     return new TypeToken>() {};
-	 *   }
-	 *   else {
-	 *     return super.getTypeToken(type);
-	 *   }
-	 * }
-	 * 
- * @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); } } diff --git a/spring-web/src/test/java/org/springframework/http/converter/json/GsonHttpMessageConverterTests.java b/spring-web/src/test/java/org/springframework/http/converter/json/GsonHttpMessageConverterTests.java index fddc74f5ca..7b2723891f 100644 --- a/spring-web/src/test/java/org/springframework/http/converter/json/GsonHttpMessageConverterTests.java +++ b/spring-web/src/test/java/org/springframework/http/converter/json/GsonHttpMessageConverterTests.java @@ -17,7 +17,7 @@ package org.springframework.http.converter.json; import java.io.IOException; -import java.lang.reflect.Type; +import java.lang.reflect.Field; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.ArrayList; @@ -25,7 +25,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import com.google.gson.reflect.TypeToken; import org.junit.Test; import org.springframework.core.ParameterizedTypeReference; @@ -75,9 +74,9 @@ public class GsonHttpMessageConverterTests { assertEquals("Foo", result.getString()); assertEquals(42, result.getNumber()); assertEquals(42F, result.getFraction(), 0F); - assertArrayEquals(new String[]{"Foo", "Bar"}, result.getArray()); + assertArrayEquals(new String[] {"Foo", "Bar"}, result.getArray()); assertTrue(result.isBool()); - assertArrayEquals(new byte[]{0x1, 0x2}, result.getBytes()); + assertArrayEquals(new byte[] {0x1, 0x2}, result.getBytes()); } @Test @@ -103,7 +102,7 @@ public class GsonHttpMessageConverterTests { for (int i = 0; i < 2; i++) { bytes[i] = resultBytes.get(i).byteValue(); } - assertArrayEquals(new byte[]{0x1, 0x2}, bytes); + assertArrayEquals(new byte[] {0x1, 0x2}, bytes); } @Test @@ -113,9 +112,9 @@ public class GsonHttpMessageConverterTests { body.setString("Foo"); body.setNumber(42); body.setFraction(42F); - body.setArray(new String[]{"Foo", "Bar"}); + body.setArray(new String[] {"Foo", "Bar"}); body.setBool(true); - body.setBytes(new byte[]{0x1, 0x2}); + body.setBytes(new byte[] {0x1, 0x2}); this.converter.write(body, null, outputMessage); Charset utf8 = StandardCharsets.UTF_8; String result = outputMessage.getBodyAsString(utf8); @@ -149,25 +148,15 @@ public class GsonHttpMessageConverterTests { @Test @SuppressWarnings("unchecked") - public void readGenerics() throws IOException { - GsonHttpMessageConverter converter = new GsonHttpMessageConverter() { - @Override - protected TypeToken getTypeToken(Type type) { - if (type instanceof Class && List.class.isAssignableFrom((Class) type)) { - return new TypeToken>() { - }; - } - else { - return super.getTypeToken(type); - } - } - }; + public void readGenerics() throws Exception { + Field beansList = ListHolder.class.getField("listField"); + String body = "[{\"bytes\":[1,2],\"array\":[\"Foo\",\"Bar\"]," + "\"number\":42,\"string\":\"Foo\",\"bool\":true,\"fraction\":42.0}]"; MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes(StandardCharsets.UTF_8)); inputMessage.getHeaders().setContentType(new MediaType("application", "json")); - List results = (List) converter.read(List.class, inputMessage); + List results = (List) converter.read(beansList.getGenericType(), MyBeanListHolder.class, inputMessage); assertEquals(1, results.size()); MyBean result = results.get(0); assertEquals("Foo", result.getString()); @@ -180,7 +169,7 @@ public class GsonHttpMessageConverterTests { @Test @SuppressWarnings("unchecked") - public void readParameterizedType() throws IOException { + public void readParameterizedType() throws Exception { ParameterizedTypeReference> beansList = new ParameterizedTypeReference>() { }; @@ -189,7 +178,6 @@ public class GsonHttpMessageConverterTests { MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes(StandardCharsets.UTF_8)); inputMessage.getHeaders().setContentType(new MediaType("application", "json")); - GsonHttpMessageConverter converter = new GsonHttpMessageConverter(); List results = (List) converter.read(beansList.getType(), null, inputMessage); assertEquals(1, results.size()); MyBean result = results.get(0); @@ -198,7 +186,7 @@ public class GsonHttpMessageConverterTests { assertEquals(42F, result.getFraction(), 0F); assertArrayEquals(new String[] { "Foo", "Bar" }, result.getArray()); assertTrue(result.isBool()); - assertArrayEquals(new byte[] { 0x1, 0x2 }, result.getBytes()); + assertArrayEquals(new byte[] {0x1, 0x2}, result.getBytes()); } @Test @@ -281,4 +269,14 @@ public class GsonHttpMessageConverterTests { } } + + public static class ListHolder { + + public List listField; + } + + + public static class MyBeanListHolder extends ListHolder { + } + }