So I have a bit of an issue with regards to Redis. It seems that SpringBoot refuses to utilise the custom RedisCacheManager bean that I've created despite using the approppriate annotations ("@Bean", "@Primary") and instead defaults to the generic one. This leads to the JsonSerializer that I have set in the custom cacheManager not being used and SpringBoot defaulting to utilising the DefaultSerializer as seen in the stack trace in the pastebin. The MainApplication class scans the basePackage so it is not a code structuring issue as all other configs in that same package are recognised. What might be the issue?
The pastebins are below. Any help fixing will be appreciated.
Stack Trace and sample methods
@Configuration
@EnableCaching
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
Jackson2JsonRedisSerializer<Object> jacksonSerializer =
new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.activateDefaultTyping(
objectMapper.getPolymorphicTypeValidator(),
ObjectMapper.DefaultTyping.NON_FINAL
);
jacksonSerializer.setObjectMapper(objectMapper);
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(jacksonSerializer);
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(jacksonSerializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
@Bean(name = "cacheManager")
@Primary
public RedisCacheManager cacheManager(RedisTemplate<String, Object> redisTemplate) {
RedisCacheWriter redisCacheWriter = RedisCacheWriter.
nonLockingRedisCacheWriter(Objects.requireNonNull(redisTemplate.getConnectionFactory()));
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
.serializeValuesWith
(RedisSerializationContext.SerializationPair.
fromSerializer(redisTemplate.getValueSerializer()));
return RedisCacheManager.builder()
.cacheWriter(redisCacheWriter)
.cacheDefaults(redisCacheConfiguration)
.withCacheConfiguration("bitcoin-balances",
RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(5)))
.withCacheConfiguration("ethereum-balances",
RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(5)))
.withCacheConfiguration("bitcoinTransactions",
RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofHours(1)))
.withCacheConfiguration("ethereumTransactions",
RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofHours(1)))
.build();
}
}
Test logs that show the cacheManager used by Springboot:
private final ApplicationContext applicationContext;
private final CacheManager cacheManager;
@PostConstruct
public void printCacheManager() {
System.out.println("Using CacheManager: " + cacheManager.getClass().getName());
}
@PostConstruct
public void listAllCacheManagers() {
String[] beans = applicationContext.getBeanNamesForType(CacheManager.class);
for (String bean : beans) {
System.out.println("CacheManager Bean: " + bean + " -> " + applicationContext.getBean(bean));
}
}
Results:
Using CacheManager: org.springframework.data.redis.cache.RedisCacheManager
CacheManager Bean: cacheManager -> org.springframework.data.redis.cache.RedisCacheManager@408bb173
The object being used as a return value:
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class WalletDto {
private String address;
private BigDecimal balance;
}
Method used in the test:
@Cacheable(
value = "ethereum-balances",
key = "T(org.springframework.security.core.context.SecurityContextHolder).getContext().getAuthentication().getName()"
)
public WalletDto getWalletBalance() {
User user = authService.getCurrentUser();
EthereumWallet ethereumWallet = ethereumWalletRepository.findByUser(user)
.orElseThrow(
() -> new EntityNotFoundException("User does not have an Ethereum wallet"));
if (ethereumWallet.isTradingLocked()) {
throw new WalletLockedException("Wallet is currently in a transaction, try again later");
}
String address = ethereumWallet.getAddress();
BigInteger updatedBalanceWei = getBalance(address);
BigDecimal updatedBalanceEth = Converter.convertWeiToEth(updatedBalanceWei);
return WalletDto.builder()
.address(address)
.balance(updatedBalanceEth)
.build();
}
Stack trace sample:
org.springframework.data.redis.serializer.SerializationException: Cannot serialize; nested exception is org.springframework.core.serializer.support.SerializationFailedException: Failed to serialize object using DefaultSerializer; nested exception is java.lang.IllegalArgumentException: DefaultSerializer requires a Serializable payload but received an object of type [com.kollybistes.common.dtos.WalletDto]
at org.springframework.data.redis.serializer.JdkSerializationRedisSerializer.serialize(JdkSerializationRedisSerializer.java:96)
at org.springframework.data.redis.serializer.DefaultRedisElementWriter.write(DefaultRedisElementWriter.java:44)
at org.springframework.data.redis.serializer.RedisSerializationContext$SerializationPair.write(RedisSerializationContext.java:287)
at org.springframework.data.redis.cache.RedisCache.serializeCacheValue(RedisCache.java:282)
at org.springframework.data.redis.cache.RedisCache.put(RedisCache.java:168)
at org.springframework.cache.interceptor.AbstractCacheInvoker.doPut(AbstractCacheInvoker.java:87)
at org.springframework.cache.interceptor.CacheAspectSupport$CachePutRequest.apply(CacheAspectSupport.java:837)
at org.springframework.cache.interceptor.CacheAspectSupport.execute(CacheAspectSupport.java:430)
at org.springframework.cache.interceptor.CacheAspectSupport.execute(CacheAspectSupport.java:345)
at org.springframework.cache.interceptor.CacheInterceptor.invoke(CacheInterceptor.java:64)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:753)
at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:698)
at com.kollybistes.core.services.EthereumService$$EnhancerBySpringCGLIB$$c3c3bd41.getWalletBalance(<generated>)
You configured each cache:
.withCacheConfiguration("bitcoin-balances",
RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(5)))
with RedisCacheConfiguration.defaultCacheConfig() which serializes values with JdkSerializationRedisSerializer by default.
Let's instead create a custom RedisCacheConfiguration and configure it to serialize values as JSON:
@Bean
RedisCacheConfiguration customRedisCacheConfiguration(CacheProperties cacheProperties) {
var objectMapper = Jackson2ObjectMapperBuilder.json().build();
objectMapper.activateDefaultTyping(
objectMapper.getPolymorphicTypeValidator(),
ObjectMapper.DefaultTyping.NON_FINAL,
JsonTypeInfo.As.PROPERTY);
var cacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
.serializeValuesWith(
RedisSerializationContext.SerializationPair.fromSerializer(
new GenericJackson2JsonRedisSerializer(objectMapper)));
CacheProperties.Redis redisProperties = cacheProperties.getRedis();
if (redisProperties.getTimeToLive() != null) {
cacheConfiguration = cacheConfiguration.entryTtl(redisProperties.getTimeToLive());
}
return cacheConfiguration;
}
Instead of creating a custom RedisCacheManager, use the RedisCacheManager autoconfigured by Spring Boot. You want to set the time to live on each cache, so let's hook into the callback that Spring Boot provides to customize the RedisCacheManager autoconfigured by Spring Boot:
@Bean
RedisCacheManagerBuilderCustomizer cacheManagerBuilderCustomizer(
RedisCacheConfiguration cacheConfiguration
) {
return redisCacheManagerBuilder -> redisCacheManagerBuilder
.withCacheConfiguration(
"bitcoin-balances",
cacheConfiguration.entryTtl(Duration.ofMinutes(5)))
// ...
}