| | |
| | | package cn.iocoder.yudao.framework.redis.config; |
| | | |
| | | import cn.iocoder.yudao.framework.common.util.json.JsonUtils; |
| | | import cn.hutool.core.util.StrUtil; |
| | | import org.redisson.spring.starter.RedissonAutoConfigurationV4; |
| | | import org.springframework.boot.autoconfigure.AutoConfiguration; |
| | | import org.springframework.context.annotation.Bean; |
| | | import org.springframework.data.redis.connection.RedisConnectionFactory; |
| | | import org.springframework.data.redis.core.RedisTemplate; |
| | | import org.springframework.data.redis.serializer.RedisSerializer; |
| | | |
| | | import java.nio.charset.StandardCharsets; |
| | | import java.util.HashSet; |
| | | import java.util.LinkedHashSet; |
| | | import java.util.Set; |
| | | import java.util.TreeSet; |
| | | |
| | | import tools.jackson.core.type.TypeReference; |
| | | import tools.jackson.databind.ObjectMapper; |
| | | |
| | | /** |
| | | * Redis 配置类 |
| | |
| | | return template; |
| | | } |
| | | |
| | | @SuppressWarnings("UnnecessaryLocalVariable") |
| | | public static RedisSerializer<?> buildRedisSerializer() { |
| | | // 使用自定义序列化器,解决 Set/List 等 Collection 反序列化类型问题 |
| | | return new TypedJsonRedisSerializer(); |
| | | } |
| | | |
| | | /** |
| | | * 自定义 Redis 序列化器,解决 Set/List 等 Collection 反序列化类型问题 |
| | | */ |
| | | private static class TypedJsonRedisSerializer implements RedisSerializer<Object> { |
| | | |
| | | private static final String JAVA_TYPE_PREFIX = "@class:"; |
| | | private static final ObjectMapper OBJECT_MAPPER = JsonUtils.getObjectMapper(); |
| | | private static final TypeReference<HashSet<Object>> HASH_SET_TYPE = new TypeReference<>() {}; |
| | | private static final TypeReference<LinkedHashSet<Object>> LINKED_HASH_SET_TYPE = new TypeReference<>() {}; |
| | | private static final TypeReference<TreeSet<Object>> TREE_SET_TYPE = new TypeReference<>() {}; |
| | | |
| | | @Override |
| | | public byte[] serialize(Object value) { |
| | | if (value == null) { |
| | | return new byte[0]; |
| | | } |
| | | // 将类型信息与 JSON 一起存储 |
| | | String json = JsonUtils.toJsonString(value); |
| | | String typePrefix = JAVA_TYPE_PREFIX + value.getClass().getName() + ":"; |
| | | return (typePrefix + json).getBytes(StandardCharsets.UTF_8); |
| | | } |
| | | |
| | | @Override |
| | | public Object deserialize(byte[] bytes) { |
| | | if (bytes == null || bytes.length == 0) { |
| | | return null; |
| | | } |
| | | String content = new String(bytes, StandardCharsets.UTF_8); |
| | | if (!content.startsWith(JAVA_TYPE_PREFIX)) { |
| | | // 兼容旧数据:没有类型信息的情况 |
| | | // 检测是否为 JSON 数组,若是则转换为 HashSet |
| | | return deserializeLegacyData(content); |
| | | } |
| | | // 提取类型信息和 JSON |
| | | int colonIndex = content.indexOf(':', JAVA_TYPE_PREFIX.length()); |
| | | if (colonIndex == -1) { |
| | | return deserializeLegacyData(content); |
| | | } |
| | | String className = content.substring(JAVA_TYPE_PREFIX.length(), colonIndex); |
| | | String json = content.substring(colonIndex + 1); |
| | | try { |
| | | Class<?> clazz = Class.forName(className); |
| | | // 特殊处理 Set 类型,因为 Jackson 默认会将 JSON 数组反序列化为 List |
| | | return deserializeWithTypeHint(json, clazz); |
| | | } catch (ClassNotFoundException e) { |
| | | // 类型不存在时,尝试兼容解析 |
| | | return deserializeLegacyData(json); |
| | | } catch (Exception e) { |
| | | // 解析失败时,尝试兼容解析 |
| | | return deserializeLegacyData(json); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 兼容旧数据(没有类型前缀)的解析 |
| | | * 对于 JSON 数组,默认转换为 HashSet |
| | | */ |
| | | private Object deserializeLegacyData(String content) { |
| | | if (StrUtil.isEmpty(content)) { |
| | | return null; |
| | | } |
| | | content = content.trim(); |
| | | // 判断是否为 JSON 数组 |
| | | if (content.startsWith("[") && content.endsWith("]")) { |
| | | try { |
| | | // 数组类型默认转为 HashSet |
| | | return OBJECT_MAPPER.readValue(content, HASH_SET_TYPE); |
| | | } catch (Exception e) { |
| | | return JsonUtils.parseObject(content, Object.class); |
| | | } |
| | | } |
| | | // 其他类型直接解析 |
| | | return JsonUtils.parseObject(content, Object.class); |
| | | } |
| | | |
| | | private Object deserializeWithTypeHint(String json, Class<?> clazz) { |
| | | // 处理 Set 接口的实现类 |
| | | if (HashSet.class.isAssignableFrom(clazz) || |
| | | "java.util.Collections$UnmodifiableSet".equals(clazz.getName()) || |
| | | "java.util.Collections$SingletonSet".equals(clazz.getName()) || |
| | | Set.class.isAssignableFrom(clazz)) { |
| | | return OBJECT_MAPPER.readValue(json, HASH_SET_TYPE); |
| | | } |
| | | if (LinkedHashSet.class.isAssignableFrom(clazz)) { |
| | | return OBJECT_MAPPER.readValue(json, LINKED_HASH_SET_TYPE); |
| | | } |
| | | if (TreeSet.class.isAssignableFrom(clazz)) { |
| | | return OBJECT_MAPPER.readValue(json, TREE_SET_TYPE); |
| | | } |
| | | // 使用 Jackson 的 readValue 指定类型,确保 POJO 正确反序列化 |
| | | return OBJECT_MAPPER.readValue(json, clazz); |
| | | } |
| | | RedisSerializer<Object> json = RedisSerializer.json(); |
| | | // 特殊:spring boot 4.x 无需解决 LocalDateTime 的序列化 |
| | | // 原因:Spring Data Redis 4 使用 Jackson 3,RedisSerializer.json() 已支持 Java Time 类型 |
| | | return json; |
| | | } |
| | | |
| | | } |