解决Springboot使用Redis反序列化遇到的类型转换异常

本文介绍了一种在SpringBoot结合Redis缓存时遇到的类型转换异常问题,该问题在单元测试中未出现,但在实际运行环境中发生。经过排查发现,问题根源在于SpringBoot DevTools导致的类加载器不一致。

异常现象

在使用Springboot 配合 Redis做缓存处理的时候,单元测试中对象的序列化和反序列化操作均正常,但是项目Runing后,接口操作出现类型转换异常,最可笑的是同一类型转换出了错?。

java.lang.ClassCastException: OauthCodeRedisCacheEntity cannot be cast to OauthCodeRedisCacheEntity

image.png

解决思路

第一、我排查了一下单元测试和实际API接口的代码逻辑是否相同,然而是相同的。

第二、通过Debug模式检查了一下单元测试和实际API接口对本系统下Redis反序列化方法入参的参数值,发现也均值一样的

第三、先后执行单元测试和实际API,查看Redis缓存中的数据是否相同,经检验也是相同的。

第四、我考虑到是否是因多线程引起的,所以对调用该序列化方法的Service层进行了并发控制,这里仅加了可重入锁。然而并没有成功

第五、MDZZ,心态爆炸,歇了一会,和丫头说了会话 …. 很开心

第六、Google 搜索关键词“Springboot Redis 反序列化类型转换失败” …. 未果,看来中文search不行

第七、Google 直接粘贴报错信息。 果然,SF大法好

刨根问底

When you use DevTools with caching, you need to be aware of this limitation.

When the object is serialized into the cache, the application class loader is C1. Then after you change some code/configuration, devtools automatically restart the context and creates a new classloader (C2). When you hit that cache method, the cache abstraction finds an entry in the cache and it deserializes it from the store. If the cache library doesn’t take the context classloader into account, that object will have the wrong classloader attached to it (which explains that weird exception A cannot be cast to A).

上面这段话是Stack Overflow社区一大佬就此问题的回答,大意是说,当使用SpringBoot 的 DevTools时,其实该工具是具有缓存效果的,这点需要注意,而且该大佬也提供的注意事项的连接地址 this limitation.

当对象被序列化到缓存里时,当前应用的类加载器是C1,当你改变了一些代码或者配置文件的时候,DevTools 工具将会自动重新启动这个容器,并且创建一个新的类加载器 C2. 这时候调用这个具有缓存的方法时,缓存管理将会从缓存里找到该条缓存记录并进行反序列化操作。如果缓存库不考虑上下文的话,也就是没注意到类加载器的变化时,该对象将会有错误的类加载器(这解释了奇怪的异常)。

其实就是因上下文类加载器不同而产生这样的错误,那么归根结底就是因SpringBoot DevTools工具搞的鬼。

这时可以将项目关闭并重启一下。

果然,在项目配置初期,为了实现所谓的热部署和热加载使用了该工具库,果断删掉。

Perfect!!

总结

但凡遇到报错信息,切记不要慌不要乱,有因才有果,可以吓你根据自己的分析按照自己的想法进行错误排查,针对出现的类型转换出现的异常,首先应该想到是不是自己代码的问题,因为此次错误我在单元测试中是可用的,所以业务代码层面不会出现问题。

尽量把自己能想到 的解决方式全都用一遍,因为此时你对这个报错已经有了大题的了解,这个了解仅仅是利用排除法了解到的,即 他不是因为什么什么而发生。所以在接下来使用搜索引擎的时候,可以避免走一些弯路。

自己能想到的解决方法都用了之后,建议直接Google 报错信息,简单粗暴。逐条查看,按照说明处理。

当然最重要的还是心态,我在掏空自己的想法之后,果断选择去和小迷妹聊会天,解放一下思路,放松一下紧绷的神经。当然,在放松之余,我也在想这个问题,只是会更放松。

总而言之,言而总之,所有的程序员们,当你写代码遇到问题遇到Bug,请找你的女神聊会天!!!



作者:xiaolyuh
链接:https://www.jianshu.com/p/b7635b08a6ce
来源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

<think>我们正在讨论Spring Boot集成Redis时的序列化反序列化配置。根据引用内容,特别是引用[3]和[4],我们可以详细解释如下: ### 1. 序列化反序列化的概念 - **序列化**:将Java对象转换为字节序列的过程,以便存储到Redis或通过网络传输[^4]。 - **反序列化**:将Redis中存储的字节序列重新转换为Java对象的过程[^4]。 ### 2. 默认序列化机制的问题 Spring Data Redis默认使用`JdkSerializationRedisSerializer`,它有以下缺点: - 序列化后的数据是二进制格式,可读性差 - 跨语言兼容性差(仅适用于Java) - 可能存在安全漏洞(如反序列化攻击) ### 3. 常用序列化方案 #### a) String序列化 - 适用场景:键(key)的序列化 - 实现:`StringRedisSerializer` - 特点:将字符串直接转换为字节数组(UTF-8编码) #### b) JSON序列化(推荐) - 实现:`GenericJackson2JsonRedisSerializer` - 优点: - 人类可读的文本格式 - 跨语言兼容 - 支持复杂对象 - 配置示例(引用[3]): ```java @Configuration public class RedisConfig { @Bean public RedisTemplate<String, Object> redisTemplate( RedisConnectionFactory redisConnectionFactory) { RedisTemplate<String, Object> template = new RedisTemplate<>(); template.setConnectionFactory(redisConnectionFactory); // 键使用字符串序列化 template.setKeySerializer(new StringRedisSerializer()); // 值使用JSON序列化 template.setValueSerializer(new GenericJackson2JsonRedisSerializer()); // Hash键序列化 template.setHashKeySerializer(new StringRedisSerializer()); // Hash值序列化 template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer()); return template; } } ``` - 依赖配置(引用[3]): ```xml <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.15.0</version> </dependency> ``` #### c) 其他方案 - `Jackson2JsonRedisSerializer`:需指定具体类型 - `GenericToStringSerializer`:通过Spring转换服务实现 ### 4. 泛型处理(引用[5]) 使用JSON序列化时,处理泛型集合需通过`TypeReference`: ```java List<User> users = redisTemplate.opsForValue() .get("users", new TypeReference<List<User>>() {}); ``` ### 5. 序列化配置原理 ```mermaid graph LR A[RedisTemplate] --> B[序列化器] B --> C[键序列化器] B --> D[值序列化器] B --> E[Hash键序列化器] B --> F[Hash值序列化器] ``` ### 6. 最佳实践 1. **键统一使用String序列化**:确保键的可读性和兼容性 2. **值使用JSON序列化**:平衡可读性和数据结构支持 3. **避免JDK默认序列化**:除非明确需要Java专属环境 4. **处理泛型时显式声明类型**:防止类型擦除问题[^5] ### 示例:用户对象存储 ```java @Data public class User { private Long id; private String name; } // 存储操作 User user = new User(1L, "Alice"); redisTemplate.opsForValue().set("user:1", user); // 读取操作 User cachedUser = (User) redisTemplate.opsForValue().get("user:1"); ``` > **警告**:使用`GenericJackson2JsonRedisSerializer`时,JSON中会包含类全限定名(如`"@class":"com.example.User"`),可能导致安全问题和存储开销。若需隐藏类型信息,可使用`Jackson2JsonRedisSerializer`并显式指定类型。 §§相关问题§§ 1. Spring Boot集成Redis时如何避免默认JDK序列化带来的安全问题? 2. 使用JSON序列化时如何解决类型擦除问题? 3. 如何为Redis的不同数据结构(String/Hash/List)配置不同的序列化策略? </think>### Spring Boot集成Redis序列化反序列化配置详解 #### 一、核心概念 1. **序列化(Serialization)** 将Java对象转换为字节序列的过程,便于存储到Redis或网络传输[^4]。 公式表示:$ \text{Object} \xrightarrow{\text{serialize}} \text{byte[]} $ 2. **反序列化(Deserialization)** 将Redis中的字节序列恢复为Java对象的过程[^4]。 公式表示:$ \text{byte[]} \xrightarrow{\text{deserialize}} \text{Object} $ #### 二、默认行为与问题 - **默认序列化器**:`JdkSerializationRedisSerializer` - **问题表现**: ```java // 默认序列化后的Redis数据(不可读) "\xac\xed\x00\x05t\x00\x05user:1" ``` - 产生二进制数据,可读性差[^3] - 跨语言兼容性差 - 存储空间较大(包含类元信息) #### 三、序列化方案对比 | 序列化器 | 特点 | 适用场景 | |----------------------------------|-----------------------------|---------------------| | `JdkSerializationRedisSerializer`| 默认,二进制格式 | 不推荐使用 | | `StringRedisSerializer` | 字符串格式,UTF-8编码 | **Key的序列化** | | `Jackson2JsonRedisSerializer` | JSON格式,需指定类型 | 简单对象序列化 | | `GenericJackson2JsonRedisSerializer` | JSON格式,记录类型信息 | **推荐方案**(通用场景)| #### 四、最佳配置实践(基于引用[3]) ```java @Configuration public class RedisConfig { @Bean public RedisTemplate<String, Object> redisTemplate( RedisConnectionFactory redisConnectionFactory) { RedisTemplate<String, Object> template = new RedisTemplate<>(); template.setConnectionFactory(redisConnectionFactory); // Key序列化:String格式 template.setKeySerializer(new StringRedisSerializer()); // Value序列化:JSON格式(带类型信息) template.setValueSerializer(new GenericJackson2JsonRedisSerializer()); // HashKey序列化 template.setHashKeySerializer(new StringRedisSerializer()); // HashValue序列化 template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer()); return template; } } ``` ##### 关键配置说明: 1. **Key序列化** 必须使用`StringRedisSerializer`,确保Key可读性: ```java redisTemplate.opsForValue().set("user:1001", userObj); // Key显示为"user:1001" ``` 2. **Value序列化** `GenericJackson2JsonRedisSerializer`会在JSON中添加类型元数据: ```json { "@class": "com.example.User", "id": 1001, "name": "Alice" } ``` 3. **必备依赖**(引用[3]): ```xml <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.15.0</version> </dependency> ``` #### 五、特殊场景处理 1. **泛型集合反序列化**(引用[5]) 使用`TypeReference`解决类型擦除问题: ```java List<User> users = redisTemplate.opsForValue() .get("users", new TypeReference<List<User>>() {}); ``` 2. **自定义序列化规则** 扩展`RedisSerializer`接口: ```java public class CustomSerializer implements RedisSerializer<MyObject> { @Override public byte[] serialize(MyObject obj) { // 自定义序列化逻辑 } @Override public MyObject deserialize(byte[] bytes) { // 自定义反序列化逻辑 } } ``` #### 六、配置效果对比 | 序列化方式 | Redis数据示例 | 可读性 | 跨语言支持 | |----------------|-------------------------------|-------|----------| | JDK默认 | `\xac\xed\x00\x05t...` | 差 | 不支持 | | String | `"name:Alice"` | 优 | 支持 | | **JSON(推荐)** | `{"@class":"...","name":"Alice"}` | 良 | 支持 | > **验证公式**:序列化前后对象一致性 > $$ \text{deserialize(serialize}(obj)) \equiv obj $$
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值