From 975398d30cc3fb7a48d1024b02c33f96ccbdd20c Mon Sep 17 00:00:00 2001 From: Dave Syer Date: Fri, 23 Feb 2018 09:46:42 +0000 Subject: [PATCH] Use Gson instead of Jackson by default --- .gitignore | 1 + spring-cloud-function-web/pom.xml | 10 + .../flux/BetterGsonHttpMessageConverter.java | 208 ++++++++++++++++++ .../web/flux/ReactorAutoConfiguration.java | 27 ++- .../FluxHandlerMethodArgumentResolver.java | 24 +- 5 files changed, 257 insertions(+), 13 deletions(-) create mode 100644 spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/flux/BetterGsonHttpMessageConverter.java diff --git a/.gitignore b/.gitignore index 2aa48a83b..617452602 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ target/ build/ bin/ .sts4-cache/ +.attach_pid* .m2/ .gradle/ _site/ diff --git a/spring-cloud-function-web/pom.xml b/spring-cloud-function-web/pom.xml index 259d217cb..f4adcbc42 100644 --- a/spring-cloud-function-web/pom.xml +++ b/spring-cloud-function-web/pom.xml @@ -18,6 +18,16 @@ org.springframework.boot spring-boot-starter-web + + + com.fasterxml.jackson.core + jackson-databind + + + + + com.google.code.gson + gson org.springframework.cloud diff --git a/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/flux/BetterGsonHttpMessageConverter.java b/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/flux/BetterGsonHttpMessageConverter.java new file mode 100644 index 000000000..ca09c4550 --- /dev/null +++ b/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/flux/BetterGsonHttpMessageConverter.java @@ -0,0 +1,208 @@ +/* + * 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.cloud.function.web.flux; + +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.io.Reader; +import java.lang.reflect.Type; +import java.nio.charset.Charset; + +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. + * + *

+ * 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 + * {@code UTF-8} character set. + * + *

+ * Tested against Gson 2.6; compatible with Gson 2.0 and higher. + * + * @author Roy Clarkson + * @since 4.1 + * @see #setGson + * @see #setSupportedMediaTypes + */ +public class BetterGsonHttpMessageConverter + extends AbstractGenericHttpMessageConverter { + + public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8"); + + private Gson gson = new Gson(); + + private String jsonPrefix; + + /** + * Construct a new {@code GsonHttpMessageConverter}. + */ + public BetterGsonHttpMessageConverter() { + super(MediaType.APPLICATION_JSON, new MediaType("application", "*+json")); + this.setDefaultCharset(DEFAULT_CHARSET); + } + + /** + * Set the {@code Gson} instance to use. If not set, a default {@link Gson#Gson() + * Gson} instance is used. + *

+ * Setting a custom-configured {@code Gson} is one way to take further control of the + * JSON serialization process. + */ + public void setGson(Gson gson) { + Assert.notNull(gson, "'gson' is required"); + this.gson = gson; + } + + /** + * Return the configured {@code Gson} instance for this converter. + */ + public Gson getGson() { + 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 + @SuppressWarnings("deprecation") + public Object read(Type type, Class contextClass, HttpInputMessage inputMessage) + throws IOException, HttpMessageNotReadableException { + + TypeToken token = getTypeToken(type); + return readTypeToken(token, inputMessage); + } + + @Override + @SuppressWarnings("deprecation") + protected Object readInternal(Class clazz, HttpInputMessage inputMessage) + throws IOException, HttpMessageNotReadableException { + + TypeToken token = getTypeToken(clazz); + 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 + * @deprecated as of Spring Framework 4.3.8, in favor of signature-based resolution + */ + @Deprecated + 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()); + } + catch (JsonParseException ex) { + throw new HttpMessageNotReadableException( + "JSON parse error: " + 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.flush(); + } + catch (JsonIOException ex) { + throw new HttpMessageNotWritableException( + "Could not write JSON: " + ex.getMessage(), ex); + } + } + +} diff --git a/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/flux/ReactorAutoConfiguration.java b/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/flux/ReactorAutoConfiguration.java index 268e92a56..989e096d1 100644 --- a/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/flux/ReactorAutoConfiguration.java +++ b/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/flux/ReactorAutoConfiguration.java @@ -19,14 +19,16 @@ package org.springframework.cloud.function.web.flux; import java.util.ArrayList; import java.util.List; -import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.gson.Gson; import org.springframework.beans.factory.SmartInitializingSingleton; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.AutoConfigureBefore; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.boot.autoconfigure.web.HttpMessageConverters; +import org.springframework.boot.autoconfigure.web.HttpMessageConvertersAutoConfiguration; import org.springframework.cloud.function.context.catalog.FunctionInspector; import org.springframework.cloud.function.core.FunctionCatalog; import org.springframework.cloud.function.web.flux.request.FluxHandlerMethodArgumentResolver; @@ -34,6 +36,8 @@ import org.springframework.cloud.function.web.flux.response.FluxReturnValueHandl import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.json.GsonHttpMessageConverter; import org.springframework.util.ClassUtils; import org.springframework.web.method.support.AsyncHandlerMethodReturnValueHandler; import org.springframework.web.method.support.HandlerMethodArgumentResolver; @@ -49,6 +53,7 @@ import reactor.core.publisher.Flux; @Configuration @ConditionalOnWebApplication @ConditionalOnClass({ Flux.class, AsyncHandlerMethodReturnValueHandler.class }) +@AutoConfigureBefore(HttpMessageConvertersAutoConfiguration.class) public class ReactorAutoConfiguration { @Autowired @@ -60,6 +65,24 @@ public class ReactorAutoConfiguration { return new FunctionHandlerMapping(catalog, inspector); } + // TODO: remove this when https://jira.spring.io/browse/SPR-16529 is resolved + @Bean + public HttpMessageConverters httpMessageConverters(Gson gson) { + List> converters = new ArrayList<>(); + for (HttpMessageConverter converter : new HttpMessageConverters() + .getConverters()) { + if (converter instanceof GsonHttpMessageConverter) { + BetterGsonHttpMessageConverter gsonConverter = new BetterGsonHttpMessageConverter(); + gsonConverter.setGson(gson); + converters.add(gsonConverter); + } + else { + converters.add(converter); + } + } + return new HttpMessageConverters(false, converters); + } + @Configuration @ConditionalOnMissingClass("org.springframework.core.ReactiveAdapter") protected static class FluxReturnValueConfiguration { @@ -74,7 +97,7 @@ public class ReactorAutoConfiguration { protected static class FluxArgumentResolverConfiguration { @Bean public FluxHandlerMethodArgumentResolver fluxHandlerMethodArgumentResolver( - FunctionInspector inspector, ObjectMapper mapper) { + FunctionInspector inspector, Gson mapper) { return new FluxHandlerMethodArgumentResolver(inspector, mapper); } } diff --git a/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/flux/request/FluxHandlerMethodArgumentResolver.java b/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/flux/request/FluxHandlerMethodArgumentResolver.java index dbe459f3b..b7c7e1e7c 100644 --- a/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/flux/request/FluxHandlerMethodArgumentResolver.java +++ b/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/flux/request/FluxHandlerMethodArgumentResolver.java @@ -16,6 +16,7 @@ package org.springframework.cloud.function.web.flux.request; +import java.io.InputStreamReader; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Arrays; @@ -23,8 +24,8 @@ import java.util.List; import javax.servlet.http.HttpServletRequest; -import com.fasterxml.jackson.databind.JsonMappingException; -import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.gson.Gson; +import com.google.gson.JsonSyntaxException; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -35,6 +36,7 @@ import org.springframework.cloud.function.web.flux.constants.WebRequestConstants import org.springframework.cloud.function.web.util.HeaderUtils; import org.springframework.core.MethodParameter; import org.springframework.core.Ordered; +import org.springframework.core.ResolvableType; import org.springframework.http.MediaType; import org.springframework.http.server.ServletServerHttpRequest; import org.springframework.messaging.MessageHeaders; @@ -57,12 +59,11 @@ public class FluxHandlerMethodArgumentResolver private static Log logger = LogFactory .getLog(FluxHandlerMethodArgumentResolver.class); - private final ObjectMapper mapper; + private final Gson mapper; private FunctionInspector inspector; - public FluxHandlerMethodArgumentResolver(FunctionInspector inspector, - ObjectMapper mapper) { + public FluxHandlerMethodArgumentResolver(FunctionInspector inspector, Gson mapper) { this.inspector = inspector; this.mapper = mapper; } @@ -95,14 +96,15 @@ public class FluxHandlerMethodArgumentResolver } else { try { - body = mapper.readValue(nativeRequest.getInputStream(), - mapper.getTypeFactory() - .constructCollectionLikeType(ArrayList.class, type)); + body = mapper.fromJson( + new InputStreamReader(nativeRequest.getInputStream()), + ResolvableType.forClassWithGenerics(ArrayList.class, type) + .getType()); } - catch (JsonMappingException e) { + catch (JsonSyntaxException e) { nativeRequest.setAttribute(WebRequestConstants.INPUT_SINGLE, true); - body = Arrays.asList( - mapper.readValue(nativeRequest.getContentAsByteArray(), type)); + body = Arrays.asList(mapper.fromJson( + new String(nativeRequest.getContentAsByteArray()), type)); } } if (message) {