啊?我的缓存中怎么出现了 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

相关推荐
最懒的菜鸟14 分钟前
spring boot jwt生成token
java·前端·spring boot
小样vvv18 分钟前
【Redis】深入解析 Redis 五大数据结构
数据结构·数据库·redis
瑜舍25 分钟前
Apache Tomcat RCE漏洞(CVE-2025-24813)
java·tomcat·apache
un_fired26 分钟前
【Spring AI】基于专属知识库的RAG智能问答小程序开发——功能优化:用户鉴权
java·人工智能·spring
martian66528 分钟前
Java并发编程从入门到实战:同步、异步、多线程核心原理全解析
java·开发语言
计算机学姐37 分钟前
基于SpringBoot的电影售票系统
java·vue.js·spring boot·后端·mysql·spring·intellij-idea
半升酒39 分钟前
Spring MVC
java·spring
M1A143 分钟前
走进Java异步编程的世界:开启高效编程之旅
java·后端
机智的人猿泰山1 小时前
java 线程创建Executors 和 ThreadPoolExecutor 和 CompletableFuture 三者 区别
java·开发语言
努力的搬砖人.1 小时前
Tomcat相关的面试题
java·经验分享·后端·面试·tomcat