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:
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;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user