Reputation: 51
I'm using Redis as a cache storage in a Spring Boot 2 application. I'm using the @Cacheable
annotation in some methods and I want to store the data in Redis as non-typed JSONs. With current my configuration, saving the data works fine but reading it is generating a ClassCastException
.
All the solutions, answers, examples and tutorials use Jackson's ObjectMapper
to configure either RedisTemplate
or RedisCacheConfiguration
adding a default typing attribute to the JSON. The thing here is this cache is going to be shared by different apps in different languages/technologies and I can't force the rest of the apps to work as Spring Boot does.
Here's what I have right now:
Config
@Bean
CacheManager cacheManager(RedisConnectionFactory connectionFactory) {
return RedisCacheManagerBuilder.fromConnectionFactory(connectionFactory)
.cacheDefaults(cacheConfiguration())
.build();
}
private RedisCacheConfiguration cacheConfiguration() {
return RedisCacheConfiguration.defaultCacheConfig()
.disableCachingNullValues()
.serializeKeysWith(SerializationPair.fromSerializer(RedisSerializer.string()))
.serializeValuesWith(SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer(redisMapper())));
}
private ObjectMapper redisMapper() {
return new ObjectMapper()
//.enableDefaultTyping(DefaultTyping.NON_FINAL, As.PROPERTY)
.setSerializationInclusion(JsonInclude.Include.NON_EMPTY);
}
Service
@Cacheable(key = "'persons::' + #id")
public Person getPerson(Long id) {
// return person from DB
}
Result with the current config:
{
"name": "John",
"lastName": "Doe",
"age": 31
}
When Spring tries to read the content from the cache using the deserializers, it doesn't find a "@class"
attribute with the type information so it returns a LinkedHashMap
. After that the CacheInterceptor
tries to convert the LinkedHashMap
to a Person
and there's when the ClassCastException
occurs.
Request processing failed; nested exception is java.lang.ClassCastException: java.util.LinkedHashMap cannot be cast to com.test.dto.Person
At this point I'm OK if I need to write a serializer per type I want to store or maybe I can create a custom one for all. So far my research has been unsuccessful.
Thanks for your time.
Upvotes: 2
Views: 3058
Reputation: 859
I'm fighting with this one right now too. And even add typing won't help you because if you use @Cacheable
on method returning "primitive" type such as Long
(even in collections), you get Integer
back, because jackson uses smart number mapping that returns 32-bit Integer if number fits in there.
And yes, you can now use flag USE_LONG_FOR_INTS
, but that means it always return Long
and not the type that was put in cache in the 1st place.
So if you want to save nice JSON to redis, you can't use @Cacheable
, because you can end up with Class cast exception
quite often.
BUT you can use cache manager manually, save JSON there as plain text and deserializing it yourself supplying desired return class to object mapper yourself (which unfortunately annotation can't do for you automatically).
Example code (not perfect and not handling exceptions from object mapper):
@Autowired
private CacheManager cacheManager;
@Autowired
private ObjectMapper objectMapper;
public Person getPerson(Long id) {
Cache persons = cacheManager.getCache("persons");
if (persons == null) return // cache not exist, should not happen, return person from DB
return Optional.ofNullable(persons.get(id))
.map(Cache.ValueWrapper::get)
.map(o -> objectMapper.readerFor(Person.class).readValue(o))
.orElseGet(() -> {
Person p = // get person from DB
persons.put(id, objectMapper.writeValueAsString(p));
return p;
});
}
Upvotes: 0
Reputation: 21
Honestly I'm not sure if this is the answer to your question because it doesn't create such a "clean" and "beautiful" JSON as your example, but it works for me to serialize and deserialize. 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();
}
}
Upvotes: 1