1

I want to add Caching to my Spring Boot Backend. Saving the entries to the Cache seems to work since I can see the json list in Redis after my first request but once I send my second request (which would read the Cache) to the backend Spring throws an internal error and the request fails:

WARN 25224 --- [nio-8080-exec-2] .w.s.m.s.DefaultHandlerExceptionResolver : 
Resolved [org.springframework.http.converter.HttpMessageNotWritableException: 
Could not write JSON: java.lang.ClassCastException@291f1fc4; 
nested exception is com.fasterxml.jackson.databind.JsonMappingException: 
java.lang.ClassCastException@291f1fc4 
(through reference chain: java.util.ArrayList[0]->java.util.LinkedHashMap["id"])]

My backend looks as it follows:

Config:

@Configuration
class RedisConfig {

    @Bean
    fun jedisConnectionFactory(): JedisConnectionFactory {
        val jedisConnectionFactory = JedisConnectionFactory()
        return jedisConnectionFactory
    }

    @Bean
    fun redisTemplate(): RedisTemplate<String, Any> {
        val myRedisTemplate = RedisTemplate<String, Any>()
        myRedisTemplate.setConnectionFactory(jedisConnectionFactory());
        return myRedisTemplate;
    }

    @Bean
    fun cacheManager(): RedisCacheManager {
        return RedisCacheManager.RedisCacheManagerBuilder.fromConnectionFactory(jedisConnectionFactory()).cacheDefaults(
            RedisCacheConfiguration.defaultCacheConfig().disableCachingNullValues()
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(RedisSerializer.string()))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(GenericJackson2JsonRedisSerializer(redisMapper())))

        ).build()
    }

    private fun redisMapper(): ObjectMapper {
        return ObjectMapper() //.enableDefaultTyping(DefaultTyping.NON_FINAL, As.PROPERTY)
            .setSerializationInclusion(JsonInclude.Include.NON_EMPTY)
    }
}

Controller:

    fun getPrivateRecipes(@RequestParam(required = false) langCode: String?): List<PrivateRecipeData> {
        val lang = langCode ?: "en"
        val userId = getCurrentUser().userRecord.uid
        return privateRecipeCacheService.getPrivateRecipesCached(lang, userId)
    }

Caching-Service

    @Cacheable("privateRecipes")
    fun getPrivateRecipesCached(lang: String, userId: String): List<PrivateRecipeData> {
        return privateRecipeService.getPrivateRecipes(lang, userId)
    }

I played around with the Cachable annotation, added keys, but it does not change the problem. The import and export of the list seems to be done with different classes. How to solve this?

finisinfinitatis
  • 861
  • 11
  • 23

2 Answers2

1

In your ObjectMapper tell Jackson to use an ArrayList to hold collections of PrivateRecipeData instances like:

objectMapper.getTypeFactory().constructCollectionType(ArrayList.class, PrivateRecipeData.class);

One possible way to set it up:

One possible way is, in your cacheManager config, pass it a RedisTemplate configure with the right object mapper. Off the top of my head:

public RedisTemplate<String, Object> redisTemplate(...) {
  Jackson2JsonRedisSerializer serializer = new ...
  ObjectMapper objectMapper = new ObjectMapper();
  //...
  serializer.setObjectMapper(objectMapper);
  RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
  // ...
  redisTemplate.setValueSerializer(serializer);
  redisTemplate.afterPropertiesSet();
  return redisTemplate;
}
BSB
  • 1,516
  • 13
  • 14
  • 1
    How can I tell the ObjectMapper to use this type as default? Also would this mean I would have to create a dedicated cacheManager for each type I want to cache? – finisinfinitatis Aug 15 '22 at 23:57
0

What I did in the end was just using the default ObjectMapper (providing no arguments to GenericJackson2JsonRedisSerializer):

    @Bean
    fun cacheManager(): RedisCacheManager {
        return RedisCacheManager.RedisCacheManagerBuilder.fromConnectionFactory(jedisConnectionFactory()).cacheDefaults(
            RedisCacheConfiguration.defaultCacheConfig().disableCachingNullValues()
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(RedisSerializer.string()))
                .serializeValuesWith(
                    RedisSerializationContext.SerializationPair.fromSerializer(
                        GenericJackson2JsonRedisSerializer()
                    )
                )
        ).build()
    }

The serialization in Json now looks a bit different (containing class names as well) but thats totally fine since it is still human readable :)

[
    "java.util.ArrayList",
    [
        {
            "@class": "com.my.package.data.PrivateRecipeData",
            "id": 1,
            "img_src": "user/xxxxxx/recipeImgs/32758c1c-35cf-4f92-9e8c-0057f4447d6c.jpg",
            "name": "VeggieBurger",
            "instructions": [
                "java.util.ArrayList",
                [
                    {
                        "id": 6,
                        "recipeId": 1,
                    }
                ]
            ],
            ...
         }
     ],
...  
finisinfinitatis
  • 861
  • 11
  • 23
  • Has this worked for anyone ? I does stored the data on cache as described, although it does no deserialize at all. It thows "java.lang.ClassCastException: class java.util.LinkedHashMap cannot be cast to class" (even though the type on Redis is "string"). – Wall-E Mar 22 '23 at 14:20
  • 1
    @Wall-E yes this worked for me actually! But maybe I forgot to post other relevant code. I will check if I still have this code when I find time! – finisinfinitatis Mar 22 '23 at 19:48