Spring-data-redis @Cacheable java.lang.ClassCastException: java.util.LinkedHashMap cannot be cast to MyObject
Asked Answered
S

4

18

I am using spring-data-redis for caching data in my spring boot app. I am using Mongo as my primary data source and Redis as a cache. When I hit the API for the first time, it fetches record from Mongo and saves it in Cache, and returns MyObject correctly to the client. But when I hit the API second time, it finds the record in the Cache, and while trying to deserialize that back into MyObject, it . always runs into a cast exception:

java.lang.ClassCastException: java.util.LinkedHashMap cannot be cast to MyObject

Here is my Redis Configuration:

public class MyConfiguration {
    @Bean
    public CacheManager cacheManager(RedisTemplate<String, MyObject> redisTemplate) {
        return new RedisCacheManager(redisTemplate);
    }

    @Bean
    public RedisTemplate<String, MyObject> redisTemplate(RedisConnectionFactory connectionFactory, ObjectMapper objectMapper) {
        StringRedisSerializer serializer = new StringRedisSerializer();
        GenericJackson2JsonRedisSerializer hashValueSerializer = new GenericJackson2JsonRedisSerializer(objectMapper);
        RedisTemplate<String, MyObject> redisTemplate = new RedisTemplate<>();
        redisTemplate.setKeySerializer(serializer);
        redisTemplate.setValueSerializer(hashValueSerializer);
        redisTemplate.setConnectionFactory(connectionFactory);
        return redisTemplate;
    }
}

Found same problem reported here: Spring-data-redis @Cacheable java.lang.ClassCastException: java.util.HashMap cannot be cast to java.lang.String

I researched for quite some time but have no ideas. Please suggest. Thanks a ton in advance.

Slater answered 27/2, 2018 at 19:19 Comment(3)
UPDATE: This is resolved by using a different Serializer with Generics and passing in the ObjectMapper. Use Jackson2JsonRedisSerializer< MyObject> and objectSerializer.setObjectMapper(objectMapper).Slater
It doesn't solve the original problem.Disputatious
Using Jackson2JsonRedisSerializer<MyObject> could solve your specific case but it's no a scalable solution since you cannot add more than one Jackson2JsonRedisSerializer to the redisTemplateTinea
C
14

Reason

You have customized the objectMapper, and there is no class name in the Redis value. So it can't be deserialized into the real type.

You can check the redis value, there is no "@class" : "com.xxxx.xxx.entity.xx" in the redis value.

My solution

@Bean
public RedisCacheConfiguration redisCacheConfiguration(ObjectMapper objectMapper) {
    // Do not change the default object mapper, we need to serialize the class name into the value
    objectMapper = objectMapper.copy();
    // This methodo was deprecated in jackson 2.10 or higher use activateDefaultTyping intead of enableDefaultTyping 
    // objectMapper = objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);

    objectMapper = objectMapper.activateDefaultTyping(objectMapper.getPolymorphicTypeValidator(), ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);

    return RedisCacheConfiguration.defaultCacheConfig()
            .entryTtl(Duration.ofMinutes(1))
            .disableCachingNullValues()
            .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer(objectMapper)));
}

I use the objectMapper.enableDefaultTyping() after digging into the source code of the GenericJackson2JsonRedisSerializer.class

Czechoslovak answered 11/10, 2021 at 3:29 Comment(1)
objectMapper.activateDefaultTyping( objectMapper.getPolymorphicTypeValidator(), ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY );Monarchist
R
6

It worked for me by simply setting a new GenericJackson2JsonRedisSerializer() with no arguments, in both Key, value serializer and hash key, value serializer

@Bean
    public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory lettuceConnectionFactory, ObjectMapper objectMapper) {
        final RedisTemplate<String, Object> redisTemplate = new RedisTemplate<String, Object>();
        redisTemplate.setConnectionFactory(lettuceConnectionFactory);
        // value serializer
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        // hash value serializer
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());

        logger.info("wiring up Redistemplate...");
        return redisTemplate;
    }
Rarefy answered 7/12, 2019 at 16:38 Comment(0)
G
1

Jesus, why is it so hard to configure ? I almost did this manually.

Java ecosystem solves some complexities beautifully but brings a lot of new ones.

Anyway, I'm using Kotlin with Spring Boot 3 using @Cacheable on a Feign HTTP client interface's method and was able to resolve this by the following configurations:

private fun redisCacheDefaultConfiguration(ttlInMinutes: Long) = RedisCacheConfiguration
    .defaultCacheConfig()
    .disableCachingNullValues()
    .entryTtl(Duration.ofMinutes(ttlInMinutes))
    .prefixCacheNameWith("cachename:")
    .serializeKeysWith(SerializationPair.fromSerializer(StringRedisSerializer()))
    .serializeValuesWith(SerializationPair.fromSerializer(GenericJackson2JsonRedisSerializer(getObjectMapper())))

private fun getObjectMapper(): ObjectMapper {
    val objectMapper = JacksonConfiguration().defaultObjectMapper().copy()
    var defaultTypeResolver: StdTypeResolverBuilder = ObjectMapper.DefaultTypeResolverBuilder(
        ObjectMapper.DefaultTyping.EVERYTHING,
        objectMapper.polymorphicTypeValidator
    )
    defaultTypeResolver = defaultTypeResolver.init(JsonTypeInfo.Id.CLASS, null)
    defaultTypeResolver = defaultTypeResolver.inclusion(JsonTypeInfo.As.PROPERTY)
    objectMapper.setDefaultTyping(defaultTypeResolver)
    return objectMapper
}

By the way, this is the same ObjectMapper created by GenericJackson2JsonRedisSerializer if you use the no args constructor. I only did this because my instantiation of the mapper have some other configurations that i'd like to be used.

The entry on redis was:

{
  "@class": "com.path.to.my.DataClazzObj",
  "id": ["java.util.UUID", "a1261350-ca26-4588-961c-c79a42990111"]
}

Not pretty, but does the job.

Godwin answered 4/8, 2023 at 19:4 Comment(1)
This is the only solution that worked in my case with complex objects, Java 8 date/time objects, lists and collections, ... I just was able to reduce DefaultTypeing.EVERYTHING to DefaultTypeing.NON_FINALSivie
T
0

It works for me to serialize and deserialize any Object. In this example the cache manager is set TTL you can remove this if you want.

@Configuration
@EnableCaching
public class RedisCacheConfig {

  @Value("${spring.redis.host}")
  private String redisHostName;

  @Value("${spring.redis.port}")
  private int redisPort;

  @Bean
  public LettuceConnectionFactory redisConnectionFactory() {
    return new LettuceConnectionFactory(new RedisStandaloneConfiguration(redisHostName, redisPort));
  }

  @Bean
  public RedisTemplate<Object, Object> redisTemplate() {
    RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<Object, Object>();
    redisTemplate.setConnectionFactory(redisConnectionFactory());
    return redisTemplate;
  }

  @Bean
  @Primary
  public RedisCacheManager redisCacheManager(LettuceConnectionFactory lettuceConnectionFactory) {
    RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig().disableCachingNullValues()
      .entryTtl(Duration.ofMinutes(1))
      .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(RedisSerializer.json()));

    redisCacheConfiguration.usePrefix();

    return RedisCacheManager.RedisCacheManagerBuilder.fromConnectionFactory(lettuceConnectionFactory)
      .cacheDefaults(redisCacheConfiguration).build();

  }
}
Terle answered 10/11, 2019 at 18:12 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.