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;
|
package com.onixbyte.ahsarahguide.config;
|
||||||
|
|
||||||
import com.onixbyte.ahsarahguide.shared.JacksonRedisSerialiser;
|
import com.onixbyte.ahsarahguide.shared.JacksonRedisSerialiser;
|
||||||
|
import org.springframework.cache.CacheManager;
|
||||||
import org.springframework.cache.annotation.EnableCaching;
|
import org.springframework.cache.annotation.EnableCaching;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
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.RedisCacheConfiguration;
|
||||||
import org.springframework.data.redis.cache.RedisCacheManager;
|
import org.springframework.data.redis.cache.RedisCacheManager;
|
||||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||||
@@ -39,21 +41,24 @@ import java.time.Duration;
|
|||||||
public class CacheConfig {
|
public class CacheConfig {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a custom Redis cache manager with JSON serialisation support.
|
* Creates the application's primary cache manager, backed by Redis.
|
||||||
* <p>
|
* <p>
|
||||||
* This method configures a {@link RedisCacheManager} that uses string serialisation for cache
|
* This manager is marked {@link Primary}, so it is the one selected for any
|
||||||
* keys and {@link GenericJackson2JsonRedisSerializer} for cache values. This setup ensures that
|
* {@code @Cacheable} or {@code @CacheEvict} annotation that does not name a manager
|
||||||
* complex objects can be stored and retrieved from Redis cache whilst maintaining readability
|
* explicitly, as well as for any {@link CacheManager} that is injected by type.
|
||||||
* and compatibility with JSON-based systems.
|
* <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
|
* @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 RedisCacheManager
|
||||||
* @see GenericJackson2JsonRedisSerializer
|
|
||||||
* @see RedisSerializationContext
|
|
||||||
*/
|
*/
|
||||||
|
@Primary
|
||||||
@Bean
|
@Bean
|
||||||
public RedisCacheManager cacheManager(
|
public CacheManager defaultCacheManager(
|
||||||
RedisConnectionFactory connectionFactory
|
RedisConnectionFactory connectionFactory
|
||||||
) {
|
) {
|
||||||
var _keySerializer = RedisSerializer.string();
|
var _keySerializer = RedisSerializer.string();
|
||||||
@@ -63,7 +68,40 @@ public class CacheConfig {
|
|||||||
.fromSerializer(_keySerializer))
|
.fromSerializer(_keySerializer))
|
||||||
.serializeValuesWith(RedisSerializationContext.SerializationPair
|
.serializeValuesWith(RedisSerializationContext.SerializationPair
|
||||||
.fromSerializer(JacksonRedisSerialiser.INSTANCE))
|
.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
|
return RedisCacheManager.RedisCacheManagerBuilder
|
||||||
.fromConnectionFactory(connectionFactory)
|
.fromConnectionFactory(connectionFactory)
|
||||||
|
|||||||
@@ -4,20 +4,15 @@ import com.fasterxml.jackson.databind.DeserializationFeature;
|
|||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
|
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
|
||||||
import com.onixbyte.ahsarahguide.domain.dto.DailyPasswordResponse;
|
import com.onixbyte.ahsarahguide.domain.dto.DailyPasswordResponse;
|
||||||
import com.onixbyte.ahsarahguide.exeption.InternalServerErrorException;
|
|
||||||
import com.onixbyte.ahsarahguide.shared.JacksonModules;
|
import com.onixbyte.ahsarahguide.shared.JacksonModules;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
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.HttpStatus;
|
||||||
import org.springframework.http.HttpStatusCode;
|
|
||||||
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
|
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.web.client.RestClient;
|
import org.springframework.web.client.RestClient;
|
||||||
import org.springframework.web.server.ResponseStatusException;
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
|
||||||
import java.time.Duration;
|
|
||||||
import java.time.LocalDate;
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -28,15 +23,13 @@ import java.util.Objects;
|
|||||||
@Component
|
@Component
|
||||||
public class DailyPasswordManager {
|
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 RestClient restClient;
|
||||||
private final RedisTemplate<String, Object> redisTemplate;
|
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
public DailyPasswordManager(
|
public DailyPasswordManager(
|
||||||
RestClient.Builder restClientBuilder,
|
RestClient.Builder restClientBuilder
|
||||||
RedisTemplate<String, Object> redisTemplate
|
|
||||||
) {
|
) {
|
||||||
var snakeCaseMapper = new ObjectMapper();
|
var snakeCaseMapper = new ObjectMapper();
|
||||||
snakeCaseMapper.setPropertyNamingStrategy(
|
snakeCaseMapper.setPropertyNamingStrategy(
|
||||||
@@ -54,25 +47,22 @@ public class DailyPasswordManager {
|
|||||||
new MappingJackson2HttpMessageConverter(snakeCaseMapper));
|
new MappingJackson2HttpMessageConverter(snakeCaseMapper));
|
||||||
})
|
})
|
||||||
.build();
|
.build();
|
||||||
this.redisTemplate = redisTemplate;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the daily password from cache or generates a new one.
|
* Retrieves the daily password from cache or generates a new one.
|
||||||
* @return the daily password response
|
* @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() {
|
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;
|
DailyPasswordResponse response;
|
||||||
try {
|
try {
|
||||||
// Fetch from the upstream remote service
|
|
||||||
response = restClient.get()
|
response = restClient.get()
|
||||||
.uri((uriBuilder) -> uriBuilder
|
.uri((uriBuilder) -> uriBuilder
|
||||||
.path("/sjzmm")
|
.path("/sjzmm")
|
||||||
@@ -90,14 +80,6 @@ public class DailyPasswordManager {
|
|||||||
throw new ResponseStatusException(HttpStatus.BAD_GATEWAY, "No daily password data available.");
|
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;
|
return response;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user