refactor(cache): replace manual redis caching with @Cacheable

- Add a primary defaultCacheManager with a two-hour entry TTL
- Add a named longTermCacheManager with a one-day entry TTL
- Replace the hand-rolled RedisTemplate read/write and midnight-TTL
  logic in DailyPasswordManager with a @Cacheable annotation
- Remove six imports left dead by the above
This commit is contained in:
siujamo committed 2026-09-10 17:35:08 +08:00
1 parent ee7b96532d
commit 8700555ae6
2 files changed
+58 -38

No files matched your search

@@ -1,9 +1,11 @@
package com.onixbyte.ahsarahguide.config;
import com.onixbyte.ahsarahguide.shared.JacksonRedisSerialiser;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
@@ -39,21 +41,24 @@ import java.time.Duration;
public class CacheConfig {
/**
* Creates a custom Redis cache manager with JSON serialisation support.
* Creates the application's primary cache manager, backed by Redis.
* <p>
* This method configures a {@link RedisCacheManager} that uses string serialisation for cache
* keys and {@link GenericJackson2JsonRedisSerializer} for cache values. This setup ensures that
* complex objects can be stored and retrieved from Redis cache whilst maintaining readability
* and compatibility with JSON-based systems.
* This manager is marked {@link Primary}, so it is the one selected for any
* {@code @Cacheable} or {@code @CacheEvict} annotation that does not name a manager
* explicitly, as well as for any {@link CacheManager} that is injected by type.
* <p>
* Cache entries are keyed using string serialisation and values are stored as JSON via
* {@link JacksonRedisSerialiser}. Every entry expires after two hours, making this manager
* suitable for data that is expensive to compute but not required to survive for long.
*
* @param connectionFactory the Redis connection factory used to establish connections
* @return a configured {@link RedisCacheManager} with custom serialisation settings
* @return a fully configured {@link CacheManager} with a two-hour default entry TTL
* @see #longTermCacheManager(RedisConnectionFactory)
* @see RedisCacheManager
* @see GenericJackson2JsonRedisSerializer
* @see RedisSerializationContext
*/
@Primary
@Bean
public RedisCacheManager cacheManager(
public CacheManager defaultCacheManager(
RedisConnectionFactory connectionFactory
) {
var _keySerializer = RedisSerializer.string();
@@ -63,7 +68,40 @@ public class CacheConfig {
.fromSerializer(_keySerializer))
.serializeValuesWith(RedisSerializationContext.SerializationPair
.fromSerializer(JacksonRedisSerialiser.INSTANCE))
.entryTtl(Duration.ofMinutes(90L));
.entryTtl(Duration.ofHours(2L));
return RedisCacheManager.RedisCacheManagerBuilder
.fromConnectionFactory(connectionFactory)
.cacheDefaults(cacheConfiguration)
.build();
}
/**
* Creates a secondary cache manager for data that must be retained for longer.
* <p>
* This manager is not {@link Primary}; it must be requested by name wherever it is needed,
* as in {@code @Cacheable(cacheManager = "longTermCacheManager")}.
* <p>
* Serialisation is configured identically to {@link #defaultCacheManager}, but entries
* expire after one day rather than two hours.
*
* @param connectionFactory the Redis connection factory used to establish connections
* @return a fully configured {@link CacheManager} with a one-day default entry TTL
* @see #defaultCacheManager(RedisConnectionFactory)
* @see RedisCacheManager
*/
@Bean
public CacheManager longTermCacheManager(
RedisConnectionFactory connectionFactory
) {
var _keySerializer = RedisSerializer.string();
var cacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
.serializeKeysWith(RedisSerializationContext.SerializationPair
.fromSerializer(_keySerializer))
.serializeValuesWith(RedisSerializationContext.SerializationPair
.fromSerializer(JacksonRedisSerialiser.INSTANCE))
.entryTtl(Duration.ofDays(1L));
return RedisCacheManager.RedisCacheManagerBuilder
.fromConnectionFactory(connectionFactory)
@@ -4,20 +4,15 @@ import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.onixbyte.ahsarahguide.domain.dto.DailyPasswordResponse;
import com.onixbyte.ahsarahguide.exeption.InternalServerErrorException;
import com.onixbyte.ahsarahguide.shared.JacksonModules;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.http.HttpStatus;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestClient;
import org.springframework.web.server.ResponseStatusException;
import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Objects;
/**
@@ -28,15 +23,13 @@ import java.util.Objects;
@Component
public class DailyPasswordManager {
private static final String CACHE_KEY_PREFIX = "daily-password:";
private static final String CACHE_NAME = "daily-password";
private final RestClient restClient;
private final RedisTemplate<String, Object> redisTemplate;
@Autowired
public DailyPasswordManager(
RestClient.Builder restClientBuilder,
RedisTemplate<String, Object> redisTemplate
RestClient.Builder restClientBuilder
) {
var snakeCaseMapper = new ObjectMapper();
snakeCaseMapper.setPropertyNamingStrategy(
@@ -54,25 +47,22 @@ public class DailyPasswordManager {
new MappingJackson2HttpMessageConverter(snakeCaseMapper));
})
.build();
this.redisTemplate = redisTemplate;
}
/**
* Retrieves the daily password from cache or generates a new one.
* @return the daily password response
*/
@Cacheable(
cacheNames = CACHE_NAME,
key = "T(java.time.LocalDate).now().toString()",
sync = true,
unless = "#result == null",
cacheManager = "longTermCacheManager"
)
public DailyPasswordResponse getDailyPassword() {
var key = CACHE_KEY_PREFIX + LocalDate.now();
// Attempt to fetch the cached response
var cached = redisTemplate.opsForValue().get(key);
if (cached != null) {
return (DailyPasswordResponse) cached;
}
DailyPasswordResponse response;
try {
// Fetch from the upstream remote service
response = restClient.get()
.uri((uriBuilder) -> uriBuilder
.path("/sjzmm")
@@ -90,14 +80,6 @@ public class DailyPasswordManager {
throw new ResponseStatusException(HttpStatus.BAD_GATEWAY, "No daily password data available.");
}
// Calculate the precise duration until midnight to optimise Redis memory usage
var now = LocalDateTime.now();
var midnight = now.toLocalDate().atStartOfDay().plusDays(1);
var durationUntilMidnight = Duration.between(now, midnight);
// Save to Redis with the calculated TTL
redisTemplate.opsForValue().set(key, response, durationUntilMidnight);
return response;
}
}