在springboot项目中,使用jackson作为redis序列化工具后,读取值可能会出现这个异常:InvalidTypeIdException: Missing type id when ...missing type id property '@class'
。搞了一下午,发现是对kotlin
和jackson
不熟导致的坑。
环境说明 我项目基于springboot
搭建,用的是gradle
构建工具,之前是纯java项目,后面看着kotlin
挺方便的,又加上了kotlin
,现在大部分代码为kotlin
,准备用redis
作为底层实现Cache
功能。根据官网和网上的资料,配置好跑起来后,调用@Cacheable
等Cache
注解的方法后,只能存,不能取,一取就报错。
初版配置 因为我这里使用jackson
序列化代替默认的JDK
序列化,所以需要配置一个RedisTemplate
,最终发现问题就出在这个配置中,最开始代码如下(java
代码):
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 @EnableCaching @Configuration public class RedisConfig { @Bean public RedisTemplate<Object, Object> redisTemplate (RedisConnectionFactory connectionFactory) { RedisTemplate<Object, Object> template = new RedisTemplate<>(); template.setConnectionFactory(connectionFactory); Jackson2JsonRedisSerializer serializer = new Jackson2JsonRedisSerializer(Object.class); ObjectMapper objectMapper = new ObjectMapper(); JavaTimeModule javaTimeModule = new JavaTimeModule(); objectMapper.registerModule(javaTimeModule); objectMapper.registerModule(new KotlinModule()); objectMapper.registerModule(new Jdk8Module()); objectMapper.configure(MapperFeature.USE_ANNOTATIONS, false ); objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); serializer.setObjectMapper(objectMapper); template.setValueSerializer(serializer); template.setKeySerializer(new StringRedisSerializer()); template.afterPropertiesSet(); return template; } @Bean public RedisCacheConfiguration redisCacheConfiguration (RedisTemplate redisTemplate) { RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig(); return redisCacheConfiguration.entryTtl(Duration.ofMinutes(15 )) .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(redisTemplate.getValueSerializer())); } }
配置好了就使用Cache
(kotlin
代码):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class TestUserKt { var id:Int ?=null var name:String?=null var pass:String?=null }@Service class MyService { @Cacheable(value = ["test-config" ]) fun getUserById (id:Int ) :TestUserKt{ return TestUserKt(id,"user:" +id,"pass:" +id) } }
在第二次调用MyService.getUserById(1)
的时候,就出错了(第一次调用不会走Cache
,因为Cache中还没有内容
)
运行结果 1 class java .util .LinkedHashMap cannot be cast to class xxx .TestUserKt
原因 网上搜索了一番资料,发现是因为spring
从redis
读取到指定key
内容后,反序列化
的时候,因为没有提供任何要反序列化对象的元信息,json
里的object
就默认反序列化为LinkedHashMap
了。而在 RedisCache
实现中,会强制转换成期望的对象(TestUserKt
),所以导致报错。修复方式非常简单,即开启RedisTemplate.objectMapper
的DefaultTyping
。
二次配置 既然如此,那就开启DefaultTyping
,配置如下(java
代码):
1 2 3 4 5 6 7 8 9 10 11 12 13 @Bean public RedisTemplate<Object, Object> redisTemplate (RedisConnectionFactory connectionFactory) { ObjectMapper objectMapper = new ObjectMapper(); objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance , ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY); template.setValueSerializer(serializer); template.setKeySerializer(new StringRedisSerializer()); template.afterPropertiesSet(); return template; }
DefaultTyping
的功能是,在把对象序列化json
时,把对象的类名称(包括包名)写入到json
中,这样下次读取的时候,就可以根据类名称就反序列化成对象了,而不是Map
。
需要注意 的是,老版本的objectMapper.objectMapper.enableDefaultTyping()
方法已经过时,官方推荐用objectMapper.activateDefaultTyping()
替代。
运行结果 讲道理,到这一步应该不会出什么幺蛾子了吧,万万没想到,跑起来又抛来个异常!!
1 2 3 org.springframework.data.redis.serializer.SerializationException: Could not read JSON: Missing type id when trying to resolve subtype of [simple type, class java.lang.Object]: missing type id property '@class' at [Source: (byte[])"{"id":1,"name":"user:1","pass":"pass:1"}"; line: 1, column: 40]; nested exception is com.fasterxml.jackson.databind.exc.InvalidTypeIdException: Missing type id when trying to resolve subtype of [simple type, class java.lang.Object]: missing type id property '@class' at [Source: (byte [])"{" id":1," name":" user:1 "," pass":" pass:1 "}" ; line: 1 , column: 40 ]
原因 找这一次的原因的花了点时间,打开Redis Desktop Manager
,找到程序存储的路径,发现Kotlin
编写的类对象已经序列化了,但是没有包括对象的类信息:
1 2 3 4 5 { "id" : 1 , "name" : "user:1" , "pass" : "pass:1" }
而Cache的普通java
类对象也已经序列化,而且包括了类信息
1 2 3 4 5 { "@class" : "xxx.TestUser" , "name" : "javaName" , "passwrod" : "javaPass" }
之所以会有这个差异,是因为默认kotlin
的类最终会转成final
的java
类。如class MyKotlinUser
会转成public final class MyKotlinUser
。还记得上面开启DefaultTyping
的参数为ObjectMapper.DefaultTyping.NON_FINAL
,这个参数的意思是,final
类不会写出类信息。因为没有类信息,而配置的RedisCache
又一定需要类信息才能完成反序列化,所以就导致了上面的报错。
避坑总结 知道原因后,就好办了,可以从以下几个方面避坑
使用默认JDK序列化 这种方式的好处是能避免很多json
序列化导致的问题,但是占用的redis空间比json
序列化大
使用open class
对于自己写的将要被Cache
的kotlin
类,加上open
修饰符,这样生成的java
就不会是final
类了。不好的地方在于,无法控制别人写的类,也就是无法直接反序列化别人的普通kotlin
类
传入DefaultTyping.EVERYTHING
参数(推荐) 传入DefaultTyping.EVERYTHING
的意思是:所有的类型(包括引用类型、原生类型)类信息都会写入json
中,这样保证了不会出现只能序列化,不能反序列化的问题。一点小问题是,带来的小问题是,有时比普通json大一点:objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance , ObjectMapper.DefaultTyping.EVERYTHING, JsonTypeInfo.As.PROPERTY);