How to solve List Mapping Exception on Redis Cache Load in Spring Boot - spring-boot

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?

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;
}

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,
}
]
],
...
}
],
...

Related

Redis Caching Does Not Work With Hibernate PersistentBag<Enum>

I have implemented spring data redis caching for my spring boot project. All my caches work except when I try to cache an item which contains a field of List<Enum> type (not the raw Enum class but any enum class I have created in my project eg. List<MyEnum>).
Caches work without a problem if there is an Enum field or a List field, but when there is a list of enums, upon requesting a cached response the json I receive abruptly ends at the enum list field.
I've even implemented a custom json converter for List<Enum> to List<String> conversion but it did not solve my problem.
Redis Config
#Configuration
#EnableCaching
public class RedisConfig {
#Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
template.setEnableTransactionSupport(true);
template.afterPropertiesSet();
return template;
}
}
Expected JSON Response
{
"field1": "value1",
"list1": [
"element1",
"element2"
],
"enum1": "enumValue1",
"enumList": [
"enumValue2",
"enumValue3",
"enumValue4"
]
}
Received JSON Response
{
"field1": "value1",
"list1": [
"element1",
"element2"
],
"enum1": "enumValue1",
"enumList"
}
EDIT
Turns out that my List<Enum> fields are using PersistentBag from Hibernate instead of ArrayList. Right now using ArrayList<>(enumList) when assigning a response field seems to solve the problem. Are there any better solutions to this?

Clear cache for specific cache name in redis spring data

I have below method which caches student-classes , I want to clear only the cache name of student-classes
#Cacheable( value = "getStudentClasses",key ="(new net.student.util.CacheKeyCreator()).createKey(''+#university)",cacheManager = "cacheManager")
public List<StudentClass> getStudentClasses(String university) {
//get studentclasses
}
I have tried to clear as below, but it doesn't clear the cache with the specific name
#Bean(name = "cacheManager")
public CacheManager cacheManager ( RedisTemplate<String, Object> redisTemplate ) {
RedisCacheManager redisCacheManager = new RedisCacheManager( redisTemplate );
redisCacheManager.setDefaultExpiration(0);
redisCacheManager.setUsePrefix( true);
return redisCacheManager;
}
#Autowired
ApplicationContext context;
public void clearStudentClasses(){
CacheManager cacheManager= (CacheManager) context.getBean("cacheManager");
cacheManager.getCache("getStudentClasses").clear(); //exceptionLine
}
I got this exception at exception line
> org.springframework.dao.InvalidDataAccessApiUsageException: ERR
> unknown command 'EVAL'; nested exception is
> redis.clients.jedis.exceptions.JedisDataException: ERR unknown command
> 'EVAL'

spring boot with redis

I worked with spring boot and redis to caching.I can cache my data that fetch from database(oracle) use #Cacheable(key = "{#input,#page,#size}",value = "on_test").
when i try to fetch data from key("on_test::0,0,10") with redisTemplate the result is 0
why??
Redis Config:
#Configuration
public class RedisConfig {
#Bean
JedisConnectionFactory jedisConnectionFactory() {
RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration("localhost", 6379);
redisStandaloneConfiguration.setPassword(RedisPassword.of("admin#123"));
return new JedisConnectionFactory(redisStandaloneConfiguration);
}
#Bean
public RedisTemplate<String,Objects> redisTemplate() {
RedisTemplate<String,Objects> template = new RedisTemplate<>();
template.setStringSerializer(new StringRedisSerializer());
template.setValueSerializer(new StringRedisSerializer());
template.setConnectionFactory(jedisConnectionFactory());
return template;
}
//service
#Override
#Cacheable(key = "{#input,#page,#size}",value = "on_test")
public Page<?> getAllByZikaConfirmedClinicIs(Integer input,int page,int size) {
try {
Pageable newPage = PageRequest.of(page, size);
String fromCache = controlledCacheService.getFromCache();
if (fromCache == null && input!=null) {
log.info("cache is empty lets initials it!!!");
Page<DataSet> all = dataSetRepository.getAllByZikaConfirmedClinicIs(input,newPage);
List<DataSet> d = redisTemplate.opsForHash().values("on_test::0,0,10");
System.out.print(d);
return all;
}
return null;
The whole point of using #Cacheable is that you don't need to be using RedisTemplate directly. You just need to call getAllByZikaConfirmedClinicIs() (from outside of the class it is defined in) and Spring will automatically check first if a cached result is available and return that instead of calling the function.
If that's not working, have you annotated one of your Spring Boot configuration classes with #EnableCaching to enable caching?
You might also need to set spring.cache.type=REDIS in application.properties, or spring.cache.type: REDIS in application.yml to ensure Spring is using Redis and not some other cache provider.

how to configure different ttl for each redis cache when using #cacheable in springboot2.0

I am using #cacheable in springboot2.0 with redis. I have configured RedisCacheManager as follow:
#Bean
public RedisCacheManager redisCacheManager(RedisConnectionFactory connectionFactory) {
RedisCacheWriter redisCacheWriter = RedisCacheWriter.lockingRedisCacheWriter(connectionFactory);
SerializationPair<Object> valueSerializationPair = RedisSerializationContext.SerializationPair
.fromSerializer(new GenericJackson2JsonRedisSerializer());
RedisCacheConfiguration cacheConfiguration = RedisCacheConfiguration.defaultCacheConfig();
cacheConfiguration = cacheConfiguration.serializeValuesWith(valueSerializationPair);
cacheConfiguration = cacheConfiguration.prefixKeysWith("myPrefix");
cacheConfiguration = cacheConfiguration.entryTtl(Duration.ofSeconds(30));
RedisCacheManager redisCacheManager = new RedisCacheManager(redisCacheWriter, cacheConfiguration);
return redisCacheManager;
}
but this make all key's ttl 30 second, how to configure different ttl for each redis cache with different cachename?
You can configure different expire time for each cache using only one CacheManager by creating different configurations for each cache and put them in a map with which you create the CacheManager.
For example:
#Bean
RedisCacheWriter redisCacheWriter() {
return RedisCacheWriter.lockingRedisCacheWriter(jedisConnectionFactory());
}
#Bean
RedisCacheConfiguration defaultRedisCacheConfiguration() {
return RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofSeconds(defaultCacheExpiration));
}
#Bean
CacheManager cacheManager() {
Map<String, RedisCacheConfiguration> cacheNamesConfigurationMap = new HashMap<>();
cacheNamesConfigurationMap.put("cacheName1", RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofSeconds(ttl1)));
cacheNamesConfigurationMap.put("cacheName2", RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofSeconds(ttl2)));
cacheNamesConfigurationMap.put("cacheName3", RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofSeconds(ttl3)));
return new RedisCacheManager(redisCacheWriter(), defaultRedisCacheConfiguration(), cacheNamesConfigurationMap);
}
If you need configure different expire time for cache when using #cacheable ,
you can configure different CacheManager with different ttl,and specify cacheManager when using cache in your service.
#Cacheable(cacheManager = "expireOneHour", value = "onehour", key = "'_onehour_'+#key", sync = true)
Here is how you can define multiple Redis based caches with different TTL and maxIdleTime using Redisson Java client:
#Bean(destroyMethod="shutdown")
RedissonClient redisson() throws IOException {
Config config = new Config();
config.useClusterServers()
.addNodeAddress("redis://127.0.0.1:7004", "redis://127.0.0.1:7001");
return Redisson.create(config);
}
#Bean
CacheManager cacheManager(RedissonClient redissonClient) {
Map<String, CacheConfig> config = new HashMap<String, CacheConfig>();
// create "myCache1" cache with ttl = 20 minutes and maxIdleTime = 12 minutes
config.put("myCache", new CacheConfig(24*60*1000, 12*60*1000));
// create "myCache2" cache with ttl = 35 minutes and maxIdleTime = 24 minutes
config.put("myCache2", new CacheConfig(35*60*1000, 24*60*1000));
return new RedissonSpringCacheManager(redissonClient, config);
}
This is my code:
The shared config in common module
#Bean
RedisCacheManagerBuilderCustomizer redisCacheManagerBuilderCustomizer(List<RedisTtlConfig> ttlConfigs) {
RedisCacheConfiguration defaultCacheConfig = RedisCacheConfiguration.defaultCacheConfig();
return (builder) -> {
Map<String, RedisCacheConfiguration> ttlConfigMap = new HashMap<>();
ttlConfigs.forEach( config -> {
config.forEach( (key, ttl) -> {
ttlConfigMap.put(key, defaultCacheConfig.entryTtl(Duration.ofSeconds(ttl)));
});
});
builder.withInitialCacheConfigurations(ttlConfigMap);
builder.cacheDefaults(defaultCacheConfig);
};
}
A custom class to collect ttl config by key
public class RedisTtlConfig extends HashMap<String, Long> {
public RedisTtlConfig setTTL(String key, Long ttl){
this.put(key, ttl);
return this;
}
}
3.Simple ttl config code in ref module
#Bean
RedisTtlConfig corpCacheTtlConfig(){
return new RedisTtlConfig()
.setTTL("test1", 300l)
.setTTL("test2", 300l);
}

How to consume a spring data rest service with java?

I have the following spring boot + data Rest repository:
#RepositoryRestResource(collectionResourceRel = "dto", path = "produtos")
public interface ProdutoRepository extends CrudRepository<Produto, Integer> {
#Query("SELECT p FROM Produto p where descricao LIKE CONCAT(UPPER(:like),'%')")
List<Produto> findByLike(#Param("like") String like);
}
I also have a java client that access this method (this is my example of doing it):
String url = "http://localhost:8080/produtos/search/findByLike?like={like}";
RestTemplate t = new RestTemplate();
ProdutoDto resp = t.getForObject(url, ProdutoDto.class, txtLoc.getText());
ProdutoDto (this one is not totally necessary):
public class ProdutoDto extends HalDto<Produto> {}
HalDto:
public class HalDto<T extends ResourceSupport> extends ResourceSupport {
#JsonProperty("_embedded")
private EmbeddedDto<T> embedded;
public EmbeddedDto<T> getEmbedded() {
return embedded;
}
public void setEmbedded(EmbeddedDto<T> embedded) {
this.embedded = embedded;
}
}
EmbeddedDto:
public class EmbeddedDto<T> {
#JsonProperty("dto")
private List<T> dtoList;
public List<T> getDtoList()
{
return dtoList;
}
public void setDto(List<T> dtoList) {
this.dtoList = dtoList;
}
}
Those classes are necessary (i think) because Spring Data returns data in the HAL (https://en.wikipedia.org/wiki/Hypertext_Application_Language) format.
Note: Produto must extend ResourceSupport.
Caveats: All collectionResourceRel must be named "dto" and it only works for collections (may be adjusted).
Is this the proper way to do this?
I have googled around and found plenty of examples of doing the server side, but almost nothing on building clients.
Thanks.
This is a solution that I have found which seems to work well.
First, setup your RestTemplate so that it expects JSON/HAL and knows what to do with it:
#Bean
public RestTemplate restTemplate() {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(new Jackson2HalModule());
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
MappingJackson2HttpMessageConverter messageConverter =
new MappingJackson2HttpMessageConverter();
messageConverter.setObjectMapper(objectMapper);
messageConverter.setSupportedMediaTypes(Arrays.asList(MediaTypes.HAL_JSON, MediaType.APPLICATION_JSON_UTF8));
return new RestTemplate(Arrays.asList(messageConverter));
}
Then you can use the exchange method of the RestTemplate to specify that you want your result to be ResponseEntity<PagedResources<Producto>>
ResponseEntity<PagedResources<Producto>> resultResponse = restTemplate.exchange(uri, HttpMethod.GET, HttpEntity.EMPTY, new ParameterizedTypeReference<PagedResources<Producto>>(){});
if(resultResponse.getStatusCode() == HttpStatus.OK){
Collection<Producto> results = resultResponse.getBody().getContent();
log.info("{} results obtained", results.size());
}
You can instantiate restTemplate by either calling the restTemplate() method defined above or you can inject (autowire) it.

Resources