前言
最近有同事问了我一个问题。为什么 Redis 缓存中的 Value 中会有 type 信息。如下面的 json 结构:
印象中,这个 type 是由一个 jackson 的配置决定的。看了一下目前使用到的序列化配置。
项目中使用了 SpringCache 做缓存管理
和猜想的一致,确实是在做序列化的时候配置了生成类信息的配置。
好了,问题有了答案。但在看到这个时候,又有了一个疑问,没有什么需要把类型序列化进去,能不能不需要?如果必须需要,那为什么(因为想到了 SpringMvc 做反序列化和序列化,是不需要类型相关信息的)
好,我们开始一步一步来剖析这个问题,第一步要看的问题是:activedDefaultTyping 这个函数干了什么
序列化中的类型信息
在 Jackson 中,activateDefaultTyping()
方法用于启用 多态类型处理(即序列化时嵌入类型信息,以便反序列化时能还原对象类型)。不同参数的重载方法会影响类型信息的存储方式和安全策略。以下是两种调用的区别和底层逻辑:
三个参数的 activateDefaultTyping()
参数解析
LaissezFaireSubTypeValidator.instance
:Jackson 的PolymorphicTypeValidator
实现,表示 不限制反序列化的子类(允许任何类)。
此配置存在安全风险(攻击者可构造恶意
@class
字段触发任意类加载)。可以通过BasicPolymorphicTypeValidator
来限制 @class 可以序列化的类的范围。
DefaultTyping.NON_FINAL
: 指定类型信息仅嵌入 非 final 类型 的字段或返回值中。final
类型(如String
、Integer
)不嵌入类型信息。- 非
final
类型(如自定义的User
类)会嵌入类型信息。
JsonTypeInfo.As.PROPERTY
: 指定类型信息以 JSON 属性 的形式嵌入,默认属性名为@class
。
适用场景
- 需要显式控制类型信息的嵌入形式(如要求类型信息以
@class
字段存储)。
两个参数的 activateDefaultTyping()
参数解析
LaissezFaireSubTypeValidator.instance
:同上。DefaultTyping.NON_FINAL
:同上。- 缺失的第三个参数 : 默认使用
JsonTypeInfo.As.WRAPPER_ARRAY
,即类型信息以 包装数组 的形式嵌入。
适用场景
- 不需要自定义类型信息的嵌入形式(接受默认的数组包装格式)。
核心区别
特性 | 三个参数版本 | 两个参数版本 |
---|---|---|
类型信息存储方式 | 通过 JsonTypeInfo.As.PROPERTY 指定(如 @class 字段) | 默认使用 JsonTypeInfo.As.WRAPPER_ARRAY(类型信息作为数组的第一个元素) |
JSON 结构 | 对象内嵌 @class 字段 | 类型信息和数据包装成数组 |
可读性 | 更友好(类型信息与数据字段共存) | 结构嵌套较深,可读性稍差 |
兼容性 | 与大多数 JSON 工具兼容 | 某些工具可能不识别数组包装格式 |
到这里,我明白了在序列化的过程中,什么情况下会出现 type,什么情况下会出现 @class 属性。
之后,我又想到一个问题,那能不能让 redis 序列化的时候不放入当前类的 type 属性或者 @class 属性呢。
Redis 序列化配置
笔者这里使用的操作 Redis 的场景比较简单,几乎都是缓存操作,对于缓存操作,直接使用了 SpringCache 来支持。具体到代码中的写法如下:
笔者这里的 CacheManager 的配置是使用了 GenericJackson2JsonRedisSerializer 来完成 Redis 序列化,具体配置和前言中的一样。使用了自定义的 ObjectMapper 来实现对象的序列化。
去除序列化时类型写入
为了验证能否去除类型相关的信息,于是我先将 activateDefaultTyping
相关的内容全部做了移除。再执行缓存的逻辑,此时第一次请求是成功,并且也写入了缓存数据,且没有携带类型信息。
但当我第二次请求的时候,我获得了一个报错信息:
这样,问题就有趣了起来,接下来,我们来跟一下代码吧。SpringCache 的代码很熟悉了,我们直接来看:
不了解的兄弟,可以看看 deepseek ,现在有了 deepseek 每个人都相当于有了一个名师指导
核心逻辑在:org.springframework.cache.interceptor.CacheAspectSupport#execute 这个类里。
其中第 19 ~20 行为没有缓存时的调用,unwrapReturnValue
会将函数返回值,包装成 cache 缓存对象,最后由 cachePutRequest.apply 逻辑写入缓存。
在执行写入的时候,会调用 RedisCache 的操作,其中对 value 进行序列化的就是使用了 CacheConfig 中配置的序列化器,即我们自定义的序列化配置。
此时写入的就是不带类型或 @class 信息的缓存。
但当执行第二次请求的时候,由于此时存在缓存,因此会选择从缓存中读取数据,于是走到下述的代码:
此时,由于没有具体的类型,Jackson 会默认将 Json 序列化为 LinkedHashMap,动态代理会将缓存中的值返回,但此时由于方法的返回值是一个对象,Java 会做默认做强转,于是就出现了上面的报错信息。
SpringMVC怎么干的
到这里,我想到了既然一定需要类型才能做反序列化,那么 SpringMVC 是怎么做序列化的。好,打开 MVC 的转换器的实现:org.springframework.http.converter.json.MappingJackson2HttpMessageConverter 核心逻辑在其父类的 org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter#read 函数中。
和 SpringCache 不同的是,SpringMVC 在处理序列化的时候获取到了当前方法要反序列化的类型,因此可以直接通过 Jackson 做反序列化。
到此,也就明白了为什么 SpringMVC 没有类型信息也可以完成序列化,而 SpringCache 必须需要类型才能序列化。
总结
- SpringCache 在做反序列化和序列化的时候是直接在 Cache 层完成,不关心缓存注解放到了哪个方法上,所以在序列化的时候必须添加类型信息,以保证反序列化成功
- SpringMVC 在序列化和反序列化的时候获取到了当前要反序列化后的类信息,因此可以直接通过 Jackson 完成 Json 到 Type 之前的转换,不需要在 Json 中添加类型信息。
题外话
在看 GenericJackson2JsonRedisSerializer
的时候,我发现了另外一个 Redis 的序列化的类 Jackson2JsonRedisSerializer
这个类的定义如下:
其构造函数接受一个类型参数,标识当前反序列化的类型是什么。
也就是说:我们可以定义多个在 SpringCache 中定义多个 CacheManager 来控制当前反序列化使用的反序列化器进而实现 Json 中没有类型属性时的反序列化。
但这样做的问题是:如果涉及到的缓存 DTO 比较多的话,需要定义很多的 CacheManager 。即有 10 个 DTO 需要缓存,则需要定义 10 个 CacheManager。
具体使用方式如下:如红色的部分,每一个 DTO 都对应一个 CacheManager