Spring Cache: @Cacheable, @CacheEvict
Spring Framework, cache işlemlerini annotation tabanlı, provider-bağımsız bir abstraction ile yönetmenizi sağlar. Cache provider olarak Caffeine, Redis, EhCache veya başka bir çözüm kullanabilirsiniz — kodunuz değişmez. Bu derste Spring Cache'in tüm annotation'larını, SpEL ile dinamik key oluşturmayı, koşullu caching'i ve CacheManager konfigürasyonunu derinlemesine öğreneceğiz.
Cache'i Aktifleştirme
@Configuration
@EnableCaching // Bu annotation olmadan hiçbir cache annotation'ı çalışmaz!
public class CacheConfig {
// Spring Boot auto-configuration ile provider otomatik algılanır
// Caffeine dependency varsa → CaffeineCacheManager
// Redis dependency varsa → RedisCacheManager
// Hiçbiri yoksa → ConcurrentMapCacheManager (development için)
}⚠️ Yaygın Hata:
@EnableCachingeklemeyi unutmak. Annotation'lar sessizce çalışmaz, hata da vermez — sadece cache'leme yapılmaz.
@Cacheable — Sonucu Cache'le
@Cacheable bir metot sonucunu cache'ler. Aynı parametrelerle tekrar çağrıldığında metot çalıştırılmaz, cache'den döner.
@Service
@RequiredArgsConstructor
public class ProductService {
private final ProductRepository productRepository;
// Temel kullanım: key otomatik olarak metot parametresinden üretilir
@Cacheable("products")
public Product getProduct(Long id) {
log.info("DB'den okuyor: {}", id); // Sadece ilk çağrıda görünür
return productRepository.findById(id)
.orElseThrow(() -> new ProductNotFoundException(id));
}
// Açık key belirtme (SpEL)
@Cacheable(value = "products", key = "#id")
public ProductDTO getProductDetail(Long id) {
return productRepository.findDetailById(id);
}
// Birden fazla parametreli key
@Cacheable(value = "productSearch",
key = "#category + ':' + #page + ':' + #size")
public Page<Product> searchProducts(String category, int page, int size) {
return productRepository.findByCategory(category, PageRequest.of(page, size));
}
}SpEL ile Dinamik Cache Key
Spring Expression Language (SpEL), cache key'lerini dinamik olarak oluşturmanızı sağlar:
@Service
public class UserService {
// Metot parametresi ile
@Cacheable(value = "users", key = "#userId")
public User getUser(Long userId) { ... }
// Obje property'si ile
@Cacheable(value = "users", key = "#request.email")
public User findByEmail(UserSearchRequest request) { ... }
// String concatenation
@Cacheable(value = "users", key = "'user:' + #id + ':profile'")
public UserProfile getProfile(Long id) { ... }
// Birden fazla parametre kombine
@Cacheable(value = "reports",
key = "#department + ':' + #year + ':' + #month")
public Report getMonthlyReport(String department, int year, int month) { ... }
// Root obje erişimi — metot adı, target sınıfı vb.
@Cacheable(value = "generic",
key = "#root.targetClass.simpleName + ':' + #root.methodName + ':' + #id")
public Object getGeneric(Long id) { ... }
}Kullanılabilir SpEL değişkenleri:
| Değişken | Açıklama | Örnek |
|---|---|---|
#paramName | Metot parametresi | #id, #name |
#p0, #p1 | Parametre index'i | #p0 = ilk parametre |
#a0, #a1 | Parametre index'i (alias) | #a0 = ilk parametre |
#result | Metot dönüş değeri (sadece unless'de) | #result.size() |
#root.method | Metot referansı | #root.method.name |
#root.target | Target obje | #root.target.class |
#root.caches | Cache koleksiyonu | #root.caches[0].name |
#root.args | Tüm argümanlar | #root.args[0] |
Custom KeyGenerator
Karmaşık key mantığı için custom KeyGenerator tanımlayabilirsiniz:
@Configuration
public class CacheConfig {
@Bean("customKeyGenerator")
public KeyGenerator keyGenerator() {
return (target, method, params) -> {
StringBuilder sb = new StringBuilder();
sb.append(target.getClass().getSimpleName()).append(":");
sb.append(method.getName()).append(":");
for (Object param : params) {
sb.append(param != null ? param.toString() : "null").append(":");
}
return sb.toString();
};
}
}
// Kullanım
@Cacheable(value = "products", keyGenerator = "customKeyGenerator")
public Product getProduct(Long id) { ... }⚠️ Dikkat:
keyvekeyGeneratoraynı anda kullanılamaz. Birini seçin.
Conditional Caching: condition ve unless
condition ve unless parametreleri ile cache'leme koşullu yapılabilir. İkisi birbirinden farklıdır:
`condition` — Cache mekanizması devreye girsin mi? (metot çalışmadan önce değerlendirilir) `unless` — Sonuç cache'e yazılmasın mı? (metot çalıştıktan sonra değerlendirilir)
@Service
public class ProductService {
// condition: Sadece id > 0 olan istekleri cache'le
// id <= 0 ise cache mekanizması hiç devreye girmez (her seferinde DB'ye gider)
@Cacheable(value = "products", key = "#id",
condition = "#id > 0")
public Product getProduct(Long id) {
return productRepository.findById(id).orElseThrow();
}
// unless: Sonuç boşsa cache'leme (ama cache'den okumayı engelleme)
@Cacheable(value = "productSearch", key = "#name",
unless = "#result == null || #result.isEmpty()")
public List<Product> searchByName(String name) {
return productRepository.findByNameContaining(name);
}
// İkisi birlikte: Sadece 3+ karakterli aramalar cache'lensin
// VE sadece sonuç 100'den az ise cache'e yazılsın
@Cacheable(value = "productSearch",
key = "#name",
condition = "#name.length() >= 3",
unless = "#result.size() > 100")
public List<Product> search(String name) {
return productRepository.findByNameContaining(name);
}
// Fiyatı 0 olan ürünleri cache'leme (muhtemelen hatalı veri)
@Cacheable(value = "products", key = "#id",
unless = "#result.price.compareTo(BigDecimal.ZERO) == 0")
public Product getProductSafe(Long id) {
return productRepository.findById(id).orElseThrow();
}
}condition vs unless karşılaştırma:
| Özellik | condition | unless |
|---|---|---|
| Ne zaman değerlendirilir? | Metottan önce | Metottan sonra |
#result kullanılabilir mi? | ❌ Hayır | ✅ Evet |
true → ne olur? | Cache aktif | Cache'e yazmaz |
false → ne olur? | Cache bypass | Cache'e yazar |
@CacheEvict — Cache'den Sil
Veri güncellendiğinde veya silindiğinde eski cache entry'sini temizlemeniz gerekir:
@Service
public class ProductService {
// Tek bir entry sil
@CacheEvict(value = "products", key = "#id")
public void deleteProduct(Long id) {
productRepository.deleteById(id);
}
// Tüm cache'i temizle
@CacheEvict(value = "products", allEntries = true)
public void clearProductCache() {
log.info("Product cache temizlendi");
}
// Metot çalışmadan ÖNCE cache'i temizle
// (metot exception fırlatsa bile cache temizlenmiş olur)
@CacheEvict(value = "products", key = "#id", beforeInvocation = true)
public void deleteProductForce(Long id) {
productRepository.deleteById(id); // Bu exception fırlatsa bile cache temiz
}
// Birden fazla cache temizle
@Caching(evict = {
@CacheEvict(value = "products", key = "#product.id"),
@CacheEvict(value = "productSearch", allEntries = true),
@CacheEvict(value = "productCount", allEntries = true)
})
public void updateProduct(Product product) {
productRepository.save(product);
}
}⚠️ beforeInvocation: Default
false'tur. Metot exception fırlatırsa cache temizlenmez. Kritik silme işlemlerindebeforeInvocation = truekullanın.
@CachePut — Cache'i Güncelle
@CachePut, metodu her zaman çalıştırır ve sonucu cache'e yazar. @Cacheable'dan farkı: @Cacheable cache hit varsa metodu atlar, @CachePut her zaman çalıştırır.
@Service
public class ProductService {
// Ürün güncelleme — DB'ye yaz ve cache'i güncelle
@CachePut(value = "products", key = "#product.id")
public Product updateProduct(Product product) {
return productRepository.save(product);
}
// Yeni ürün oluştur — DB'ye yaz ve hemen cache'e ekle
@CachePut(value = "products", key = "#result.id")
public Product createProduct(ProductCreateRequest request) {
Product product = Product.builder()
.name(request.getName())
.price(request.getPrice())
.build();
return productRepository.save(product);
}
}⚠️ Yaygın Hata: Aynı metotta
@Cacheableve@CachePutbirlikte kullanmak. İkisi çelişir —@CachePuther zaman çalıştırmak ister,@Cacheablecache hit'te atlamak ister. Birini seçin.
@Caching — Composite Operations
Birden fazla cache operasyonunu tek metotta birleştirmek için:
@Caching(
cacheable = {
@Cacheable(value = "users", key = "#id")
},
evict = {
@CacheEvict(value = "userList", allEntries = true)
}
)
public User getUser(Long id) {
return userRepository.findById(id).orElseThrow();
}
// Tipik senaryo: ürün güncellenince ilgili tüm cache'leri temizle
@Caching(
put = { @CachePut(value = "products", key = "#result.id") },
evict = {
@CacheEvict(value = "productSearch", allEntries = true),
@CacheEvict(value = "categoryProducts", key = "#result.categoryId")
}
)
public Product updateProduct(Product product) {
return productRepository.save(product);
}@CacheConfig — Sınıf Düzeyinde Konfigürasyon
Her metotta cache name tekrarlamak yerine sınıf düzeyinde tanımlayın:
@Service
@CacheConfig(cacheNames = "products") // Tüm metotlar için default cache name
public class ProductService {
@Cacheable(key = "#id") // value = "products" otomatik
public Product getProduct(Long id) { ... }
@CacheEvict(key = "#id") // value = "products" otomatik
public void deleteProduct(Long id) { ... }
@CachePut(key = "#product.id") // value = "products" otomatik
public Product updateProduct(Product product) { ... }
}CacheManager Türleri
Spring Boot, classpath'teki dependency'ye göre otomatik CacheManager seçer:
// 1. ConcurrentMapCacheManager — Development için
// Dependency gerekmez, Spring Boot default
// Tek JVM, eviction/TTL yok
@Bean
public CacheManager concurrentMapCacheManager() {
return new ConcurrentMapCacheManager("products", "users");
}
// 2. CaffeineCacheManager — Single instance production
// com.github.ben-manes.caffeine:caffeine
@Bean
public CacheManager caffeineCacheManager() {
CaffeineCacheManager manager = new CaffeineCacheManager();
manager.setCaffeine(Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(Duration.ofMinutes(30))
.recordStats());
return manager;
}
// 3. RedisCacheManager — Distributed (microservice)
// spring-boot-starter-data-redis
@Bean
public RedisCacheManager redisCacheManager(RedisConnectionFactory factory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(30))
.disableCachingNullValues()
.serializeValuesWith(
SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
return RedisCacheManager.builder(factory)
.cacheDefaults(config)
.withCacheConfiguration("products",
config.entryTtl(Duration.ofHours(1))) // products için özel TTL
.withCacheConfiguration("sessions",
config.entryTtl(Duration.ofMinutes(5))) // sessions için özel TTL
.build();
}Cache Bazında Farklı TTL (Redis)
@Configuration
@EnableCaching
public class RedisCacheConfig {
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory factory) {
// Default konfigürasyon
RedisCacheConfiguration defaultConfig = RedisCacheConfiguration
.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(30))
.disableCachingNullValues();
// Cache bazında özel konfigürasyon
Map<String, RedisCacheConfiguration> cacheConfigs = Map.of(
"products", defaultConfig.entryTtl(Duration.ofHours(2)),
"users", defaultConfig.entryTtl(Duration.ofMinutes(15)),
"sessions", defaultConfig.entryTtl(Duration.ofMinutes(5)),
"reports", defaultConfig.entryTtl(Duration.ofHours(24))
);
return RedisCacheManager.builder(factory)
.cacheDefaults(defaultConfig)
.withInitialCacheConfigurations(cacheConfigs)
.transactionAware() // Spring @Transactional ile uyumlu
.build();
}
}Cache Hata Yönetimi
Cache provider çökerse (Redis down vb.) uygulamanız da çökmemeli:
@Configuration
public class CacheErrorConfig extends CachingConfigurerSupport {
@Override
public CacheErrorHandler errorHandler() {
return new CacheErrorHandler() {
@Override
public void handleCacheGetError(RuntimeException e,
Cache cache, Object key) {
log.warn("Cache GET hatası [{}:{}]: {}",
cache.getName(), key, e.getMessage());
// Exception'ı yutuyoruz — DB'ye fallback olacak
}
@Override
public void handleCachePutError(RuntimeException e,
Cache cache, Object key, Object value) {
log.warn("Cache PUT hatası [{}:{}]: {}",
cache.getName(), key, e.getMessage());
}
@Override
public void handleCacheEvictError(RuntimeException e,
Cache cache, Object key) {
log.warn("Cache EVICT hatası [{}:{}]: {}",
cache.getName(), key, e.getMessage());
}
@Override
public void handleCacheClearError(RuntimeException e, Cache cache) {
log.warn("Cache CLEAR hatası [{}]: {}",
cache.getName(), e.getMessage());
}
};
}
}Yaygın Hatalar ve Anti-Pattern'lar
| Hata | Açıklama | Doğrusu |
|---|---|---|
| Aynı sınıf içi çağrı | this.getProduct() cache'i bypass eder (proxy!) | Farklı bean'den çağırın veya self-injection |
| Private metot | @Cacheable private metotta çalışmaz | Public yapın |
| Null cache'leme | null değer cache'lenirse her seferinde null döner | unless = "#result == null" |
| Mutable obje | Cache'den dönen obje değiştirilirse cache bozulur | Immutable DTO döndürün |
| Key collision | Farklı metotlar aynı cache name + key üretir | cache name'i ayırın veya key prefix ekleyin |
// ❌ YANLIŞ: Aynı sınıf içi çağrı — cache çalışmaz!
@Service
public class ProductService {
public ProductDTO getProductDTO(Long id) {
Product product = getProduct(id); // Cache ÇALIŞMAZ (proxy bypass)
return mapper.toDTO(product);
}
@Cacheable("products")
public Product getProduct(Long id) {
return repository.findById(id).orElseThrow();
}
}
// ✅ DOĞRU: Self-injection ile çözüm
@Service
public class ProductService {
@Lazy @Autowired
private ProductService self; // Proxy referansı
public ProductDTO getProductDTO(Long id) {
Product product = self.getProduct(id); // Cache ÇALIŞIR
return mapper.toDTO(product);
}
@Cacheable("products")
public Product getProduct(Long id) {
return repository.findById(id).orElseThrow();
}
}💡 Özet: Spring Cache Abstraction,
@Cacheable,@CacheEvict,@CachePutile declarative caching sağlar. SpEL ile dinamik key,condition/unlessile koşullu caching yapılır. CacheManager ile provider seçilir. Cache hata yönetimi ile resilience sağlanır. Aynı sınıf içi çağrılarda proxy bypass'a dikkat edin.
AI Asistan
Sorularını yanıtlamaya hazır