From 366d05050f7ae833f367f8a33d2290a26325c22d Mon Sep 17 00:00:00 2001 From: Oleg Zhurakousky Date: Wed, 1 Apr 2020 16:07:42 +0200 Subject: [PATCH] GH-476 Add generic JsonMessageConverter compatible with Jackson and Gson Added implementation of JsonMessageConverter which is initialized with JsonMapper so it can delegate to Jackson or Gson based on property setting and/or availability of the underlying library. Resolves #476 --- ...ntextFunctionCatalogAutoConfiguration.java | 48 +++-------- .../context/config/JsonMessageConverter.java | 84 +++++++++++++++++++ .../cloud/function/json/GsonMapper.java | 29 ++++++- .../cloud/function/json/JacksonMapper.java | 37 +++++++- .../cloud/function/json/JsonMapper.java | 7 ++ 5 files changed, 165 insertions(+), 40 deletions(-) create mode 100644 spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/config/JsonMessageConverter.java diff --git a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/config/ContextFunctionCatalogAutoConfiguration.java b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/config/ContextFunctionCatalogAutoConfiguration.java index 3139d6d58..ed06b6d48 100644 --- a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/config/ContextFunctionCatalogAutoConfiguration.java +++ b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/config/ContextFunctionCatalogAutoConfiguration.java @@ -27,8 +27,6 @@ import java.util.stream.Collectors; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.gson.Gson; -import org.springframework.boot.autoconfigure.condition.AnyNestedCondition; -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; @@ -40,11 +38,11 @@ import org.springframework.cloud.function.context.catalog.BeanFactoryAwareFuncti import org.springframework.cloud.function.context.catalog.FunctionInspector; import org.springframework.cloud.function.json.GsonMapper; import org.springframework.cloud.function.json.JacksonMapper; +import org.springframework.cloud.function.json.JsonMapper; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.ComponentScan.Filter; -import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.FilterType; import org.springframework.core.convert.converter.GenericConverter; @@ -53,7 +51,6 @@ import org.springframework.lang.Nullable; import org.springframework.messaging.converter.AbstractMessageConverter; import org.springframework.messaging.converter.ByteArrayMessageConverter; import org.springframework.messaging.converter.CompositeMessageConverter; -import org.springframework.messaging.converter.MappingJackson2MessageConverter; import org.springframework.messaging.converter.MessageConverter; import org.springframework.messaging.converter.StringMessageConverter; import org.springframework.util.CollectionUtils; @@ -74,8 +71,7 @@ public class ContextFunctionCatalogAutoConfiguration { static final String PREFERRED_MAPPER_PROPERTY = "spring.http.converters.preferred-json-mapper"; @Bean - public FunctionRegistry functionCatalog(List messageConverters, @Nullable ObjectMapper objectMapper, - ConfigurableApplicationContext context) { + public FunctionRegistry functionCatalog(List messageConverters, JsonMapper jsonMapper, ConfigurableApplicationContext context) { ConfigurableConversionService conversionService = (ConfigurableConversionService) context.getBeanFactory().getConversionService(); Map converters = context.getBeansOfType(GenericConverter.class); for (GenericConverter converter : converters.values()) { @@ -104,10 +100,7 @@ public class ContextFunctionCatalogAutoConfiguration { : converter; }) .collect(Collectors.toList()); - - MappingJackson2MessageConverter jsonConverter = new MappingJackson2MessageConverter(); - jsonConverter.setObjectMapper(objectMapper == null ? new ObjectMapper() : objectMapper); - mcList.add(NegotiatingMessageConverterWrapper.wrap(jsonConverter)); + mcList.add(NegotiatingMessageConverterWrapper.wrap(new JsonMessageConverter(jsonMapper))); mcList.add(NegotiatingMessageConverterWrapper.wrap(new ByteArrayMessageConverter())); mcList.add(NegotiatingMessageConverterWrapper.wrap(new StringMessageConverter())); @@ -144,32 +137,16 @@ public class ContextFunctionCatalogAutoConfiguration { } - private static class PreferGsonOrMissingJacksonCondition extends AnyNestedCondition { - - PreferGsonOrMissingJacksonCondition() { - super(ConfigurationPhase.REGISTER_BEAN); - } - - @ConditionalOnProperty(name = PREFERRED_MAPPER_PROPERTY, havingValue = "gson", matchIfMissing = false) - static class GsonPreferred { - - } - - @ConditionalOnMissingBean(ObjectMapper.class) - static class JacksonMissing { - - } - - } - @Configuration(proxyBeanMethods = false) @ConditionalOnClass(Gson.class) - @ConditionalOnBean(Gson.class) - @Conditional(PreferGsonOrMissingJacksonCondition.class) + @ConditionalOnProperty(name = PREFERRED_MAPPER_PROPERTY, havingValue = "gson", matchIfMissing = false) protected static class GsonConfiguration { @Bean - public GsonMapper jsonMapper(Gson gson) { + public GsonMapper jsonMapper(@Nullable Gson gson) { + if (gson == null) { + gson = new Gson(); + } return new GsonMapper(gson); } @@ -177,13 +154,14 @@ public class ContextFunctionCatalogAutoConfiguration { @Configuration(proxyBeanMethods = false) @ConditionalOnClass(ObjectMapper.class) - @ConditionalOnBean(ObjectMapper.class) - @ConditionalOnProperty(name = ContextFunctionCatalogAutoConfiguration.PREFERRED_MAPPER_PROPERTY, // - havingValue = "jackson", matchIfMissing = true) + @ConditionalOnProperty(name = PREFERRED_MAPPER_PROPERTY, havingValue = "jackson", matchIfMissing = true) protected static class JacksonConfiguration { @Bean - public JacksonMapper jsonMapper(ObjectMapper mapper) { + public JacksonMapper jsonMapper(@Nullable ObjectMapper mapper) { + if (mapper == null) { + mapper = new ObjectMapper(); + } return new JacksonMapper(mapper); } diff --git a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/config/JsonMessageConverter.java b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/config/JsonMessageConverter.java new file mode 100644 index 000000000..961b7c521 --- /dev/null +++ b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/config/JsonMessageConverter.java @@ -0,0 +1,84 @@ +/* + * Copyright 2020-2020 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 + * + * https://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.context.config; + +import org.springframework.cloud.function.json.JsonMapper; +import org.springframework.lang.Nullable; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageHeaders; +import org.springframework.messaging.converter.AbstractMessageConverter; +import org.springframework.messaging.converter.MessageConverter; +import org.springframework.util.MimeType; + +/** + * Implementation of {@link MessageConverter} which uses Jackson or Gson libraries to do the + * actual conversion via {@link JsonMapper} instance. + * + * @author Oleg Zhurakousky + * @since 3.0.4 + */ +class JsonMessageConverter extends AbstractMessageConverter { + + private final JsonMapper jsonMapper; + + JsonMessageConverter(JsonMapper jsonMapper) { + this(jsonMapper, new MimeType("application", "json")); + } + + JsonMessageConverter(JsonMapper jsonMapper, MimeType... supportedMimeTypes) { + super(supportedMimeTypes); + this.jsonMapper = jsonMapper; + } + + @Override + protected boolean supports(Class clazz) { + // should not be called, since we override canConvertFrom/canConvertTo instead + throw new UnsupportedOperationException(); + } + + @Override + protected boolean canConvertTo(Object payload, @Nullable MessageHeaders headers) { + if (!supportsMimeType(headers)) { + return false; + } + return true; + } + + @Override + protected boolean canConvertFrom(Message message, @Nullable Class targetClass) { + if (targetClass == null || !supportsMimeType(message.getHeaders())) { + return false; + } + return true; + } + + @Override + protected Object convertFromInternal(Message message, Class targetClass, @Nullable Object conversionHint) { + if (targetClass.isInstance(message.getPayload())) { + return message.getPayload(); + } + Object result = jsonMapper.fromJson(message.getPayload(), targetClass); + return result; + } + + @Override + protected Object convertToInternal(Object payload, @Nullable MessageHeaders headers, + @Nullable Object conversionHint) { + return jsonMapper.toJson(payload); + } + +} diff --git a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/json/GsonMapper.java b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/json/GsonMapper.java index ce0dce087..58a3ce12c 100644 --- a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/json/GsonMapper.java +++ b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/json/GsonMapper.java @@ -16,14 +16,16 @@ package org.springframework.cloud.function.json; +import java.io.Reader; import java.lang.reflect.Type; +import java.nio.charset.StandardCharsets; import com.google.gson.Gson; +import com.google.gson.JsonElement; /** * @author Dave Syer * @author Oleg Zhurakousky - * */ public class GsonMapper implements JsonMapper { @@ -35,7 +37,7 @@ public class GsonMapper implements JsonMapper { @Override public T toObject(String json, Type type) { - return this.gson.fromJson(json, type); + return this.fromJson(json, type); } @Override @@ -43,4 +45,27 @@ public class GsonMapper implements JsonMapper { return this.gson.toJson(value); } + @Override + public T fromJson(Object json, Type type) { + T convertedValue = null; + if (json instanceof byte[]) { + convertedValue = this.gson.fromJson(new String(((byte[]) json), StandardCharsets.UTF_8), type); + } + else if (json instanceof String) { + convertedValue = this.gson.fromJson((String) json, type); + } + else if (json instanceof Reader) { + convertedValue = this.gson.fromJson((Reader) json, type); + } + else if (json instanceof JsonElement) { + convertedValue = this.gson.fromJson((JsonElement) json, type); + } + return convertedValue; + } + + @Override + public byte[] toJson(Object value) { + return this.gson.toJson(value).getBytes(StandardCharsets.UTF_8); + } + } diff --git a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/json/JacksonMapper.java b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/json/JacksonMapper.java index 484cba413..7865e08f6 100644 --- a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/json/JacksonMapper.java +++ b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/json/JacksonMapper.java @@ -16,9 +16,11 @@ package org.springframework.cloud.function.json; +import java.io.Reader; import java.lang.reflect.Type; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.type.TypeFactory; @@ -36,13 +38,40 @@ public class JacksonMapper implements JsonMapper { @Override public T toObject(String json, Type type) { + return this.fromJson(json, type); + } + + @Override + public T fromJson(Object json, Type type) { + T convertedValue = null; + JavaType constructType = TypeFactory.defaultInstance().constructType(type); + try { - return this.mapper.readValue(json, - TypeFactory.defaultInstance().constructType(type)); + if (json instanceof String) { + convertedValue = this.mapper.readValue((String) json, constructType); + } + else if (json instanceof byte[]) { + convertedValue = this.mapper.readValue((byte[]) json, constructType); + } + else if (json instanceof Reader) { + convertedValue = this.mapper.readValue((Reader) json, constructType); + } } catch (Exception e) { - throw new IllegalArgumentException("Cannot convert JSON " + json, e); + //ignore and let other converters have a chance } + return convertedValue; + } + + @Override + public byte[] toJson(Object value) { + try { + return this.mapper.writeValueAsBytes(value); + } + catch (Exception e) { + //ignore and let other converters have a chance + } + return null; } @Override @@ -55,4 +84,6 @@ public class JacksonMapper implements JsonMapper { } } + + } diff --git a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/json/JsonMapper.java b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/json/JsonMapper.java index 1a34ac8a0..f5a814d5a 100644 --- a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/json/JsonMapper.java +++ b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/json/JsonMapper.java @@ -50,9 +50,15 @@ public interface JsonMapper { * @param type type * @return object * @since 2.0 + * @deprecated since v3.0.4 in favor of {@link #fromJson(Object, Type)} */ + @Deprecated T toObject(String json, Type type); + T fromJson(Object json, Type type); + + byte[] toJson(Object value); + /** * @param type for list arguments * @param json JSON input @@ -60,6 +66,7 @@ public interface JsonMapper { * @return single object * @deprecated since v2.0 in favor of {@link #toObject(String, Type)} */ + @Deprecated default T toSingle(String json, Class type) { return toObject(json, type); }