来源:blog.csdn.net/shenlf_bk/article/details/140846100
👉 欢迎加入小哈的星球,你将获得: 专属的项目实战 / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论
新项目:《从零手撸:仿小红书(微服务架构)》 正在持续爆肝中,基于 Spring Cloud Alibaba + Spring Boot 3.x + JDK 17..., 点击查看项目介绍; 《从零手撸:前后端分离博客项目(全栈开发)》 2期已完结,演示链接:http://116.62.199.48/; 截止目前,累计输出 73w+ 字,讲解图 3088+ 张,还在持续爆肝中.. 后续还会上新更多项目,目标是将 Java 领域典型的项目都整一波,如秒杀系统, 在线商城, IM 即时通讯,Spring Cloud Alibaba 等等,戳我加入学习,解锁全部项目,已有2500+小伙伴加入
一、背景 1.报错: 2.redis3种序列化策略 二、解决方案: 1.统一使用一种序列化策略 2.使用objectMapper移除@type 3.解决步骤
一、背景
有三个项目:
项目A通过redis 存放传输记录
redis序列化策略使用:FastJson2JsonRedisSerializeri
,若新增数据时数据中包含@type
项目B负责更新redis中的传输记录
redis序列化策略使用:Jackson2JsonRedisSerializer
,若新增数据时数据中包含@class
1. 报错:
由于使用FastJson进行的序列化,使用的 Jackson 进行反序列化时,反序列化时缺少类型信息
这行代码报错:
UserNfsTransferRecord record = (UserNfsTransferRecord) redisTemplate.opsForHash().get(redisKey, key);
报错信息:
org.springframework.data.redis.serializer.SerializationException: Could not read JSON: Could not resolve subtype of [simple type, class java.lang.Object]: missing type id property '@class' at [Source: (byte[])"{"@type":"com.ly.docker.userins.domain.UserNfsTransferRecord","creatTime":"2024-07-30 16:37:11","fileKey":"b1eff874-568d-4dbd-9b3c-af6c169d0bca8687","fileName":"头像3.webp","fileSize":11596,"isValid":1,"regionId":8,"status":2,"updateTime":"2024-07-30 16:37:11","userId":8687}"; line: 1, column: 277]; nested exception is com.fasterxml.jackson.databind.exc.InvalidTypeIdException: Could not resolve subtype of [simple type, class java.lang.Object]: missing type id property '@class' at [Source: (byte[])"{"@type":"com.ly.docker.userins.domain.UserNfsTransferRecord","creatTime":"2024-07-30 16:37:11","fileKey":"b1eff874-568d-4dbd-9b3c-af6c169d0bca8687","fileName":"头像3.webp","fileSize":11596,"isValid":1,"regionId":8,"status":2,"updateTime":"2024-07-30 16:37:11","userId":8687}"; line: 1, column: 277] at org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer.deserialize(Jackson2JsonRedisSerializer.java:75) at org.springframework.data.redis.core.AbstractOperations.deserializeHashValue(AbstractOperations.java:380) at org.springframework.data.redis.core.DefaultHashOperations.get(DefaultHashOperations.java:57) at com.ly.service.UserNfsTransferRecordService.getOneRecord(UserNfsTransferRecordService.java:67) UserNfsTransferRecord retrievedRecord = (UserNfsTransferRecord) redisTemplate.opsForHash().get(redisKey, fileKey);
2. redis3种序列化策略
Jackson2JsonRedisSerializer
和 GenericJackson2JsonRedisSerializer
都是 Spring Data Redis 中用于序列化和反序列化的工具,它们的主要区别如下:
1. Jackson
库: 使用 Jackson 库。 类型安全: Jackson2JsonRedisSerializer
在序列化和反序列化时需要指定目标类型。代码示例: Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(Object.class);
使用场景: 更适合用于类型已知的场景,序列化和反序列化都需要知道具体的 Java 类型。 缺点: 对多类型对象处理不便,需要显式指定类型。
数据举例:
{"@class":"com.ly.pojo.domain.UserNfsTransferRecord","regionId":8,"userId":1842,"fileName":"node-v14.21.3-x64 (7).zip","fileSize":29266106,"fileKey":"d0132f3d-3051-4e60-816d-3dc18b99c56f1842","status":2,"creatTime":"2024-07-23 16:11:40","updateTime":"2024-07-23 16:11:40","isValid":1
2. GenericJackson
库: 使用 Jackson 库。 泛型支持: GenericJackson2JsonRedisSerializer
可以处理任意类型的对象,而不需要在每次序列化和反序列化时指定类型。代码示例: GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer();
使用场景: 更加通用,适合于不确定对象类型或者需要处理多种类型对象的场景。 优点: 更加通用,适合处理多种类型对象的场景。 缺点: 序列化数据中包含类型信息,增加了一些开销。
数据举例:
{"@class":"com.ly.pojo.domain.UserNfsTransferRecord","regionId":8,"userId":1842,"fileName":"node-v14.21.3-x64 (7).zip","fileSize":29266106,"fileKey":"d0132f3d-3051-4e60-816d-3dc18b99c56f1842","status":2,"creatTime":"2024-07-23 16:11:40","updateTime":"2024-07-23 16:11:40","isValid":1}
3. FastJson
库: 使用 Fastjson 库。 性能: Fastjson 通常比 Jackson 更快,尤其在序列化和反序列化大批量数据时性能更好。 代码示例: FastJson2JsonRedisSerializer<Object> serializer = new FastJson2JsonRedisSerializer<>(Object.class);
优点: 性能较好,序列化和反序列化速度较快。 缺点: 安全性问题,Fastjson 曾经暴露出一些安全漏洞;而且社区维护和更新频率较低。
{"@type":"com.ly.docker.userins.domain.UserNfsTransferRecord","creatTime":"2024-07-30 17:08:45","fileKey":"04aa2c2d-0ceb-43e2-a5fc-713feabe57e31842","fileName":"Feige.dmg","fileSize":15407187,"isValid":1,"regionId":8,"status":2,"updateTime":"2024-07-30 17:08:45","userId":1842
二、解决方案:
写在前面:报错是在反序列化时出错的,两个项目的实体类的地址还不一样,GenericJackson
和Jackson解析的都是@class。其实只用在反序列化时移除@type即可
1. 统一使用一种序列化策略
使用 Jackson
、GenericJackson
、FastJson
种的一种进行序列化和反序列化
弊端:两个项目的实体类的地址需要一样
2. 使用objectMapper移除@type
忽略未知属性: 通过配置 ObjectMapper
,忽略未知属性。(这个方法修改的是RedisConfig需要统一序列化策略)
手动移除 @type 字段: 在反序列化之前,手动移除 @type 字段。
自动忽略@type字段: 通过配置 ObjectMapper
,忽略未知属性(不是修改的RedisConfig)
3. 解决步骤
我在不影响现有 Jackson 序列化配置的情况下新增一个基于 GenericJackson
的 Redis 配置,可以创建一个新的 RedisTemplate
实例,使用 GenericJackson
作为序列化器。这样可以在项目中同时使用不同的序列化策略,而不相互干扰。
PS:实际上不用新增,直接用原来的就行,这里我没弄清楚,可忽略
@Configuration
@EnableCaching
@Slf4j
public class RedisConfig extends CachingConfigurerSupport {
@Bean
@Primary //在原有方法上新增这个注解,原始代码默认调用这个redisTemplate
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(connectionFactory);
Jackson2JsonRedisSerializer serializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper mapper = new ObjectMapper();
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
serializer.setObjectMapper(mapper);
// 使用StringRedisSerializer来序列化和反序列化redis的key值
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(serializer);
// Hash的key也采用StringRedisSerializer的序列化方式
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(serializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
@Bean(name = "generiRedisTemplate") //新增的generiRedisTemplate策略配置
public RedisTemplate<String, Object> generiRedisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(connectionFactory);
ObjectMapper objectMapper = new ObjectMapper();
// Configure ObjectMapper if needed
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer(objectMapper);
// 使用StringRedisSerializer来序列化和反序列化redis的key值
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(serializer);
// Hash的key也采用StringRedisSerializer的序列化方式
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(serializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
要确保你使用的是 generiRedisTemplate
而不是 redisTemplate
,可以通过以下几种方法来调用和区分不同的 RedisTemplate
实例
可以通过 @Qualifier
注解来区分和注入不同的 RedisTemplate
实例。
1) 在服务类中注入特定的 RedisTemplate 实例
@Bean 注解
默认行为是使用方法名作为 Bean 名称。因此,确保你为每个 RedisTemplate
实例的方法指定了不同的名称,以避免 Bean 名称冲突。如果需要,可以显式指定 Bean 名称,如下所示:
@Bean(name = "generiRedisTemplate")
public RedisTemplate<String, Object> generiRedisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
//........
}
@Qualifier 注解
区分和注入不同的 RedisTemplate
实例。首先,为每个 RedisTemplate
实例定义一个唯一的 Bean 名称。
@Autowired
@Qualifier("generiRedisTemplate")
private RedisTemplate<String,Object> generiRedisTemplate;
@Primary 注解
当有多个 Bean 实现了相同的接口(例如 RedisTemplate
),@Primary
注解用于指定默认的 Bean。如果一个 Bean 被标记为 @Primary
,Spring 会优先注入这个 Bean。
如果你只需要一个主要的 RedisTemplate
实例而没有特殊的区分要求,可以使用 @Primary
注解来指定默认的 RedisTemplate
实例。然而,既然你希望同时支持多个 RedisTemplate
实例,并且需要明确区分它们,@Qualifier
是更合适的选择。
2 )移除@type
1. 忽略未知属性
在RedisConfig中配置。
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.DeserializationFeature;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> genericJacksonRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
GenericJackson2JsonRedisSerializer genericJacksonSerializer = new GenericJackson2JsonRedisSerializer(objectMapper);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(genericJacksonSerializer);
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(genericJacksonSerializer);
template.setConnectionFactory(redisConnectionFactory);
return template;
}
}
2. 手动移除
/**
* 从 Redis 中获取记录列表
*
* @return 记录列表
*/
public List<UserNfsTransferRecord> getRecords(Long userId, Long regionId) {
String redisKey = REDIS_KEY_PREFIX + userId + ":" + regionId;
ObjectMapper objectMapper = new ObjectMapper();
// 从 Redis 中获取数据 结果类型是LinkedHashMap
Map<Object, Object> recordsMap = generiRedisTemplate.opsForHash().entries(redisKey);
// 将 LinkedHashMap 转换为 UserNfsTransferRecord 对象
return recordsMap.values().stream()
.map(value -> {
try {
// 将值转换为 ObjectNode
ObjectNode objectNode = objectMapper.convertValue(value, ObjectNode.class);
// 移除 @type 字段
objectNode.remove("@type");
// 将 ObjectNode 转换为 UserNfsTransferRecord 对象
return objectMapper.treeToValue(objectNode, UserNfsTransferRecord.class);
} catch (Exception e) {
// 处理转换异常
e.printStackTrace();
return null;
}
})
.filter(record -> record != null) // 过滤掉转换失败的记录
.collect(Collectors.toList());
}
3. 忽略未知属性
在业务逻辑中配置,可以通过自定义 ObjectMapper
配置来忽略 @type 字段,而不是在每个 LinkedHashMap
中手动移除它。可以使用 DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES
来忽略未知属性。以下是如何实现这个目标:
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.Map;
import java.util.List;
import java.util.stream.Collectors;
/**
* 从 Redis 中获取记录列表
*
* @return 记录列表
*/
public List<UserNfsTransferRecord> getRecords(Long userId, Long regionId) {
String redisKey = REDIS_KEY_PREFIX + userId + ":" + regionId;
// 配置 ObjectMapper 忽略未知属性
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
// 从 Redis 中获取数据 结果类型是LinkedHashMap
Map<Object, Object> recordsMap = generiRedisTemplate.opsForHash().entries(redisKey);
// 将 LinkedHashMap 转换为 UserNfsTransferRecord 对象
return recordsMap.values().stream()
.map(value -> {
try {
// 直接将 LinkedHashMap 转换为 UserNfsTransferRecord 对象
return objectMapper.convertValue(value, UserNfsTransferRecord.class);
} catch (Exception e) {
// 处理转换异常
e.printStackTrace();
return null;
}
})
.filter(record -> record != null) // 过滤掉转换失败的记录
.collect(Collectors.toList());
}
关键点
配置 ObjectMapper
忽略未知属性:
使用 objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
来忽略在 JSON 中但未在目标类中定义的属性。这将自动忽略 @type 字段。
直接转换为目标对象:
使用 objectMapper.convertValue(value, UserNfsTransferRecord.class)
直接将 LinkedHashMap
转换为 UserNfsTransferRecord
对象,而不需要手动移除 @type 字段。
👉 欢迎加入小哈的星球,你将获得: 专属的项目实战 / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论
新项目:《从零手撸:仿小红书(微服务架构)》 正在持续爆肝中,基于 Spring Cloud Alibaba + Spring Boot 3.x + JDK 17..., 点击查看项目介绍; 《从零手撸:前后端分离博客项目(全栈开发)》 2期已完结,演示链接:http://116.62.199.48/; 截止目前,累计输出 73w+ 字,讲解图 3088+ 张,还在持续爆肝中.. 后续还会上新更多项目,目标是将 Java 领域典型的项目都整一波,如秒杀系统, 在线商城, IM 即时通讯,Spring Cloud Alibaba 等等,戳我加入学习,解锁全部项目,已有2500+小伙伴加入
最近面试BAT,整理一份面试资料《Java面试BATJ通关手册》,覆盖了Java核心技术、JVM、Java并发、SSM、微服务、数据库、数据结构等等。
获取方式:点“在看”,关注公众号并回复 Java 领取,更多内容陆续奉上。
PS:因公众号平台更改了推送规则,如果不想错过内容,记得读完点一下“在看”,加个“星标”,这样每次新文章推送才会第一时间出现在你的订阅列表里。
点“在看”支持小哈呀,谢谢啦