Kotlin使用Jackson序列化中的一些问题

问题复现

在实现自定义的缓存DSL中的RedisCache模块的时候,序列化这里是参考SpringBoot中RedisCache的来的,使用的是RedisSerializer.json(),点进去其实是如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public GenericJackson2JsonRedisSerializer(@Nullable String classPropertyTypeName) {

this(new ObjectMapper());

// simply setting {@code mapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS)} does not help here since we need
// the type hint embedded for deserialization using the default typing feature.
registerNullValueSerializer(mapper, classPropertyTypeName);

if (StringUtils.hasText(classPropertyTypeName)) {
mapper.enableDefaultTypingAsProperty(DefaultTyping.NON_FINAL, classPropertyTypeName);
} else {
mapper.enableDefaultTyping(DefaultTyping.NON_FINAL, As.PROPERTY);
}
}

这次的设计思路是需要保证我们的数据既要存到redis,又要能从redis中序列化出来,即进去的是一个对象,出来的还是那个对象。这种情况下 RedisSerializer.json()会把类的信息给输出到Json结构中,然后反序列化的时候就知道是哪个类了,如下面所示:

1
{"@class":"com.bybutter.sisyphus.middleware.cache.redis.Test","a":"a","b":"b"}

但是当我使用Kotlin的data class的时候,却怎么也不能进行序列化,输出的Json是不带@class的。

1
{"a":"a","b":"b"}

问题解决

我们点进去GenericJackson2JsonRedisSerializer类会发现,其中有个属性是ObjectMapper.DefaultTyping.NON_FINAL,但是我在前面Kotlin入门-class那篇文章中也说过了,Kotlin的class默认都是Final的,然后点进去这个枚举发现有如下属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
public enum DefaultTyping {
/**
* This value means that only properties that have
* {@link java.lang.Object} as declared type (including
* generic types without explicit type) will use default
* typing.
*/
JAVA_LANG_OBJECT,

/**
* Value that means that default typing will be used for
* properties with declared type of {@link java.lang.Object}
* or an abstract type (abstract class or interface).
* Note that this does <b>not</b> include array types.
*<p>
* Since 2.4, this does NOT apply to {@link TreeNode} and its subtypes.
*/
OBJECT_AND_NON_CONCRETE,

/**
* Value that means that default typing will be used for
* all types covered by {@link #OBJECT_AND_NON_CONCRETE}
* plus all array types for them.
*<p>
* Since 2.4, this does NOT apply to {@link TreeNode} and its subtypes.
*/
NON_CONCRETE_AND_ARRAYS,

/**
* Value that means that default typing will be used for
* all non-final types, with exception of small number of
* "natural" types (String, Boolean, Integer, Double), which
* can be correctly inferred from JSON; as well as for
* all arrays of non-final types.
*<p>
* Since 2.4, this does NOT apply to {@link TreeNode} and its subtypes.
*/
NON_FINAL,

/**
* Value that means that default typing will be used for
* all non-final types, with exception of small number of
* "natural" types (String, Boolean, Integer, Double) that
* can be correctly inferred from JSON, and primitives (which
* can not be polymorphic either). Typing is also enabled for
* all array types.
*<p>
* Note that the only known use case for this setting is for serialization
* when passing instances of final class, and base type is not
* separately specified.
*
* @since 2.10
*/
EVERYTHING
}

在Jackson-databind的issue中有人问到过类似的问题 然后官方的回答如下:

picture 1

然后就加了一种叫EVERYTHING的类型,好吧。。。挺省事的。

然后就配置了objectMapper 如下:

1
2
3
4
5
6
7
8
GenericJackson2JsonRedisSerializer(ObjectMapper().apply {
this.registerKotlinModule()
this.setSerializationInclusion(JsonInclude.Include.NON_NULL)
this.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
this.configure(JsonParser.Feature.IGNORE_UNDEFINED, true)
this.configure(JsonGenerator.Feature.IGNORE_UNKNOWN, true)
this.activateDefaultTyping(BasicPolymorphicTypeValidator.builder().allowIfBaseType(Any::class.java).build(), ObjectMapper.DefaultTyping.EVERYTHING)
})

然后就又出现了新的错误:

1
java.lang.UnsupportedOperationException: com.fasterxml.jackson.databind.JsonMappingException: Unexpected token (START_OBJECT), expected VALUE_STRING: need JSON String that contains type id (for subtype of java.util.List)

然后搜了一下,结果查到的解决办法全是不用enableDefaultTyping,这。。也有点简单粗暴,肯定是不可取的然后点进去看了下类的构造,大致错误定位到错误是另外一个直接使用默认属性,默认的为JsonTypeInfo.As.WRAPPER_ARRAY,这样就会导致对象会被序列化成数组。然后改成JsonTypeInfo.As.PROPERTY就可以了。

总结

​ 这次的问题虽然是我自己写的缓存的DSL过程中发现的,但是对于正常的使用Kotlin的Spring Boot的项目来说在使用@Cacheable的注解的时候,自定义序列化器的时候要注意一下。其实还有一种解决办法,我在前面Kotlin类的那一篇文章也说了使用all-open插件,这样也是可以的

​ 其实这次的问题不是很难,内容也不是很多,但是网上相关资料很少,尤其是中文资料,应该同时使用Kotlin和Spring Boot的人不多吧,但是Kotlin真的是一门优秀的语言,在Spring Boot中使用也是很丝滑的,希望以后这么用的人越来越多,也更加希望我们在使用过程中踩过的坑能为后来者提供一个解决方案,少踩一些坑。

参考:[How can I easily cache Kotlin Objects in Redis using json via Jackson?]