Java tutorial
/* * Copyright 2015-2019 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.kafka.support.serializer; import java.io.IOException; import java.util.Arrays; import java.util.Collections; import java.util.Map; import org.apache.kafka.common.errors.SerializationException; import org.apache.kafka.common.header.Headers; import org.apache.kafka.common.serialization.Deserializer; import org.springframework.core.ResolvableType; import org.springframework.kafka.support.JacksonUtils; import org.springframework.kafka.support.converter.AbstractJavaTypeMapper; import org.springframework.kafka.support.converter.DefaultJackson2JavaTypeMapper; import org.springframework.kafka.support.converter.Jackson2JavaTypeMapper; import org.springframework.kafka.support.converter.Jackson2JavaTypeMapper.TypePrecedence; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectReader; import com.fasterxml.jackson.databind.type.TypeFactory; /** * Generic {@link org.apache.kafka.common.serialization.Deserializer Deserializer} for * receiving JSON from Kafka and return Java objects. * * @param <T> class of the entity, representing messages * * @author Igor Stepanov * @author Artem Bilan * @author Gary Russell * @author Yanming Zhou * @author Elliot Kennedy * @author Torsten Schleede * @author Ivan Ponomarev */ public class JsonDeserializer<T> implements Deserializer<T> { private static final String KEY_DEFAULT_TYPE_STRING = "spring.json.key.default.type"; private static final String DEPRECATED_DEFAULT_VALUE_TYPE = "spring.json.default.value.type"; /** * Kafka config property for the default key type if no header. * @deprecated in favor of {@link #KEY_DEFAULT_TYPE} */ @Deprecated public static final String DEFAULT_KEY_TYPE = KEY_DEFAULT_TYPE_STRING; /** * Kafka config property for the default value type if no header. * @deprecated in favor of {@link #VALUE_DEFAULT_TYPE} */ @Deprecated public static final String DEFAULT_VALUE_TYPE = DEPRECATED_DEFAULT_VALUE_TYPE; /** * Kafka config property for the default key type if no header. */ public static final String KEY_DEFAULT_TYPE = KEY_DEFAULT_TYPE_STRING; /** * Kafka config property for the default value type if no header. */ public static final String VALUE_DEFAULT_TYPE = "spring.json.value.default.type"; /** * Kafka config property for trusted deserialization packages. */ public static final String TRUSTED_PACKAGES = "spring.json.trusted.packages"; /** * Kafka config property to add type mappings to the type mapper: * 'foo=com.Foo,bar=com.Bar'. */ public static final String TYPE_MAPPINGS = JsonSerializer.TYPE_MAPPINGS; /** * Kafka config property for removing type headers (default true). */ public static final String REMOVE_TYPE_INFO_HEADERS = "spring.json.remove.type.headers"; /** * Kafka config property for using type headers (default true). * @since 2.2.3 */ public static final String USE_TYPE_INFO_HEADERS = "spring.json.use.type.headers"; protected final ObjectMapper objectMapper; // NOSONAR protected JavaType targetType; // NOSONAR protected Jackson2JavaTypeMapper typeMapper = new DefaultJackson2JavaTypeMapper(); // NOSONAR private volatile ObjectReader reader; private boolean typeMapperExplicitlySet = false; private boolean removeTypeHeaders = true; private boolean useTypeHeaders = true; /** * Construct an instance with a default {@link ObjectMapper}. */ public JsonDeserializer() { this((Class<T>) null, true); } /** * Construct an instance with the provided {@link ObjectMapper}. * @param objectMapper a custom object mapper. */ public JsonDeserializer(ObjectMapper objectMapper) { this((Class<T>) null, objectMapper, true); } /** * Construct an instance with the provided target type, and a default * {@link ObjectMapper}. * @param targetType the target type to use if no type info headers are present. */ public JsonDeserializer(Class<? super T> targetType) { this(targetType, true); } /** * Construct an instance with the provided target type, and a default {@link ObjectMapper}. * @param targetType the target type reference to use if no type info headers are present. * @since 2.3 */ public JsonDeserializer(TypeReference<? super T> targetType) { this(targetType, true); } /** * Construct an instance with the provided target type, and * useHeadersIfPresent with a default {@link ObjectMapper}. * @param targetType the target type. * @param useHeadersIfPresent true to use headers if present and fall back to target * type if not. * @since 2.2 */ public JsonDeserializer(@Nullable Class<? super T> targetType, boolean useHeadersIfPresent) { this(targetType, JacksonUtils.enhancedObjectMapper(), useHeadersIfPresent); } /** * Construct an instance with the provided target type, and * useHeadersIfPresent with a default {@link ObjectMapper}. * @param targetType the target type reference. * @param useHeadersIfPresent true to use headers if present and fall back to target * type if not. * @since 2.3 */ public JsonDeserializer(TypeReference<? super T> targetType, boolean useHeadersIfPresent) { this(targetType, JacksonUtils.enhancedObjectMapper(), useHeadersIfPresent); } /** * Construct an instance with the provided target type, and {@link ObjectMapper}. * @param targetType the target type to use if no type info headers are present. * @param objectMapper the mapper. type if not. */ public JsonDeserializer(Class<? super T> targetType, ObjectMapper objectMapper) { this(targetType, objectMapper, true); } /** * Construct an instance with the provided target type, and {@link ObjectMapper}. * @param targetType the target type reference to use if no type info headers are present. * @param objectMapper the mapper. type if not. */ public JsonDeserializer(TypeReference<? super T> targetType, ObjectMapper objectMapper) { this(targetType, objectMapper, true); } /** * Construct an instance with the provided target type, {@link ObjectMapper} and * useHeadersIfPresent. * @param targetType the target type. * @param objectMapper the mapper. * @param useHeadersIfPresent true to use headers if present and fall back to target * type if not. * @since 2.2 */ public JsonDeserializer(@Nullable Class<? super T> targetType, ObjectMapper objectMapper, boolean useHeadersIfPresent) { Assert.notNull(objectMapper, "'objectMapper' must not be null."); this.objectMapper = objectMapper; JavaType javaType = null; if (targetType == null) { Class<?> genericType = ResolvableType.forClass(getClass()).getSuperType().resolveGeneric(0); if (genericType != null) { javaType = TypeFactory.defaultInstance().constructType(genericType); } } else { javaType = TypeFactory.defaultInstance().constructType(targetType); } initialize(javaType, useHeadersIfPresent); } /** * Construct an instance with the provided target type, {@link ObjectMapper} and * useHeadersIfPresent. * @param targetType the target type reference. * @param objectMapper the mapper. * @param useHeadersIfPresent true to use headers if present and fall back to target * type if not. * @since 2.3 */ public JsonDeserializer(TypeReference<? super T> targetType, ObjectMapper objectMapper, boolean useHeadersIfPresent) { this(targetType != null ? TypeFactory.defaultInstance().constructType(targetType) : null, objectMapper, useHeadersIfPresent); } /** * Construct an instance with the provided target type, {@link ObjectMapper} and * useHeadersIfPresent. * @param targetType the target type reference. * @param objectMapper the mapper. * @param useHeadersIfPresent true to use headers if present and fall back to target * type if not. * @since 2.3 */ public JsonDeserializer(@Nullable JavaType targetType, ObjectMapper objectMapper, boolean useHeadersIfPresent) { Assert.notNull(objectMapper, "'objectMapper' must not be null."); this.objectMapper = objectMapper; initialize(targetType, useHeadersIfPresent); } private void initialize(@Nullable JavaType type, boolean useHeadersIfPresent) { this.targetType = type; Assert.isTrue(this.targetType != null || useHeadersIfPresent, "'targetType' cannot be null if 'useHeadersIfPresent' is false"); if (this.targetType != null) { this.reader = this.objectMapper.readerFor(this.targetType); } addTargetPackageToTrusted(); this.typeMapper.setTypePrecedence(useHeadersIfPresent ? TypePrecedence.TYPE_ID : TypePrecedence.INFERRED); } public Jackson2JavaTypeMapper getTypeMapper() { return this.typeMapper; } /** * Set a customized type mapper. * @param typeMapper the type mapper. * @since 2.1 */ public void setTypeMapper(Jackson2JavaTypeMapper typeMapper) { Assert.notNull(typeMapper, "'typeMapper' cannot be null"); this.typeMapper = typeMapper; this.typeMapperExplicitlySet = true; } /** * Configure the default Jackson2JavaTypeMapper to use key type headers. * @param isKey Use key type headers if true * @since 2.1.3 */ public void setUseTypeMapperForKey(boolean isKey) { if (!this.typeMapperExplicitlySet && this.getTypeMapper() instanceof AbstractJavaTypeMapper) { ((AbstractJavaTypeMapper) this.getTypeMapper()).setUseForKey(isKey); } } /** * Set to false to retain type information headers after deserialization. * Default true. * @param removeTypeHeaders true to remove headers. * @since 2.2 */ public void setRemoveTypeHeaders(boolean removeTypeHeaders) { this.removeTypeHeaders = removeTypeHeaders; } /** * Set to false to ignore type information in headers and use the configured * target type instead. * Only applies if the preconfigured type mapper is used. * Default true. * @param useTypeHeaders false to ignore type headers. * @since 2.2.8 */ public void setUseTypeHeaders(boolean useTypeHeaders) { if (!this.typeMapperExplicitlySet) { this.useTypeHeaders = useTypeHeaders; setUpTypePrecedence(Collections.emptyMap()); } } @Override public void configure(Map<String, ?> configs, boolean isKey) { setUseTypeMapperForKey(isKey); setUpTypePrecedence(configs); setupTarget(configs, isKey); if (configs.containsKey(TRUSTED_PACKAGES) && configs.get(TRUSTED_PACKAGES) instanceof String) { this.typeMapper.addTrustedPackages( StringUtils.commaDelimitedListToStringArray((String) configs.get(TRUSTED_PACKAGES))); } if (configs.containsKey(TYPE_MAPPINGS) && !this.typeMapperExplicitlySet && this.typeMapper instanceof AbstractJavaTypeMapper) { ((AbstractJavaTypeMapper) this.typeMapper).setIdClassMapping( JsonSerializer.createMappings(configs.get(JsonSerializer.TYPE_MAPPINGS).toString())); } if (configs.containsKey(REMOVE_TYPE_INFO_HEADERS)) { this.removeTypeHeaders = Boolean.parseBoolean(configs.get(REMOVE_TYPE_INFO_HEADERS).toString()); } } private void setUpTypePrecedence(Map<String, ?> configs) { if (!this.typeMapperExplicitlySet) { if (configs.containsKey(USE_TYPE_INFO_HEADERS)) { this.useTypeHeaders = Boolean.parseBoolean(configs.get(USE_TYPE_INFO_HEADERS).toString()); } this.typeMapper .setTypePrecedence(this.useTypeHeaders ? TypePrecedence.TYPE_ID : TypePrecedence.INFERRED); } } private void setupTarget(Map<String, ?> configs, boolean isKey) { try { JavaType javaType = null; if (isKey && configs.containsKey(KEY_DEFAULT_TYPE)) { javaType = setupTargetType(configs, KEY_DEFAULT_TYPE); } // TODO don't forget to remove these code after DEFAULT_VALUE_TYPE being removed. else if (!isKey && configs.containsKey(DEPRECATED_DEFAULT_VALUE_TYPE)) { javaType = setupTargetType(configs, DEPRECATED_DEFAULT_VALUE_TYPE); } else if (!isKey && configs.containsKey(VALUE_DEFAULT_TYPE)) { javaType = setupTargetType(configs, VALUE_DEFAULT_TYPE); } if (javaType != null) { initialize(javaType, TypePrecedence.TYPE_ID.equals(this.typeMapper.getTypePrecedence())); } } catch (ClassNotFoundException | LinkageError e) { throw new IllegalStateException(e); } } private JavaType setupTargetType(Map<String, ?> configs, String key) throws ClassNotFoundException, LinkageError { if (configs.get(key) instanceof Class) { return TypeFactory.defaultInstance().constructType((Class<?>) configs.get(key)); } else if (configs.get(key) instanceof String) { return TypeFactory.defaultInstance().constructType(ClassUtils.forName((String) configs.get(key), null)); } else { throw new IllegalStateException(key + " must be Class or String"); } } /** * Add trusted packages for deserialization. * @param packages the packages. * @since 2.1 */ public void addTrustedPackages(String... packages) { doAddTrustedPackages(packages); } private void addTargetPackageToTrusted() { String targetPackageName = getTargetPackageName(); if (targetPackageName != null) { doAddTrustedPackages(targetPackageName); } } private String getTargetPackageName() { if (this.targetType != null) { return ClassUtils.getPackageName(this.targetType.getRawClass()).replaceFirst("\\[L", ""); } return null; } private void doAddTrustedPackages(String... packages) { this.typeMapper.addTrustedPackages(packages); } @Override public T deserialize(String topic, Headers headers, byte[] data) { if (data == null) { return null; } ObjectReader deserReader = null; if (this.typeMapper.getTypePrecedence().equals(TypePrecedence.TYPE_ID)) { JavaType javaType = this.typeMapper.toJavaType(headers); if (javaType != null) { deserReader = this.objectMapper.readerFor(javaType); } } if (this.removeTypeHeaders) { this.typeMapper.removeHeaders(headers); } if (deserReader == null) { deserReader = this.reader; } Assert.state(deserReader != null, "No type information in headers and no default type provided"); try { return deserReader.readValue(data); } catch (IOException e) { throw new SerializationException( "Can't deserialize data [" + Arrays.toString(data) + "] from topic [" + topic + "]", e); } } @Override public T deserialize(String topic, @Nullable byte[] data) { if (data == null) { return null; } Assert.state(this.reader != null, "No headers available and no default type provided"); try { return this.reader.readValue(data); } catch (IOException e) { throw new SerializationException( "Can't deserialize data [" + Arrays.toString(data) + "] from topic [" + topic + "]", e); } } @Override public void close() { // No-op } // Fluent API /** * Designate this deserializer for deserializing keys (default is values); only * applies if the default type mapper is used. * @return the deserializer. * @since 2.3 */ public JsonDeserializer<T> forKeys() { setUseTypeMapperForKey(true); return this; } /** * Don't remove type information headers. * @return the deserializer. * @since 2.3 * @see #setRemoveTypeHeaders(boolean) */ public JsonDeserializer<T> dontRemoveTypeHeaders() { setRemoveTypeHeaders(false); return this; } /** * Ignore type information headers and use the configured target class. * @return the deserializer. * @since 2.3 * @see #setUseTypeHeaders(boolean) */ public JsonDeserializer<T> ignoreTypeHeaders() { setUseTypeHeaders(false); return this; } /** * Use the supplied {@link Jackson2JavaTypeMapper}. * @param mapper the mapper. * @return the deserializer. * @since 2.3 * @see #setTypeMapper(Jackson2JavaTypeMapper) */ public JsonDeserializer<T> typeMapper(Jackson2JavaTypeMapper mapper) { setTypeMapper(mapper); return this; } }