和通义灵码聊聊spring-boot-data-redis使用过程中的困惑(序列化,反序列化,Jackson, JavaType, TypeReference)

1,130 阅读15分钟

背景

项目中用到Redis比较多,当前有一个痛点:在存储Redis时,不用ObjectMapper#enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY), 序列化后的JSON是干净的(没多增加@class属性),比如下面这样。

{
    "id": 1,
    "names": "james.h.fu"
}

如果增加了这个选项,那么JSON内容中会出现@class属性,比如下面这样

{
    "@class": "com.example.redis_demo.dto.SystemUserDTO",
    "id": 1,
    "names": "james.h.fu"
}

不启用这个选项,RedisTemplate<String, Object> 获取到的对象为LinkedHashMap<String, Object>, 会出现下面这个异常

java.lang.ClassCastException: class java.util.LinkedHashMap cannot be cast to class com.example.redis_demo.dto.SystemUserDTO (java.util.LinkedHashMap is in module java.base of loader 'bootstrap'; com.example.redis_demo.dto.SystemUserDTO is in unnamed module of loader 'app')

如果启用这个选项,RedisTemplate<String, Object> 能反序列化为对应的类型,但是,如果后面重构时,不小心把类型名称(比如改为SystemUserDTO2)改了,那么会出现这个异常 java.lang.ClassNotFoundException: com.example.redis_demo.dto.SystemUserDTO

在Spring-Boot-Data-redis中,RedisTemplate<String, Object>中的Jackson2JsonRedisSerializer<Object>为什么会将Object序列化存储为JSON,但是在读取时,又读成LinkedHashMap<String, Object>呢?

在使用 Spring-Boot-Data-Redis 时,如果你配置了 Jackson2JsonRedisSerializer<Object> 来序列化和反序列化 Redis 中的数据,可能会遇到读取时数据被转换为 LinkedHashMap<String, Object> 的情况。这是由于以下几个原因:

1. 类型信息丢失

  • 在 Java 中,泛型参数在运行时会被擦除(Type Erasure),因此当你将对象序列化为 JSON 并存储到 Redis 中时,实际存储的是一个 JSON 字符串,而没有保留具体的类型信息。
  • 当你从 Redis 中读取 JSON 字符串并反序列化时,默认情况下 Jackson 会根据 JSON 的结构将其解析为最合适的 Java 类型。对于复杂的嵌套结构,通常会解析为 LinkedHashMap<String, Object>

2. 默认反序列化行为

  • Jackson2JsonRedisSerializer<Object> 默认会将 JSON 反序列化为 MapList 等集合类型,而不是你期望的具体类型。这是因为 Object 是一个非常宽泛的类型,Jackson 无法确定具体的目标类型。

解决方案:

为了确保反序列化后的对象是正确的类型,你可以采取以下几种方法:

1. 传递类型信息

  • 通过显式传递 TypeReferenceClass<T> 来指定反序列化的目标类型。
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;

@Override
public <T> List<T> getAll(List<String> keys, Class<T> clazz) {
    List<Object> valueList = redisTemplate.opsForValue().multiGet(keys);
    
    if (CollectionUtils.isEmpty(valueList)) {
        return Collections.emptyList();
    }

    ObjectMapper objectMapper = new ObjectMapper();
    List<T> resultList = new ArrayList<>();
    for (Object object : valueList) {
        if (object != null) {
            try {
                // 假设 Redis 中存储的是 JSON 字符串
                String jsonString = objectMapper.writeValueAsString(object);
                T t = objectMapper.readValue(jsonString, clazz);
                resultList.add(t);
            } catch (IOException e) {
                throw new RuntimeException("Failed to deserialize object", e);
            }
        } else {
            resultList.add(null);
        }
    }
    return resultList;
}

2. 自定义 RedisTemplate 配置

  • 你可以自定义 RedisTemplate 的序列化器,并确保在反序列化时传递类型信息。
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.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import com.fasterxml.jackson.databind.ObjectMapper;

@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);

        // 使用 Jackson2JsonRedisSerializer 进行序列化和反序列化
        Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(Object.class);
        
        ObjectMapper objectMapper = new ObjectMapper();
        // 配置 ObjectMapper 以处理特定类型
        serializer.setObjectMapper(objectMapper);

        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(serializer);
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(serializer);
        template.afterPropertiesSet();

        return template;
    }
}

3. 使用 GenericJackson2JsonRedisSerializer

  • 如果你需要更灵活的类型处理,可以考虑使用 GenericJackson2JsonRedisSerializer,它会保留更多的类型信息。
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> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);

        // 使用 GenericJackson2JsonRedisSerializer 进行序列化和反序列化
        GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer();

        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(serializer);
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(serializer);
        template.afterPropertiesSet();

        return template;
    }
}

总结:

  • 类型擦除 导致序列化和反序列化时类型信息丢失。
  • 默认情况下,Jackson2JsonRedisSerializer<Object> 会将复杂对象反序列化为 LinkedHashMap<String, Object>
  • 通过传递类型信息、自定义 RedisTemplate 配置或使用 GenericJackson2JsonRedisSerializer,可以确保反序列化后的对象是正确的类型。

选择哪种方式取决于你的具体需求和应用场景。


具体如何”使用 GenericJackson2JsonRedisSerializer“,请你详细展开说一说呢

好的,使用 GenericJackson2JsonRedisSerializer 可以帮助你在 Redis 中存储和读取复杂对象时保留更多的类型信息。下面是详细的步骤和配置方法,确保你能够正确地使用它。

1. 引入依赖

首先,确保你的项目中已经包含了必要的依赖项。如果你使用的是 Spring Boot,默认情况下会包含 Jackson 和 Spring Data Redis 的依赖。如果没有,请在 pom.xmlbuild.gradle 中添加:

Maven:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
</dependency>

Gradle:

implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'com.fasterxml.jackson.core:jackson-databind'

2. 配置 RedisTemplate 使用 GenericJackson2JsonRedisSerializer

你需要自定义 RedisTemplate 的序列化器,以便在存储和读取数据时使用 GenericJackson2JsonRedisSerializer。这可以通过创建一个配置类来实现。

配置类示例:

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> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);

        // 使用 GenericJackson2JsonRedisSerializer 进行序列化和反序列化
        GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer();

        // 设置键(key)的序列化采用 StringRedisSerializer
        template.setKeySerializer(new StringRedisSerializer());

        // 设置值(value)的序列化采用 GenericJackson2JsonRedisSerializer
        template.setValueSerializer(serializer);

        // 设置 Hash 键(hash key)的序列化采用 StringRedisSerializer
        template.setHashKeySerializer(new StringRedisSerializer());

        // 设置 Hash 值(hash value)的序列化采用 GenericJackson2JsonRedisSerializer
        template.setHashValueSerializer(serializer);

        template.afterPropertiesSet();
        return template;
    }
}

3. 理解 GenericJackson2JsonRedisSerializer

GenericJackson2JsonRedisSerializer 是 Spring Data Redis 提供的一个序列化器,它基于 Jackson 库,可以将 Java 对象序列化为 JSON 字符串,并且在反序列化时尝试保留更多的类型信息。具体来说:

  • 序列化:将 Java 对象转换为 JSON 字符串,并附加类型信息。
  • 反序列化:从 JSON 字符串中读取并根据附加的类型信息还原为原始的 Java 对象。

4. 测试和验证

为了确保配置正确,你可以编写一个简单的测试用例来验证 GenericJackson2JsonRedisSerializer 的行为。

测试代码示例:

import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;

@SpringBootTest
public class RedisTemplateTest {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    @Autowired
    private ObjectMapper objectMapper;

    @Test
    public void testGenericJackson2JsonRedisSerializer() throws Exception {
        // 定义一个复杂对象
        MyComplexObject complexObject = new MyComplexObject("test", 123, List.of("item1", "item2"));

        // 存储到 Redis
        redisTemplate.opsForValue().set("complexObject", complexObject);

        // 从 Redis 读取
        Object result = redisTemplate.opsForValue().get("complexObject");

        // 打印结果,确保类型正确
        System.out.println(result.getClass()); // 应该输出 MyComplexObject.class
        System.out.println(result); // 应该输出完整的 MyComplexObject 内容

        // 验证是否是正确的类型
        if (result instanceof MyComplexObject) {
            MyComplexObject retrievedObject = (MyComplexObject) result;
            System.out.println(retrievedObject);
        } else {
            throw new RuntimeException("Deserialization failed");
        }
    }

    // 定义一个复杂对象类
    public static class MyComplexObject {
        private String name;
        private int age;
        private List<String> items;

        public MyComplexObject() {}

        public MyComplexObject(String name, int age, List<String> items) {
            this.name = name;
            this.age = age;
            this.items = items;
        }

        // Getters and Setters
        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public int getAge() {
            return age;
        }

        public void setAge(int age) {
            this.age = age;
        }

        public List<String> getItems() {
            return items;
        }

        public void setItems(List<String> items) {
            this.items = items;
        }

        @Override
        public String toString() {
            return "MyComplexObject{" +
                    "name='" + name + '\'' +
                    ", age=" + age +
                    ", items=" + items +
                    '}';
        }
    }
}

5. 注意事项

  • 性能影响GenericJackson2JsonRedisSerializer 会在序列化和反序列化时附加额外的类型信息,这可能会稍微增加一些开销。对于简单的键值对存储,可能不需要这么复杂的序列化器。
  • 兼容性:确保所有相关的类都有无参构造函数,因为 Jackson 在反序列化时需要调用它。
  • 异常处理:在实际应用中,建议添加适当的异常处理逻辑,以应对可能的序列化/反序列化失败情况。

总结

通过上述步骤,你可以成功配置 RedisTemplate 使用 GenericJackson2JsonRedisSerializer,从而在 Redis 中存储和读取复杂对象时保留更多的类型信息。这有助于避免反序列化时出现类型不匹配的问题,并确保数据的完整性和一致性。



在泛型方法中,运行时能获取到List中,T的Class吗?

在 Java 的泛型实现中,由于类型擦除(Type Erasure),编译后的字节码中泛型参数 T 的具体类型信息会被擦除,因此在运行时无法直接获取到 List<T>T 的具体 Class 信息。这意味着你不能直接通过反射或其他方式获取到 T 的实际类型。

解决方案:

如果你确实需要在运行时知道 T 的具体类型,可以通过以下几种方式来解决:

1. 传递类型信息

  • 你可以通过方法参数显式地传递 Class<T> 对象。这样可以在运行时使用该类对象进行类型检查或转换。
@Override
public <T> List<T> getAll(List<String> keys, Class<T> clazz) {
    List<Object> valueList = redisTemplate.opsForValue().multiGet(keys);
    
    if (CollectionUtils.isEmpty(valueList)) {
        return Collections.emptyList();
    }

    List<T> resultList = new ArrayList<>();
    for (Object object : valueList) {
        if (object != null) {
            // 使用 clazz 进行类型转换
            T t = clazz.cast(object);
            resultList.add(t);
        } else {
            resultList.add(null);
        }
    }
    return resultList;
}

2. 使用 TypeToken 或其他库

  • 如果你需要更复杂的类型处理(例如处理泛型类的子类型),可以考虑使用第三方库如 Google Guava 的 TypeToken,它可以帮助你在运行时保留更多的类型信息。
import com.google.common.reflect.TypeToken;

public class RedisServiceImpl {

    @Override
    public <T> List<T> getAll(List<String> keys, TypeToken<T> typeToken) {
        List<Object> valueList = redisTemplate.opsForValue().multiGet(keys);
        
        if (CollectionUtils.isEmpty(valueList)) {
            return Collections.emptyList();
        }

        List<T> resultList = new ArrayList<>();
        for (Object object : valueList) {
            if (object != null) {
                // 使用 TypeToken 进行类型转换
                T t = typeToken.cast(object);
                resultList.add(t);
            } else {
                resultList.add(null);
            }
        }
        return resultList;
    }
}

3. 使用序列化/反序列化

  • 如果你存储的是复杂对象,可以考虑将对象序列化为 JSON 或其他格式,然后在读取时根据类型信息进行反序列化。这通常需要结合 JacksonGson 等库来实现。
import com.fasterxml.jackson.databind.ObjectMapper;

public class RedisServiceImpl {

    private final ObjectMapper objectMapper = new ObjectMapper();

    @Override
    public <T> List<T> getAll(List<String> keys, Class<T> clazz) {
        List<Object> valueList = redisTemplate.opsForValue().multiGet(keys);
        
        if (CollectionUtils.isEmpty(valueList)) {
            return Collections.emptyList();
        }

        List<T> resultList = new ArrayList<>();
        for (Object object : valueList) {
            if (object != null) {
                try {
                    // 假设 Redis 中存储的是 JSON 字符串
                    String jsonString = (String) object;
                    T t = objectMapper.readValue(jsonString, clazz);
                    resultList.add(t);
                } catch (IOException e) {
                    throw new RuntimeException("Failed to deserialize object", e);
                }
            } else {
                resultList.add(null);
            }
        }
        return resultList;
    }
}

总结:

  • 类型擦除 导致在运行时无法直接获取 T 的具体类型。
  • 可以通过显式传递 Class<T> 或使用 TypeToken 来解决这个问题。
  • 对于复杂对象,可以考虑使用序列化/反序列化技术。 选择哪种方式取决于你的具体需求和应用场景。

com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException: Unrecognized field "@class" (class com.example.redis_demo.dto.SystemUserDTO), not marked as ignorable (2 known properties: "id", "name"])

根据你提供的异常信息,UnrecognizedPropertyException 表示 Jackson 在反序列化 JSON 数据到 SystemUserDTO 对象时遇到了一个未识别的字段 @class。这个字段在 SystemUserDTO 类中并不存在,且没有被标记为可忽略。

以下是几种解决方法:

  • 忽略未知属性

    如果你不关心 @class 字段,并希望忽略所有未知属性,可以在 ObjectMapper 中配置忽略未知属性:

    ObjectMapper objectMapper = new ObjectMapper();
    objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
    
  • 使用注解忽略特定类的未知属性

    如果只想对 SystemUserDTO 类忽略未知属性,可以在类上添加注解:

    @JsonIgnoreProperties(ignoreUnknown = true)
    public class SystemUserDTO {
        // 现有的属性和方法
    }
    
  • 添加 @class 属性

    如果你需要处理 @class 字段,可以考虑在 SystemUserDTO 类中添加相应的属性或 getter/setter 方法。不过通常情况下,@class 是由某些框架(如 Hibernate)自动生成的类型信息字段,直接添加可能不是最佳选择。

  • 检查输入 JSON 数据

    确认输入的 JSON 数据是否包含不必要的 @class 字段。如果可能的话,尝试修正数据源以移除该字段。

选择适合你应用场景的方法进行调整即可解决问题。


在使用Jackson时,如何获取到List的type

在使用 Jackson 反序列化 JSON 数据到 List<SystemUserDTO> 时,需要提供正确的类型信息,以便 Jackson 能够正确地将 JSON 数据转换为 List<SystemUserDTO> 对象。这可以通过 TypeReferenceJavaType 来实现。

以下是两种常见的方法:

方法一:使用 TypeReference

TypeReference 是 Jackson 提供的一个抽象类,用于捕获泛型类型信息。你可以通过创建一个匿名内部类来获取 List<SystemUserDTO> 的类型信息。

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.example.redis_demo.dto.SystemUserDTO;

import java.io.IOException;
import java.util.List;

public class Example {
    public static void main(String[] args) throws IOException {
        String jsonString = "[{\"id\":1,\"name\":\"james-1234567890\"},{\"id\":2,\"name\":\"james-9876543210\"}]";

        ObjectMapper objectMapper = new ObjectMapper();
        List<SystemUserDTO> systemUserDTOList = objectMapper.readValue(jsonString, new TypeReference<List<SystemUserDTO>>() {});

        // 处理 systemUserDTOList
        systemUserDTOList.forEach(System.out::println);
    }
}

方法二:使用 JavaType

JavaType 是 Jackson 提供的另一种类型表示方式。你可以通过 ObjectMappergetTypeFactory 方法来创建 JavaType 对象。

import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.example.redis_demo.dto.SystemUserDTO;

import java.io.IOException;
import java.util.List;

public class Example {
    public static void main(String[] args) throws IOException {
        String jsonString = "[{\"id\":1,\"name\":\"james-1234567890\"},{\"id\":2,\"name\":\"james-9876543210\"}]";

        ObjectMapper objectMapper = new ObjectMapper();
        JavaType javaType = objectMapper.getTypeFactory().constructParametricType(List.class, SystemUserDTO.class);
        List<SystemUserDTO> systemUserDTOList = objectMapper.readValue(jsonString, javaType);

        // 处理 systemUserDTOList
        systemUserDTOList.forEach(System.out::println);
    }
}

应用到你的测试方法

假设你在 RedisDemoApplicationTests 中有一个测试方法需要反序列化 JSON 数据到 List<SystemUserDTO>,可以参考以下示例:

@Test
void test_getListSystemUserDTO() throws JsonProcessingException {
    String jsonString = "[{\"id\":1,\"name\":\"james-1234567890\"},{\"id\":2,\"name\":\"james-9876543210\"}]";

    ObjectMapper objectMapper = SpringUtil.getBean(ObjectMapper.class);
    List<SystemUserDTO> systemUserDTOList = objectMapper.readValue(jsonString, new TypeReference<List<SystemUserDTO>>() {});

    // 处理 systemUserDTOList
    systemUserDTOList.forEach(systemUserDTO -> log.info("systemUserDTO: {}", systemUserDTO));
}

或者使用 JavaType

@Test
void test_getListSystemUserDTO() throws JsonProcessingException {
    String jsonString = "[{\"id\":1,\"name\":\"james-1234567890\"},{\"id\":2,\"name\":\"james-9876543210\"}]";

    ObjectMapper objectMapper = SpringUtil.getBean(ObjectMapper.class);
    JavaType javaType = objectMapper.getTypeFactory().constructParametricType(List.class, SystemUserDTO.class);
    List<SystemUserDTO> systemUserDTOList = objectMapper.readValue(jsonString, javaType);

    // 处理 systemUserDTOList
    systemUserDTOList.forEach(systemUserDTO -> log.info("systemUserDTO: {}", systemUserDTO));
}

这两种方法都可以有效地获取 List<SystemUserDTO> 的类型信息,选择其中一种即可。


在使用Jackson时,如何获取到Map<String , Object>的type

在使用 Jackson 反序列化 JSON 数据到 Map<String, Object> 时,不需要特别指定类型信息,因为 Map<String, Object> 是一个通用的类型,Jackson 可以直接处理。不过,为了确保类型信息正确,你可以使用 TypeReferenceJavaType,尽管对于 Map<String, Object> 这种简单类型,通常直接使用 Map.class 即可。

以下是几种方法来反序列化 JSON 数据到 Map<String, Object>

方法一:直接使用 Map.class

这是最简单的方法,适用于大多数情况。

import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.util.Map;

public class Example {
    public static void main(String[] args) throws IOException {
        String jsonString = "{\"id\":1,\"name\":\"james-1234567890\"}";

        ObjectMapper objectMapper = new ObjectMapper();
        Map<String, Object> map = objectMapper.readValue(jsonString, Map.class);

        // 处理 map
        map.forEach((key, value) -> System.out.println(key + ": " + value));
    }
}

方法二:使用 TypeReference

虽然对于 Map<String, Object> 直接使用 Map.class 即可,但如果你习惯使用 TypeReference,也可以这样做。

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.util.Map;

public class Example {
    public static void main(String[] args) throws IOException {
        String jsonString = "{\"id\":1,\"name\":\"james-1234567890\"}";

        ObjectMapper objectMapper = new ObjectMapper();
        Map<String, Object> map = objectMapper.readValue(jsonString, new TypeReference<Map<String, Object>>() {});

        // 处理 map
        map.forEach((key, value) -> System.out.println(key + ": " + value));
    }
}

方法三:使用 JavaType

同样,对于 Map<String, Object> 直接使用 Map.class 即可,但如果你需要更复杂的类型处理,可以使用 JavaType

import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.util.Map;

public class Example {
    public static void main(String[] args) throws IOException {
        String jsonString = "{\"id\":1,\"name\":\"james-1234567890\"}";

        ObjectMapper objectMapper = new ObjectMapper();
        JavaType javaType = objectMapper.getTypeFactory().constructMapType(Map.class, String.class, Object.class);
        Map<String, Object> map = objectMapper.readValue(jsonString, javaType);

        // 处理 map
        map.forEach((key, value) -> System.out.println(key + ": " + value));
    }
}

应用到你的测试方法

假设你在 RedisDemoApplicationTests 中有一个测试方法需要反序列化 JSON 数据到 Map<String, Object>,可以参考以下示例:

@Test
void test_getMapAsString() throws JsonProcessingException {
    long id = 1L;
    String object = stringRedisTemplate.opsForValue().get("mymap:" + id);
    if (object != null) {
        ObjectMapper objectMapper = SpringUtil.getBean(ObjectMapper.class);
        Map<String, Object> map = objectMapper.readValue(object, Map.class);

        // 处理 map
        map.forEach((key, value) -> log.info("{}: {}", key, value));
    }
}

或者使用 TypeReference

@Test
void test_getMapAsString() throws JsonProcessingException {
    long id = 1L;
    String object = stringRedisTemplate.opsForValue().get("mymap:" + id);
    if (object != null) {
        ObjectMapper objectMapper = SpringUtil.getBean(ObjectMapper.class);
        Map<String, Object> map = objectMapper.readValue(object, new TypeReference<Map<String, Object>>() {});

        // 处理 map
        map.forEach((key, value) -> log.info("{}: {}", key, value));
    }
}

或者使用 JavaType

@Test
void test_getMapAsString() throws JsonProcessingException {
    long id = 1L;
    String object = stringRedisTemplate.opsForValue().get("mymap:" + id);
    if (object != null) {
        ObjectMapper objectMapper = SpringUtil.getBean(ObjectMapper.class);
        JavaType javaType = objectMapper.getTypeFactory().constructMapType(Map.class, String.class, Object.class);
        Map<String, Object> map = objectMapper.readValue(object, javaType);

        // 处理 map
        map.forEach((key, value) -> log.info("{}: {}", key, value));
    }
}

对于 Map<String, Object>,直接使用 Map.class 是最简单和推荐的方式。如果你有更复杂的类型需求,可以考虑使用 TypeReferenceJavaType


总结: 参考Redission源码,对相关get/getAll方法进行增强

  1. 引入StringRedisTemplate: 在写入时,用RedisTemplate<String, Object>Jackson2JsonRedisSerializer<String, Object>将对象序列化为JSON。在读取时,将JSON读取为String, 然后使用ObjectMapper的JavaType/TypeReference/Class<T>实现反序列化。
  2. 使用泛型方法。
@Service
public class RedisServiceImpl implements RedisService {
    private static final int MULTI_GET_BATCH_SIZE = 20;

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Autowired
    private ObjectMapper objectMapper;


@Override
@SneakyThrows(JsonProcessingException.class)
public <T> T get(String key, Class<T> itemType) {
    String value = stringRedisTemplate.opsForValue().get(key);
    if (value == null) {
        return null;
    } else {
        return objectMapper.readValue(value, itemType);
    }
}


@Override
@SneakyThrows(JsonProcessingException.class)
public <T> T get(String key, TypeReference<T> itemType) {
    String value = stringRedisTemplate.opsForValue().get(key);
    if (value == null) {
        return null;
    } else {
        return objectMapper.readValue(value, itemType);
    }
}

@Override
public String get(String key) {
    return stringRedisTemplate.opsForValue().get(key);
}

@Override
@SneakyThrows(JsonProcessingException.class)
public <T> List<T> getAll(List<String> keys, Class<T> listItemType) {
    JavaType listT = objectMapper.getTypeFactory().constructType(listItemType);
    return getAllToList(keys, listT);
}

@Override
@SneakyThrows(JsonProcessingException.class)
public <T> List<T> getAll(List<String> keys, TypeReference<T> listItemType) {
    JavaType listT = objectMapper.getTypeFactory().constructType(listItemType);
    return getAllToList(keys, listT);
}

private <T> List<T> getAllToList(List<String> keys, JavaType listItemType) throws JsonProcessingException {
    if (CollectionUtils.isEmpty(keys)) {
        return Collections.emptyList();
    }

    List<String> valueList = stringRedisTemplate.opsForValue().multiGet(keys);
    assert valueList != null;

    List<T> resultList = new ArrayList<>();
    for (String value : valueList) {
        if (value != null) {
            T t = objectMapper.readValue(value, listItemType);
            resultList.add(t);
        } else {
            resultList.add(null);
        }
    }
    return resultList;
}

@Override
public List<String> getAll(List<String> keys) {
    if (CollectionUtils.isEmpty(keys)) {
        return Collections.emptyList();
    }
    return stringRedisTemplate.opsForValue().multiGet(keys);
}

@Override
public Map<String, String> getAllToMap(List<String> keys) {
    if (CollectionUtils.isEmpty(keys)) {
        return Collections.emptyMap();
    }
    float loadFactor = 0.75f;
    int initialCapacity = (int) (keys.size() / loadFactor) + 3;
    Map<String, String> resultMap;

    if (keys.size() > MULTI_GET_BATCH_SIZE) {
        resultMap = new ConcurrentHashMap<>(initialCapacity, loadFactor);
        List<List<String>> partitionList = ListUtil.partition(keys, MULTI_GET_BATCH_SIZE);
        List<CompletableFuture<Void>> taskList = new ArrayList<>(partitionList.size());
        for (List<String> subList : partitionList) {
            CompletableFuture<Void> future = CompletableFuture.runAsync(() -> fillKeyValueToResultMap(subList, resultMap));
            taskList.add(future);
        }
        // 等待所有任务完成
        CompletableFuture<Void> allOf = CompletableFuture.allOf(taskList.toArray(new CompletableFuture[0]));
        allOf.join();
    } else {
        resultMap = new HashMap<>(initialCapacity, loadFactor);
        fillKeyValueToResultMap(keys, resultMap);
    }


    return resultMap;
}

private void fillKeyValueToResultMap(List<String> keyList, Map<String, String> resultMap) {
    List<String> valueList = getAll(keyList);
    if (CollectionUtils.isEmpty(valueList)) {
        for (String key : keyList) {
            resultMap.put(key, null);
        }
    } else {
        for (int i = 0; i < keyList.size(); i++) {
            String key = keyList.get(i);
            String value = valueList.get(i);
            resultMap.put(key, value);
        }
    }
}

附表

不启用ObjectMapper#enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY)的完整代码段
public RedisSerializer<?> redisSerializer() {
        /**
         *  方式一
         * 采用Jackson自己实现的 GenericJackson2JsonRedisSerializer
         * 当类型来自不被信任的源的时候会通过反序列化配置中基本配置中的PolymorphicTypeValidator 进行有效性校验,以预防安全问题.
         * 如果后期项目架构调整,包的类型调整那么将无法从redis里面读取
         * example:{"@class":"com.lenovo.nec.paapi.model.mtmbuilder.BuilderReqDto","materialNumber":null,"storeCode":"10001","couponCode":"ablccc","selectedProduct":null,"mtm":null,"online":true}
         */
        ObjectMapper om = new ObjectMapper();
        //空对象{} 序列化报错
        om.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
        // 只访问field
        om.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
        //如果是null则过滤掉
        //objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
        // om.registerModule(new JavaTimeModule());
        // om.registerModule(new SimpleModule());
        // 不要使用om.enableDefaultTyping这个会有安全问题
//        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);

        return new Jackson2JsonRedisSerializer<>(om, Object.class);
    }

在JDK17环境下,下面这个泛型方法,写的怎么样?还能改进吗?

在JDK17环境下,下面这个泛型方法,写的怎么样?还能改进吗?

这段代码在JDK 17环境下是可行的,但可以进行一些改进以提高代码的可读性、性能和安全性。以下是改进建议:

1. 避免不必要的 null 添加

  • objectnull 时,直接添加 null 到结果列表中可能会导致后续处理时出现空指针异常或不必要的逻辑复杂性。建议考虑是否真的需要保留这些 null 值,或者可以通过其他方式处理。

2. 使用流式编程(Stream API)

  • JDK 8 引入了 Stream API,可以简化集合操作。使用 Stream 可以使代码更简洁且更具表达力。

3. 移除不必要的 @SuppressWarnings("unchecked")

  • 通过改进泛型的使用,可以避免使用 @SuppressWarnings("unchecked") 注解,从而减少潜在的类型安全问题。

4. 优化空检查

  • 使用 Objects.requireNonNullElseGetOptional 来处理可能为空的情况,使代码更加健壮。

5. 考虑并发场景

  • 如果 redisTemplate.opsForValue().multiGet(keys) 返回的结果较大,考虑使用并行流来加速处理。

改进后的代码示例:

@Override
public <T> List<T> getAll(List<String> keys) {
    List<Object> valueList = redisTemplate.opsForValue().multiGet(keys);
    
    if (CollectionUtils.isEmpty(valueList)) {
        return Collections.emptyList();
    }

    // 使用 Stream API 进行转换,避免显式的类型转换警告
    return valueList.stream()
                    .map(object -> object != null ? (T) object : null)
                    .collect(Collectors.toList());
}

进一步优化:

如果不需要保留 null 值,可以进一步简化:

@Override
public <T> List<T> getAll(List<String> keys) {
    List<Object> valueList = redisTemplate.opsForValue().multiGet(keys);
    
    if (CollectionUtils.isEmpty(valueList)) {
        return Collections.emptyList();
    }

    // 过滤掉 null 值,并进行类型转换
    return valueList.stream()
                    .filter(Objects::nonNull)
                    .map(object -> (T) object)
                    .collect(Collectors.toList());
}

总结:

  • 使用 Stream API 简化代码逻辑。

  • 避免不必要的 null 值处理。

  • 移除不必要的类型转换警告。

  • 提高代码的可读性和维护性。