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); }