⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 blog.csdn.net/shenlf_bk/article/details/140846100 「亦碎流年」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注微信公众号:【芋道源码】有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言都将得到认真回复。甚至不知道如何读源码也可以请教噢。
  4. 新的源码解析文章实时收到通知。每周更新一篇左右。
  5. 认真的源码交流微信群。

一、背景

有三个项目:

项目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 字段。

文章目录
  1. 1. 一、背景
    1. 1.1. 1. 报错:
    2. 1.2. 2. redis3种序列化策略
      1. 1.2.0.1. 1. Jackson
      2. 1.2.0.2. 2. GenericJackson
      3. 1.2.0.3. 3. FastJson
  • 2. 二、解决方案:
    1. 2.1. 1. 统一使用一种序列化策略
    2. 2.2. 2. 使用objectMapper移除@type
    3. 2.3. 3. 解决步骤
      1. 2.3.0.1. 1) 在服务类中注入特定的 RedisTemplate 实例
      2. 2.3.0.2. 2 )移除@type
      3. 2.3.0.3. 关键点