spring-projects / spring-boot

Spring Boot
https://spring.io/projects/spring-boot
Apache License 2.0
74.57k stars 40.55k forks source link

Redis Cache With GenericJackson2JsonRedisSerializer throws ClassCastException: class java.util.LinkedHashMap cannot be cast to class X #27577

Closed sivaprasadreddy closed 3 years ago

sivaprasadreddy commented 3 years ago

I am trying out basic example of using Redis as Cache provider and trying to customise it to use GenericJackson2JsonRedisSerializer instead of default JdkSerializationRedisSerializer.

When I call the same method which is cached 2nd time then it is throwing error with following stacktrace:

java.lang.ClassCastException: class java.util.LinkedHashMap cannot be cast to class com.example.rediscachedemo.BookmarkDTO (java.util.LinkedHashMap is in module java.base of loader 'bootstrap'; com.example.rediscachedemo.BookmarkDTO is in unnamed module of loader 'app')
    at com.example.rediscachedemo.BookmarkService$$EnhancerBySpringCGLIB$$d8594d86.getBookmarkById(<generated>) ~[classes/:na]
    at com.example.rediscachedemo.BookmarkController.getBookmarkById(BookmarkController.java:19) ~[classes/:na]

You can reproduce the issue using this repository https://github.com/sivaprasadreddy/spring-boot-redis-cache-demo

With default JdkSerializationRedisSerializer it is working fine.

I also saw few issues opening with similar error but they are happening because of spring-boot-devtools which I am not using.

Is it a bug or am I missing some configuration?

snicoll commented 3 years ago

@sivaprasadreddy thanks for the sample but considering that you are defining the CacheManager bean yourself, I don't think this should have been reported against Spring Boot.

Spring Data's GenericJackson2JsonRedisSerializer requires to serialize the type of the object. This is done automatically if you create the serializer with the String-based constructor. If you provide your own mapper, then that feature must be enabled.

I believe the documentation of Spring Data Redis could be improved, I've created https://github.com/spring-projects/spring-data-redis/issues/2140

sivaprasadreddy commented 1 year ago

Just in case someone came across the same issue, it is solved by registering the RedisCacheManager bean as follows:

@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.enable(JsonGenerator.Feature.IGNORE_UNKNOWN);
        objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
        //objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
        objectMapper.activateDefaultTyping(objectMapper.getPolymorphicTypeValidator(), ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);

        RedisSerializer<Object> serializer = new GenericJackson2JsonRedisSerializer(objectMapper);

        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(serializer))
                .entryTtl(Duration.ofMinutes(1));

        return RedisCacheManager.builder(redisConnectionFactory)
                .cacheDefaults(defaultRedisCacheConfiguration())
                .build();
}
paynezhuang commented 5 months ago

Just in case someone came across the same issue, it is solved by registering the RedisCacheManager bean as follows:

@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.enable(JsonGenerator.Feature.IGNORE_UNKNOWN);
        objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
        //objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
        objectMapper.activateDefaultTyping(objectMapper.getPolymorphicTypeValidator(), ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);

        RedisSerializer<Object> serializer = new GenericJackson2JsonRedisSerializer(objectMapper);

        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(serializer))
                .entryTtl(Duration.ofMinutes(1));

        return RedisCacheManager.builder(redisConnectionFactory)
                .cacheDefaults(defaultRedisCacheConfiguration())
                .build();
}

I used your configuration,When saving in redis, you can see the @class information, but when retrieve the value again, it prompts:

Could not read JSON:Unexpected token (START_OBJECT), expected VALUE_STRING: need String, Number of Boolean value that contains type id (for subtype of java.lang.Object)

Complete configuration

@Bean
public <T> RedisTemplate<String, T> redisTemplate(@Autowired LettuceConnectionFactory lettuceConnectionFactory) {
    RedisTemplate<String, T> template = new RedisTemplate<>();
    template.setConnectionFactory(lettuceConnectionFactory);

    StringRedisSerializer keySerializer = new StringRedisSerializer();
    RedisSerializer<Object> valueSerializer = RedisSerializer.json();

    template.setKeySerializer(keySerializer);
    template.setValueSerializer(valueSerializer);

    template.setHashKeySerializer(keySerializer);
    template.setHashValueSerializer(valueSerializer);

    template.afterPropertiesSet();
    return template;
}

@Bean
public RedisCacheManager redisCacheManager(RedisConnectionFactory factory) {
    ObjectMapper objectMapper = new ObjectMapper();
    objectMapper.enable(JsonGenerator.Feature.IGNORE_UNKNOWN);
    objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
    //objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
    objectMapper.activateDefaultTyping(objectMapper.getPolymorphicTypeValidator(), ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);

    RedisSerializer<Object> serializer = new GenericJackson2JsonRedisSerializer(objectMapper);

    RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
            .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
            .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(serializer))
            .entryTtl(Duration.ofMinutes(1));

    return RedisCacheManager.builder(factory)
            .cacheDefaults(config)
            .build();
}