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
This commit is contained in:
Oleg Zhurakousky
2020-04-01 16:07:42 +02:00
parent fb08a50b94
commit 366d05050f
5 changed files with 165 additions and 40 deletions

View File

@@ -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<MessageConverter> messageConverters, @Nullable ObjectMapper objectMapper,
ConfigurableApplicationContext context) {
public FunctionRegistry functionCatalog(List<MessageConverter> messageConverters, JsonMapper jsonMapper, ConfigurableApplicationContext context) {
ConfigurableConversionService conversionService = (ConfigurableConversionService) context.getBeanFactory().getConversionService();
Map<String, GenericConverter> 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);
}

View File

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

View File

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

View File

@@ -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> T toObject(String json, Type type) {
return this.fromJson(json, type);
}
@Override
public <T> 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 {
}
}
}

View File

@@ -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> T toObject(String json, Type type);
<T> T fromJson(Object json, Type type);
byte[] toJson(Object value);
/**
* @param <T> 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> T toSingle(String json, Class<T> type) {
return toObject(json, type);
}