← Kursa Dön
📄 Text · 18 min

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: @EnableCaching eklemeyi 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şkenAçıklamaÖrnek
#paramNameMetot parametresi#id, #name
#p0, #p1Parametre index'i#p0 = ilk parametre
#a0, #a1Parametre index'i (alias)#a0 = ilk parametre
#resultMetot dönüş değeri (sadece unless'de)#result.size()
#root.methodMetot referansı#root.method.name
#root.targetTarget obje#root.target.class
#root.cachesCache koleksiyonu#root.caches[0].name
#root.argsTü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: key ve keyGenerator aynı 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:

Özellikconditionunless
Ne zaman değerlendirilir?Metottan önceMetottan sonra
#result kullanılabilir mi?❌ Hayır✅ Evet
true → ne olur?Cache aktifCache'e yazmaz
false → ne olur?Cache bypassCache'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şlemlerinde beforeInvocation = true kullanı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 @Cacheable ve @CachePut birlikte kullanmak. İkisi çelişir — @CachePut her zaman çalıştırmak ister, @Cacheable cache 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

HataAçıklamaDoğ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ışmazPublic yapın
Null cache'lemenull değer cache'lenirse her seferinde null dönerunless = "#result == null"
Mutable objeCache'den dönen obje değiştirilirse cache bozulurImmutable DTO döndürün
Key collisionFarklı metotlar aynı cache name + key üretircache 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, @CachePut ile declarative caching sağlar. SpEL ile dinamik key, condition/unless ile 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.