啊?我的缓存中怎么出现了 java.util.ArrayList

前言

最近有同事问了我一个问题。为什么 Redis 缓存中的 Value 中会有 type 信息。如下面的 json 结构:

印象中,这个 type 是由一个 jackson 的配置决定的。看了一下目前使用到的序列化配置。

项目中使用了 SpringCache 做缓存管理

和猜想的一致,确实是在做序列化的时候配置了生成类信息的配置。

好了,问题有了答案。但在看到这个时候,又有了一个疑问,没有什么需要把类型序列化进去,能不能不需要?如果必须需要,那为什么(因为想到了 SpringMvc 做反序列化和序列化,是不需要类型相关信息的)

好,我们开始一步一步来剖析这个问题,第一步要看的问题是:activedDefaultTyping 这个函数干了什么

序列化中的类型信息

在 Jackson 中,activateDefaultTyping() 方法用于启用 多态类型处理(即序列化时嵌入类型信息,以便反序列化时能还原对象类型)。不同参数的重载方法会影响类型信息的存储方式和安全策略。以下是两种调用的区别和底层逻辑:

三个参数的 activateDefaultTyping()

参数解析

  1. LaissezFaireSubTypeValidator.instance:Jackson 的 PolymorphicTypeValidator 实现,表示 不限制反序列化的子类(允许任何类)。

此配置存在安全风险(攻击者可构造恶意 @class 字段触发任意类加载)。可以通过 BasicPolymorphicTypeValidator 来限制 @class 可以序列化的类的范围。

  1. DefaultTyping.NON_FINAL: 指定类型信息仅嵌入 非 final 类型 的字段或返回值中。
    1. final 类型(如 StringInteger)不嵌入类型信息。
    2. final 类型(如自定义的 User 类)会嵌入类型信息。
  2. JsonTypeInfo.As.PROPERTY: 指定类型信息以 JSON 属性 的形式嵌入,默认属性名为 @class

适用场景

  • 需要显式控制类型信息的嵌入形式(如要求类型信息以 @class 字段存储)。

两个参数的 activateDefaultTyping()

参数解析

  1. LaissezFaireSubTypeValidator.instance:同上。
  2. DefaultTyping.NON_FINAL:同上。
  3. 缺失的第三个参数 : 默认使用 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

相关推荐
我命由我123452 小时前
Kotlin 数据容器 - List(List 概述、创建 List、List 核心特性、List 元素访问、List 遍历)
java·开发语言·jvm·windows·java-ee·kotlin·list
武子康4 小时前
Java-80 深入浅出 RPC Dubbo 动态服务降级:从雪崩防护到配置中心秒级生效
java·分布式·后端·spring·微服务·rpc·dubbo
YuTaoShao7 小时前
【LeetCode 热题 100】131. 分割回文串——回溯
java·算法·leetcode·深度优先
源码_V_saaskw7 小时前
JAVA图文短视频交友+自营商城系统源码支持小程序+Android+IOS+H5
java·微信小程序·小程序·uni-app·音视频·交友
超浪的晨7 小时前
Java UDP 通信详解:从基础到实战,彻底掌握无连接网络编程
java·开发语言·后端·学习·个人开发
双力臂4048 小时前
Spring Boot 单元测试进阶:JUnit5 + Mock测试与切片测试实战及覆盖率报告生成
java·spring boot·后端·单元测试
心之语歌8 小时前
Spring AI MCP 客户端
人工智能·spring·github
Edingbrugh.南空8 小时前
Aerospike与Redis深度对比:从架构到性能的全方位解析
java·开发语言·spring
半新半旧8 小时前
python 整合使用 Redis
redis·python·bootstrap
QQ_4376643149 小时前
C++11 右值引用 Lambda 表达式
java·开发语言·c++