'How can I easily cache Kotlin Objects in Redis using json via Jackson?

I have a Spring boot app written in Kotlin where I would like to enable caching in Redis. I'd like to have the objects stored as serialized JSON and ideally don't want to have to register each type that could be potentially cached. I have some configuration that mostly works, with a big caveat.

@Bean
fun redisCacheConfiguration(): RedisCacheConfiguration {
    val objectMapper =
        ObjectMapper()
            .registerModule(KotlinModule())
            .registerModule(JavaTimeModule())
            .enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY)

    val serializer = GenericJackson2JsonRedisSerializer(objectMapper)

    return RedisCacheConfiguration
        .defaultCacheConfig()
        .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(serializer))
}

I'm having a little trouble understanding the different values for DefaultTyping but NON_FINAL seems to be the most expansive. However, since objects in Kotlin are final by default, this only works for objects flagged as "open". Ideally I'd like to avoid having to "open" objects just so they can be cached.

Is there some other way I can make this work?



Solution 1:[1]

I had the same problem. You should use "open" classes. But this will not help you with data classes, because you cannot make them "open".
There is a plugin called "all-open" where you can define annotations. If you use these annotations classes become "open", even data classes.

spring-kotlin plugin uses "all-open" plugin under the hood, so spring annotations like @Service, @Component etc. make classes open for AOP, because proxying requires you to inherit from classes.

If you use spring-kotlin plugin, there is nice annotation that makes sense for you problem, it is used in Spring Cache, its name is @Cacheable. If you use @Cacheable on your classes, they will become open and save their type-info to json (ex: {@class: "com.example.MyClass", ...}) when you include this code:

    val objectMapper =
    ObjectMapper()
        .registerModule(KotlinModule())
        .registerModule(JavaTimeModule())
        .enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY)

val serializer = GenericJackson2JsonRedisSerializer(objectMapper)

More details: https://kotlinlang.org/docs/reference/compiler-plugins.html

Shortly: You don't have to do anything except adding @Cacheable annotation to the classes you want, and it fits by sense also IMO.

Solution 2:[2]

The issues have been solved. Therefore we can remove @Cacheble hack from the code. You have to modify your ObjectMapper with the next implementation

val om = ObjectMapper()
            .registerModule(KotlinModule())
            .registerModule(JavaTimeModule())
            .activateDefaultTyping(BasicPolymorphicTypeValidator.builder()
                .allowIfBaseType(Any::class.java)
                .build(), ObjectMapper.DefaultTyping.EVERYTHING)

val serializer = GenericJackson2JsonRedisSerializer(om)

Fixed Maven Jackon dependency

<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind -->
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.10.0.pr2</version>
</dependency>

Solution 3:[3]

You can look this:

https://github.com/endink/caching-kotlin

Its support both jackson and kryo

Solution 4:[4]

I had a problem since my data classes were extending some interfaces, so generic would not do the trick, I end up with this solution, its a custom serialiser and deserialiser, the generic would just save time compiled getter as a variable and break the deserialise

@Configuration
@EnableCaching
class CachingConfiguration() : CachingConfigurerSupport() {

    @Bean
    fun configureRedisAction(): ConfigureRedisAction? {
        return ConfigureRedisAction.NO_OP
    }

    @Autowired
    private lateinit var redisConnectionFactory: RedisConnectionFactory

    companion object {
        const val CACHE_KEY = "cache-key"
    }

    @Bean
    override fun cacheManager(): CacheManager? {
        return RedisCacheManager.RedisCacheManagerBuilder
                .fromConnectionFactory(redisConnectionFactory)
                .withCacheConfiguration(CACHE_KEY, cacheConfig<User>(ofMinutes(5)))
                .build()
    }

    private inline fun <reified T> cacheConfig(ttl: Duration): RedisCacheConfiguration {
        return RedisCacheConfiguration
                .defaultCacheConfig()
                .serializeValuesWith(fromSerializer(object : RedisSerializer<Any> {
                    val mapper = ObjectMapper().registerModule(ParameterNamesModule())
                    override fun serialize(t: Any?): ByteArray? {
                        return mapper.writeValueAsBytes(t)
                    }
                    override fun deserialize(bytes: ByteArray?): Any? {
                        return try {
                            mapper.readValue(bytes!!, T::class.java) as Any
                        } catch (e: Exception) {
                            null
                        }
                    }
                })
                )
                .entryTtl(ttl)
    }
}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2
Solution 3 sharping
Solution 4 Douglas Caina