Initial commit
This commit is contained in:
579
skills/spring-boot-cache/references/cache-core-reference.md
Normal file
579
skills/spring-boot-cache/references/cache-core-reference.md
Normal file
@@ -0,0 +1,579 @@
|
||||
# Spring Boot Cache Abstraction - References
|
||||
|
||||
Complete API reference and external resources for Spring Boot caching.
|
||||
|
||||
## Spring Cache Abstraction API Reference
|
||||
|
||||
### Core Interfaces
|
||||
|
||||
#### CacheManager
|
||||
Interface for managing cache instances.
|
||||
|
||||
```java
|
||||
public interface CacheManager {
|
||||
// Get a cache by name
|
||||
Cache getCache(String name);
|
||||
|
||||
// Get all available cache names
|
||||
Collection<String> getCacheNames();
|
||||
}
|
||||
```
|
||||
|
||||
**Common Implementations:**
|
||||
- `ConcurrentMapCacheManager` - In-memory, thread-safe caching
|
||||
- `SimpleCacheManager` - Simple static cache configuration
|
||||
- `CaffeineCacheManager` - High-performance caching with Caffeine library
|
||||
- `EhCacheManager` - Enterprise caching with EhCache
|
||||
- `RedisCacheManager` - Distributed caching with Redis
|
||||
|
||||
#### Cache
|
||||
Interface representing a single cache.
|
||||
|
||||
```java
|
||||
public interface Cache {
|
||||
// Get cache name
|
||||
String getName();
|
||||
|
||||
// Get native cache implementation
|
||||
Object getNativeCache();
|
||||
|
||||
// Get value by key
|
||||
ValueWrapper get(Object key);
|
||||
|
||||
// Put value in cache
|
||||
void put(Object key, Object value);
|
||||
|
||||
// Remove entry from cache
|
||||
void evict(Object key);
|
||||
|
||||
// Clear entire cache
|
||||
void clear();
|
||||
}
|
||||
```
|
||||
|
||||
### Cache Annotations
|
||||
|
||||
| Annotation | Purpose | Target | Parameters |
|
||||
|-----------|---------|--------|-----------|
|
||||
| `@Cacheable` | Cache method result before execution | Methods | `value`, `key`, `condition`, `unless` |
|
||||
| `@CachePut` | Always execute, then cache result | Methods | `value`, `key`, `condition`, `unless` |
|
||||
| `@CacheEvict` | Remove entry/entries from cache | Methods | `value`, `key`, `allEntries`, `condition`, `beforeInvocation` |
|
||||
| `@Caching` | Combine multiple cache operations | Methods | `cacheable`, `put`, `evict` |
|
||||
| `@CacheConfig` | Class-level cache configuration | Classes | `cacheNames` |
|
||||
| `@EnableCaching` | Enable caching support | Configuration classes | None |
|
||||
|
||||
### Annotation Parameters
|
||||
|
||||
#### value / cacheNames
|
||||
Name(s) of the cache(s) to use.
|
||||
|
||||
```java
|
||||
@Cacheable(value = "products") // Single cache
|
||||
@Cacheable(value = {"products", "inventory"}) // Multiple caches
|
||||
```
|
||||
|
||||
#### key
|
||||
SpEL expression to generate cache key (if not using method parameters as key).
|
||||
|
||||
```java
|
||||
@Cacheable(value = "products", key = "#id")
|
||||
@Cacheable(value = "products", key = "#p0") // First parameter
|
||||
@Cacheable(value = "products", key = "#root.methodName + #id")
|
||||
@Cacheable(value = "products", key = "T(java.util.Objects).hash(#id, #name)")
|
||||
```
|
||||
|
||||
**SpEL Context Variables:**
|
||||
- `#root.methodName` - Method name being invoked
|
||||
- `#root.method` - Method object
|
||||
- `#root.target` - Target object
|
||||
- `#root.targetClass` - Target class
|
||||
- `#root.args[0]` - Method arguments array
|
||||
- `#a0`, `#p0` - First argument
|
||||
- `#result` - Method result (only in @CachePut, @CacheEvict)
|
||||
|
||||
#### condition
|
||||
SpEL expression evaluated before cache operation. Operation only executes if true.
|
||||
|
||||
```java
|
||||
@Cacheable(value = "products", condition = "#id > 0")
|
||||
@Cacheable(value = "products", condition = "#price > 100 && #active == true")
|
||||
@Cacheable(value = "products", condition = "#size() > 0") // For collections
|
||||
```
|
||||
|
||||
#### unless
|
||||
SpEL expression evaluated AFTER method execution. Entry is cached only if false.
|
||||
|
||||
```java
|
||||
@Cacheable(value = "products", unless = "#result == null")
|
||||
@CachePut(value = "products", unless = "#result.isPrivate()")
|
||||
```
|
||||
|
||||
#### beforeInvocation
|
||||
For @CacheEvict only. If true, cache is evicted BEFORE method execution (default: false).
|
||||
|
||||
```java
|
||||
@CacheEvict(value = "products", beforeInvocation = true) // Evict before call
|
||||
@CacheEvict(value = "products", beforeInvocation = false) // Evict after call
|
||||
```
|
||||
|
||||
#### allEntries
|
||||
For @CacheEvict only. If true, entire cache is cleared instead of single entry.
|
||||
|
||||
```java
|
||||
@CacheEvict(value = "products", allEntries = true) // Clear all entries
|
||||
```
|
||||
|
||||
## Configuration Reference
|
||||
|
||||
### Maven Dependencies
|
||||
|
||||
```xml
|
||||
<!-- Spring Cache Starter -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-cache</artifactId>
|
||||
<version>3.5.6</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Caffeine (Optional, for advanced caching) -->
|
||||
<dependency>
|
||||
<groupId>com.github.ben-manes.caffeine</groupId>
|
||||
<artifactId>caffeine</artifactId>
|
||||
<version>3.1.6</version>
|
||||
</dependency>
|
||||
|
||||
<!-- EhCache (Optional, for distributed caching) -->
|
||||
<dependency>
|
||||
<groupId>javax.cache</groupId>
|
||||
<artifactId>cache-api</artifactId>
|
||||
<version>1.1.1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.ehcache</groupId>
|
||||
<artifactId>ehcache</artifactId>
|
||||
<version>3.10.8</version>
|
||||
<classifier>jakarta</classifier>
|
||||
</dependency>
|
||||
|
||||
<!-- Redis (Optional, for distributed caching) -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-redis</artifactId>
|
||||
<version>3.5.6</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
### Gradle Dependencies
|
||||
|
||||
```gradle
|
||||
dependencies {
|
||||
// Spring Cache Starter
|
||||
implementation 'org.springframework.boot:spring-boot-starter-cache:3.5.6'
|
||||
|
||||
// Caffeine
|
||||
implementation 'com.github.ben-manes.caffeine:caffeine:3.1.6'
|
||||
|
||||
// EhCache
|
||||
implementation 'javax.cache:cache-api:1.1.1'
|
||||
implementation 'org.ehcache:ehcache:3.10.8'
|
||||
|
||||
// Redis
|
||||
implementation 'org.springframework.boot:spring-boot-starter-data-redis:3.5.6'
|
||||
}
|
||||
```
|
||||
|
||||
### Application Properties (application.properties)
|
||||
|
||||
```properties
|
||||
# General Caching Configuration
|
||||
spring.cache.type=simple # Type: simple, redis, caffeine, ehcache, jcache
|
||||
|
||||
# Caffeine Configuration
|
||||
spring.cache.caffeine.spec=maximumSize=1000,expireAfterWrite=10m
|
||||
spring.cache.cache-names=products,users,orders
|
||||
|
||||
# Redis Configuration
|
||||
spring.data.redis.host=localhost
|
||||
spring.data.redis.port=6379
|
||||
spring.data.redis.password=
|
||||
spring.cache.redis.time-to-live=600000 # 10 minutes in ms
|
||||
|
||||
# EhCache Configuration
|
||||
spring.cache.jcache.config=classpath:ehcache.xml
|
||||
```
|
||||
|
||||
### Application Properties (application.yml)
|
||||
|
||||
```yaml
|
||||
spring:
|
||||
cache:
|
||||
type: simple
|
||||
cache-names:
|
||||
- products
|
||||
- users
|
||||
- orders
|
||||
|
||||
caffeine:
|
||||
spec: maximumSize=1000,expireAfterWrite=10m
|
||||
|
||||
redis:
|
||||
time-to-live: 600000 # 10 minutes in ms
|
||||
|
||||
jcache:
|
||||
config: classpath:ehcache.xml
|
||||
```
|
||||
|
||||
## Performance Tuning Reference
|
||||
|
||||
### Cache Types Comparison
|
||||
|
||||
| Type | Use Case | Memory | Thread-Safe | Distributed |
|
||||
|------|----------|--------|------------|-------------|
|
||||
| Simple | Local, small data | Low | Yes | No |
|
||||
| Caffeine | High-performance local | Medium | Yes | No |
|
||||
| EhCache | Enterprise local | High | Yes | Optional |
|
||||
| Redis | Distributed, large | External | Yes | Yes |
|
||||
|
||||
### Performance Tips
|
||||
|
||||
**1. Key Generation Strategy:**
|
||||
```java
|
||||
// Fast (uses method parameters directly)
|
||||
@Cacheable(value = "products") // Uses all parameters as key
|
||||
@Cacheable(value = "products", key = "#id") // Specific parameter
|
||||
|
||||
// Slower (computed SpEL)
|
||||
@Cacheable(value = "products", key = "T(java.util.Objects).hash(#id, #name)")
|
||||
```
|
||||
|
||||
**2. Cache Size Tuning:**
|
||||
```properties
|
||||
# Caffeine: Set appropriate maximumSize
|
||||
spring.cache.caffeine.spec=maximumSize=10000,expireAfterWrite=15m
|
||||
|
||||
# Redis: Monitor memory usage
|
||||
# MEMORY STATS command in Redis CLI
|
||||
```
|
||||
|
||||
**3. TTL Configuration:**
|
||||
```properties
|
||||
# Redis: TTL in milliseconds
|
||||
spring.cache.redis.time-to-live=600000 # 10 minutes
|
||||
|
||||
# Caffeine: In spec
|
||||
spring.cache.caffeine.spec=expireAfterWrite=10m
|
||||
```
|
||||
|
||||
## Spring Boot Auto-Configuration
|
||||
|
||||
### Auto-Detected Cache Managers
|
||||
|
||||
Spring Boot auto-configures a CacheManager based on classpath presence (in priority order):
|
||||
|
||||
1. **Redis** - if `spring-boot-starter-data-redis` is present
|
||||
2. **Caffeine** - if `caffeine` library is present
|
||||
3. **EhCache** - if `ehcache` library is present
|
||||
4. **Simple** - default in-memory caching
|
||||
|
||||
To explicitly set the cache type:
|
||||
```properties
|
||||
spring.cache.type=redis
|
||||
```
|
||||
|
||||
### Conditional Bean Creation
|
||||
|
||||
```java
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(CacheManager.class)
|
||||
public CacheManager cacheManager() {
|
||||
return new ConcurrentMapCacheManager("products", "users");
|
||||
}
|
||||
```
|
||||
|
||||
## Transaction Integration
|
||||
|
||||
### Cache + @Transactional Interaction
|
||||
|
||||
```java
|
||||
@Service
|
||||
@Transactional
|
||||
public class ProductService {
|
||||
|
||||
@Cacheable(value = "products", key = "#id")
|
||||
@Transactional(readOnly = true) // Combines with cache
|
||||
public Product getProduct(Long id) {
|
||||
return productRepository.findById(id).orElse(null);
|
||||
}
|
||||
|
||||
@CachePut(value = "products", key = "#product.id")
|
||||
@Transactional // Ensure atomicity of save + cache update
|
||||
public Product updateProduct(Product product) {
|
||||
return productRepository.save(product);
|
||||
}
|
||||
|
||||
@CacheEvict(value = "products", key = "#id")
|
||||
@Transactional
|
||||
public void deleteProduct(Long id) {
|
||||
productRepository.deleteById(id);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Monitoring and Metrics
|
||||
|
||||
### Spring Boot Actuator Integration
|
||||
|
||||
```properties
|
||||
# Enable caching metrics
|
||||
management.endpoints.web.exposure.include=metrics,health
|
||||
|
||||
# View cache metrics
|
||||
GET http://localhost:8080/actuator/metrics
|
||||
GET http://localhost:8080/actuator/metrics/cache.hits
|
||||
GET http://localhost:8080/actuator/metrics/cache.misses
|
||||
```
|
||||
|
||||
### Custom Cache Metrics
|
||||
|
||||
```java
|
||||
@Component
|
||||
public class CacheMetricsCollector {
|
||||
private final MeterRegistry meterRegistry;
|
||||
|
||||
public void recordCacheHit(String cacheName) {
|
||||
meterRegistry.counter("cache.hits", "cache", cacheName).increment();
|
||||
}
|
||||
|
||||
public void recordCacheMiss(String cacheName) {
|
||||
meterRegistry.counter("cache.misses", "cache", cacheName).increment();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## EhCache XML Configuration Reference
|
||||
|
||||
### ehcache.xml Structure
|
||||
|
||||
```xml
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns="http://www.ehcache.org/v3"
|
||||
xmlns:jsr107="http://www.ehcache.org/v3/jsr107"
|
||||
xsi:schemaLocation="
|
||||
http://www.ehcache.org/v3 http://www.ehcache.org/schema/ehcache-core-3.0.xsd
|
||||
http://www.ehcache.org/v3/jsr107 http://www.ehcache.org/schema/ehcache-107-ext-3.0.xsd">
|
||||
|
||||
<!-- Cache Configuration -->
|
||||
<cache alias="cacheName">
|
||||
<key-type>java.lang.Long</key-type>
|
||||
<value-type>com.example.Product</value-type>
|
||||
|
||||
<!-- Time to Live -->
|
||||
<expiry>
|
||||
<ttl unit="minutes">30</ttl>
|
||||
</expiry>
|
||||
|
||||
<!-- Storage Configuration -->
|
||||
<resources>
|
||||
<heap unit="entries">1000</heap>
|
||||
<offheap unit="MB">50</offheap>
|
||||
<disk unit="GB">1</disk>
|
||||
</resources>
|
||||
|
||||
<!-- Listeners (optional) -->
|
||||
<listeners>
|
||||
<listener>
|
||||
<class>com.example.CustomCacheEventListener</class>
|
||||
<event-firing-mode>ASYNCHRONOUS</event-firing-mode>
|
||||
<events-to-fire-on>CREATED</events-to-fire-on>
|
||||
<events-to-fire-on>EXPIRED</events-to-fire-on>
|
||||
</listener>
|
||||
</listeners>
|
||||
</cache>
|
||||
</config>
|
||||
```
|
||||
|
||||
### Common EhCache Attributes
|
||||
|
||||
- `heap` - On-heap memory storage (fast, limited)
|
||||
- `offheap` - Off-heap memory storage (slower, larger)
|
||||
- `disk` - Disk storage (slowest, unlimited)
|
||||
- `ttl` - Time to live before expiration
|
||||
- `idle` - Time to idle before expiration (if not accessed)
|
||||
|
||||
## Common Pitfalls and Solutions
|
||||
|
||||
### Problem 1: Cache Not Working
|
||||
|
||||
**Symptoms:** Cache is never hit, always querying database.
|
||||
|
||||
**Causes & Solutions:**
|
||||
```java
|
||||
// Problem: @Cacheable on public method called from same bean
|
||||
@Service
|
||||
public class ProductService {
|
||||
@Cacheable("products")
|
||||
public Product get(Long id) { }
|
||||
|
||||
public Product getDetails(Long id) {
|
||||
return this.get(id); // ❌ Won't use cache (no proxy)
|
||||
}
|
||||
}
|
||||
|
||||
// Solution: Inject service or call through interface
|
||||
@Service
|
||||
public class DetailsService {
|
||||
@Autowired
|
||||
private ProductService productService;
|
||||
|
||||
public Product getDetails(Long id) {
|
||||
return productService.get(id); // ✅ Uses cache
|
||||
}
|
||||
}
|
||||
|
||||
// Problem: Caching non-serializable objects with Redis
|
||||
@Cacheable("products")
|
||||
public Product get(Long id) {
|
||||
Product p = new Product();
|
||||
p.setConnection(dbConnection); // ❌ Not serializable
|
||||
return p;
|
||||
}
|
||||
|
||||
// Solution: Ensure all cached objects are serializable
|
||||
@Cacheable("products")
|
||||
public ProductDTO get(Long id) {
|
||||
return mapper.toDTO(productRepository.findById(id)); // ✅ DTO is serializable
|
||||
}
|
||||
```
|
||||
|
||||
### Problem 2: Stale Cache Data
|
||||
|
||||
**Symptoms:** Updates aren't reflected in cached data.
|
||||
|
||||
**Solution:**
|
||||
```java
|
||||
// Always evict cache on update
|
||||
@CacheEvict(value = "products", key = "#id")
|
||||
public void updateProduct(Long id, UpdateRequest req) {
|
||||
Product product = productRepository.findById(id).orElseThrow();
|
||||
product.update(req);
|
||||
productRepository.save(product);
|
||||
}
|
||||
|
||||
// Or use @CachePut to keep cache fresh
|
||||
@CachePut(value = "products", key = "#result.id")
|
||||
public Product updateProduct(Long id, UpdateRequest req) {
|
||||
Product product = productRepository.findById(id).orElseThrow();
|
||||
product.update(req);
|
||||
return productRepository.save(product);
|
||||
}
|
||||
```
|
||||
|
||||
### Problem 3: Memory Leak
|
||||
|
||||
**Symptoms:** Memory usage grows unbounded.
|
||||
|
||||
**Solution:**
|
||||
```properties
|
||||
# Configure cache eviction policies
|
||||
spring.cache.caffeine.spec=maximumSize=10000,expireAfterWrite=10m
|
||||
|
||||
# Redis: Set TTL
|
||||
spring.cache.redis.time-to-live=600000
|
||||
|
||||
# Monitor cache size
|
||||
```
|
||||
|
||||
## External Resources
|
||||
|
||||
### Official Documentation
|
||||
|
||||
- [Spring Cache Abstraction](https://docs.spring.io/spring-framework/reference/integration/cache.html)
|
||||
- [Spring Boot Caching Documentation](https://docs.spring.io/spring-boot/docs/current/reference/html/io.html#io.caching)
|
||||
- [Spring Framework Caching Guide](https://spring.io/guides/gs/caching/)
|
||||
|
||||
### Third-Party Libraries
|
||||
|
||||
- [Caffeine Cache](https://github.com/ben-manes/caffeine/wiki)
|
||||
- [EhCache Documentation](https://www.ehcache.org/documentation/3.10/)
|
||||
- [Redis Documentation](https://redis.io/documentation)
|
||||
|
||||
### Related Skills
|
||||
|
||||
- **spring-boot-performance-tuning** - Comprehensive performance optimization
|
||||
- **spring-boot-data-persistence** - Database optimization patterns
|
||||
- **spring-boot-rest-api-standards** - API design with caching headers
|
||||
|
||||
### Useful Articles
|
||||
|
||||
- [Spring Cache Abstraction Tutorial](https://www.baeldung.com/spring-cache-tutorial)
|
||||
- [Redis Caching in Spring Boot](https://www.baeldung.com/spring-boot-redis)
|
||||
- [Cache Stampede Problem](https://en.wikipedia.org/wiki/Cache_stampede)
|
||||
- [Cache Invalidation Strategies](https://martinfowler.com/bliki/TwoHardThings.html)
|
||||
|
||||
## SpEL Reference for Cache Keys
|
||||
|
||||
### Basic Expressions
|
||||
|
||||
```java
|
||||
// Method parameters
|
||||
@Cacheable(key = "#id") // Single parameter
|
||||
@Cacheable(key = "#user.id") // Object property
|
||||
@Cacheable(key = "#root.args[0]") // First argument
|
||||
|
||||
// Composite keys
|
||||
@Cacheable(key = "#id + '-' + #type")
|
||||
@Cacheable(key = "T(java.util.Objects).hash(#id, #type)")
|
||||
|
||||
// Collections
|
||||
@Cacheable(key = "#ids.toString()")
|
||||
@Cacheable(condition = "#ids.size() > 0")
|
||||
```
|
||||
|
||||
### SpEL Context Variables
|
||||
|
||||
| Variable | Description |
|
||||
|----------|-------------|
|
||||
| `#root.method` | Method object |
|
||||
| `#root.methodName` | Method name |
|
||||
| `#root.target` | Target object |
|
||||
| `#root.targetClass` | Target class |
|
||||
| `#root.args` | Arguments array |
|
||||
| `#p<index>` | Argument at index |
|
||||
| `#<name>` | Named argument |
|
||||
| `#result` | Method result (@CachePut, @CacheEvict) |
|
||||
|
||||
## Testing Reference
|
||||
|
||||
### Testing Cache Behavior
|
||||
|
||||
```java
|
||||
@Test
|
||||
void shouldCacheResult() {
|
||||
// Arrange
|
||||
when(repository.find(1L)).thenReturn(mockObject);
|
||||
|
||||
// Act - First call
|
||||
service.get(1L);
|
||||
|
||||
// Assert - Database was queried
|
||||
verify(repository, times(1)).find(1L);
|
||||
|
||||
// Act - Second call
|
||||
service.get(1L);
|
||||
|
||||
// Assert - Database NOT queried again (cache hit)
|
||||
verify(repository, times(1)).find(1L);
|
||||
}
|
||||
```
|
||||
|
||||
### Disabling Cache in Tests
|
||||
|
||||
```java
|
||||
@SpringBootTest
|
||||
@PropertySource("classpath:application-test.properties")
|
||||
class MyServiceTest {
|
||||
// In application-test.properties:
|
||||
// spring.cache.type=none
|
||||
}
|
||||
```
|
||||
617
skills/spring-boot-cache/references/cache-examples.md
Normal file
617
skills/spring-boot-cache/references/cache-examples.md
Normal file
@@ -0,0 +1,617 @@
|
||||
# Spring Boot Cache Abstraction - Examples
|
||||
|
||||
This document provides concrete, progressive examples demonstrating Spring Boot caching patterns from basic to advanced scenarios.
|
||||
|
||||
## Example 1: Basic Product Caching
|
||||
|
||||
A simple e-commerce scenario with product lookup caching.
|
||||
|
||||
### Domain Model
|
||||
|
||||
```java
|
||||
@Getter
|
||||
@ToString
|
||||
@EqualsAndHashCode(of = "id")
|
||||
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class Product {
|
||||
private Long id;
|
||||
private String name;
|
||||
private BigDecimal price;
|
||||
private Integer stock;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
```
|
||||
|
||||
### Service with @Cacheable
|
||||
|
||||
```java
|
||||
@Service
|
||||
@CacheConfig(cacheNames = "products")
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class ProductService {
|
||||
private final ProductRepository productRepository;
|
||||
|
||||
@Cacheable
|
||||
public Product getProductById(Long id) {
|
||||
log.info("Fetching product {} from database", id);
|
||||
return productRepository.findById(id)
|
||||
.orElseThrow(() -> new ResourceNotFoundException("Product not found"));
|
||||
}
|
||||
|
||||
@Cacheable(key = "#name")
|
||||
public Product getProductByName(String name) {
|
||||
log.info("Fetching product by name: {}", name);
|
||||
return productRepository.findByName(name)
|
||||
.orElseThrow(() -> new ResourceNotFoundException("Product not found"));
|
||||
}
|
||||
|
||||
@CachePut(key = "#product.id")
|
||||
public Product updateProduct(Product product) {
|
||||
log.info("Updating product {}", product.getId());
|
||||
return productRepository.save(product);
|
||||
}
|
||||
|
||||
@CacheEvict
|
||||
public void deleteProduct(Long id) {
|
||||
log.info("Deleting product {}", id);
|
||||
productRepository.deleteById(id);
|
||||
}
|
||||
|
||||
@CacheEvict(allEntries = true)
|
||||
public void refreshAllProducts() {
|
||||
log.info("Refreshing all product cache");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Test Example
|
||||
|
||||
```java
|
||||
@SpringBootTest
|
||||
@Testcontainers
|
||||
class ProductServiceCacheTest {
|
||||
|
||||
@Container
|
||||
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15-alpine");
|
||||
|
||||
@Autowired
|
||||
private ProductService productService;
|
||||
|
||||
@SpyBean
|
||||
private ProductRepository productRepository;
|
||||
|
||||
@Test
|
||||
void shouldCacheProductAfterFirstCall() {
|
||||
// Given
|
||||
Product product = Product.builder()
|
||||
.id(1L)
|
||||
.name("Laptop")
|
||||
.price(BigDecimal.valueOf(999.99))
|
||||
.stock(10)
|
||||
.build();
|
||||
|
||||
when(productRepository.findById(1L)).thenReturn(Optional.of(product));
|
||||
|
||||
// When - First call
|
||||
Product result1 = productService.getProductById(1L);
|
||||
|
||||
// Then - Verify database was called
|
||||
verify(productRepository, times(1)).findById(1L);
|
||||
assertThat(result1).isEqualTo(product);
|
||||
|
||||
// When - Second call (should hit cache)
|
||||
Product result2 = productService.getProductById(1L);
|
||||
|
||||
// Then - Database not called again
|
||||
verify(productRepository, times(1)).findById(1L); // Still 1x
|
||||
assertThat(result2).isEqualTo(result1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldEvictCacheOnDelete() {
|
||||
// Given
|
||||
Product product = Product.builder()
|
||||
.id(1L)
|
||||
.name("Laptop")
|
||||
.price(BigDecimal.valueOf(999.99))
|
||||
.build();
|
||||
|
||||
when(productRepository.findById(1L)).thenReturn(Optional.of(product));
|
||||
|
||||
// Populate cache
|
||||
productService.getProductById(1L);
|
||||
verify(productRepository, times(1)).findById(1L);
|
||||
|
||||
// When - Delete (evicts cache)
|
||||
productService.deleteProduct(1L);
|
||||
|
||||
// Then - Next call should query database again
|
||||
when(productRepository.findById(1L)).thenReturn(Optional.empty());
|
||||
assertThatThrownBy(() -> productService.getProductById(1L))
|
||||
.isInstanceOf(ResourceNotFoundException.class);
|
||||
verify(productRepository, times(2)).findById(1L);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Example 2: Conditional Caching with Business Logic
|
||||
|
||||
Cache products only under specific conditions (e.g., only expensive items).
|
||||
|
||||
```java
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class PremiumProductService {
|
||||
private final ProductRepository productRepository;
|
||||
|
||||
@Cacheable(
|
||||
value = "premiumProducts",
|
||||
condition = "#price > 500", // Cache only items over 500
|
||||
unless = "#result == null"
|
||||
)
|
||||
public Product getPremiumProduct(Long id, BigDecimal price) {
|
||||
log.info("Fetching premium product {} (price: {})", id, price);
|
||||
return productRepository.findById(id)
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
@CachePut(
|
||||
value = "discountedProducts",
|
||||
key = "#product.id",
|
||||
condition = "#product.price < 50" // Cache only discounted items
|
||||
)
|
||||
public Product updateDiscountedProduct(Product product) {
|
||||
log.info("Updating discounted product {}", product.getId());
|
||||
return productRepository.save(product);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Test:**
|
||||
|
||||
```java
|
||||
@Test
|
||||
void shouldCachePremiumProductsOnly() {
|
||||
// Given - Cheap product
|
||||
Product cheapProduct = Product.builder()
|
||||
.id(1L)
|
||||
.name("Budget Item")
|
||||
.price(BigDecimal.valueOf(29.99))
|
||||
.build();
|
||||
|
||||
// When - Call with cheap price (won't cache due to condition)
|
||||
Product result = premiumProductService.getPremiumProduct(1L, BigDecimal.valueOf(29.99));
|
||||
|
||||
// Then - Result should be cached (condition false, so not cached)
|
||||
verify(productRepository, times(1)).findById(1L);
|
||||
|
||||
// Second call should hit DB again
|
||||
premiumProductService.getPremiumProduct(1L, BigDecimal.valueOf(29.99));
|
||||
verify(productRepository, times(2)).findById(1L);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Example 3: Multiple Caches and @Caching
|
||||
|
||||
Handle complex scenarios with multiple cache operations.
|
||||
|
||||
```java
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class InventoryService {
|
||||
private final ProductRepository productRepository;
|
||||
|
||||
@Caching(
|
||||
cacheable = @Cacheable("inventoryCache"),
|
||||
put = {
|
||||
@CachePut(value = "stockCache", key = "#id"),
|
||||
@CachePut(value = "priceCache", key = "#id")
|
||||
}
|
||||
)
|
||||
public Product getInventoryDetails(Long id) {
|
||||
log.info("Fetching inventory details for {}", id);
|
||||
return productRepository.findById(id)
|
||||
.orElseThrow(() -> new ResourceNotFoundException("Product not found"));
|
||||
}
|
||||
|
||||
@Caching(
|
||||
evict = {
|
||||
@CacheEvict("inventoryCache"),
|
||||
@CacheEvict("stockCache"),
|
||||
@CacheEvict("priceCache")
|
||||
}
|
||||
)
|
||||
public void reloadInventory(Long id) {
|
||||
log.info("Reloading inventory for {}", id);
|
||||
// Trigger inventory sync from external system
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Example 4: Programmatic Cache Management
|
||||
|
||||
Manually managing caches for advanced scenarios.
|
||||
|
||||
```java
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class CacheManagementService {
|
||||
private final CacheManager cacheManager;
|
||||
|
||||
public void evictProductCache(Long productId) {
|
||||
Cache cache = cacheManager.getCache("products");
|
||||
if (cache != null) {
|
||||
cache.evict(productId);
|
||||
log.info("Evicted product {} from cache", productId);
|
||||
}
|
||||
}
|
||||
|
||||
public void clearAllCaches() {
|
||||
cacheManager.getCacheNames().forEach(cacheName -> {
|
||||
Cache cache = cacheManager.getCache(cacheName);
|
||||
if (cache != null) {
|
||||
cache.clear();
|
||||
log.info("Cleared cache: {}", cacheName);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public <T> T getOrCompute(String cacheName, Object key, Callable<T> valueLoader) {
|
||||
Cache cache = cacheManager.getCache(cacheName);
|
||||
if (cache == null) {
|
||||
log.warn("Cache {} not found", cacheName);
|
||||
return null;
|
||||
}
|
||||
|
||||
Cache.ValueWrapper wrapper = cache.get(key);
|
||||
if (wrapper != null) {
|
||||
return (T) wrapper.get();
|
||||
}
|
||||
|
||||
try {
|
||||
T value = valueLoader.call();
|
||||
cache.put(key, value);
|
||||
return value;
|
||||
} catch (Exception e) {
|
||||
log.error("Error computing cache value", e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Example 5: Cache Warming/Preloading
|
||||
|
||||
Populate cache with frequently accessed data at startup.
|
||||
|
||||
```java
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class CacheWarmupService implements InitializingBean {
|
||||
private final ProductService productService;
|
||||
private final ProductRepository productRepository;
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() {
|
||||
warmupCache();
|
||||
}
|
||||
|
||||
private void warmupCache() {
|
||||
log.info("Warming up product cache...");
|
||||
|
||||
// Load top 100 products
|
||||
List<Product> topProducts = productRepository.findTop100ByOrderByPopularityDesc();
|
||||
topProducts.forEach(product -> {
|
||||
try {
|
||||
productService.getProductById(product.getId());
|
||||
} catch (Exception e) {
|
||||
log.warn("Failed to warm cache for product {}", product.getId(), e);
|
||||
}
|
||||
});
|
||||
|
||||
log.info("Cache warmup completed. {} products cached", topProducts.size());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Example 6: Cache Statistics and Monitoring
|
||||
|
||||
Track cache performance metrics.
|
||||
|
||||
```java
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class CacheStatsService {
|
||||
private final CacheManager cacheManager;
|
||||
|
||||
@Scheduled(fixedRate = 60000) // Every minute
|
||||
public void logCacheStats() {
|
||||
cacheManager.getCacheNames().forEach(cacheName -> {
|
||||
Cache cache = cacheManager.getCache(cacheName);
|
||||
if (cache != null && cache.getNativeCache() instanceof ConcurrentMapCache) {
|
||||
ConcurrentMapCache concreteCache = (ConcurrentMapCache) cache.getNativeCache();
|
||||
log.info("Cache [{}] - Size: {}", cacheName, concreteCache.getStore().size());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@GetMapping("/cache/stats")
|
||||
public ResponseEntity<Map<String, CacheStats>> getCacheStatistics() {
|
||||
Map<String, CacheStats> stats = new HashMap<>();
|
||||
|
||||
cacheManager.getCacheNames().forEach(cacheName -> {
|
||||
Cache cache = cacheManager.getCache(cacheName);
|
||||
if (cache != null) {
|
||||
CacheStats cacheStats = new CacheStats(
|
||||
cacheName,
|
||||
getCacheSize(cache),
|
||||
LocalDateTime.now()
|
||||
);
|
||||
stats.put(cacheName, cacheStats);
|
||||
}
|
||||
});
|
||||
|
||||
return ResponseEntity.ok(stats);
|
||||
}
|
||||
|
||||
private int getCacheSize(Cache cache) {
|
||||
if (cache.getNativeCache() instanceof ConcurrentMap) {
|
||||
return ((ConcurrentMap<?, ?>) cache.getNativeCache()).size();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
class CacheStats {
|
||||
private String cacheName;
|
||||
private int size;
|
||||
private LocalDateTime timestamp;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Example 7: TTL-Based Cache with Scheduled Eviction
|
||||
|
||||
Expire cache entries after a specific time.
|
||||
|
||||
```java
|
||||
@Configuration
|
||||
@EnableCaching
|
||||
@EnableScheduling
|
||||
public class CacheConfig {
|
||||
|
||||
@Bean
|
||||
public CacheManager cacheManager() {
|
||||
return new ConcurrentMapCacheManager("products", "users", "orders");
|
||||
}
|
||||
}
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class CacheExpirationService {
|
||||
private final CacheManager cacheManager;
|
||||
private final Map<String, LocalDateTime> cacheExpirations = new ConcurrentHashMap<>();
|
||||
|
||||
public void setExpiration(String cacheName, Object key, Duration duration) {
|
||||
String expirationKey = cacheName + ":" + key;
|
||||
cacheExpirations.put(expirationKey, LocalDateTime.now().plus(duration));
|
||||
log.info("Set cache expiration for {} after {}", expirationKey, duration);
|
||||
}
|
||||
|
||||
@Scheduled(fixedRate = 5000) // Check every 5 seconds
|
||||
public void evictExpiredEntries() {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
|
||||
cacheExpirations.entrySet()
|
||||
.removeIf(entry -> {
|
||||
if (now.isAfter(entry.getValue())) {
|
||||
String[] parts = entry.getKey().split(":");
|
||||
String cacheName = parts[0];
|
||||
String key = parts[1];
|
||||
|
||||
Cache cache = cacheManager.getCache(cacheName);
|
||||
if (cache != null) {
|
||||
cache.evict(key);
|
||||
log.info("Evicted expired cache entry: {}", entry.getKey());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Example 8: Cache Invalidation Pattern with Events
|
||||
|
||||
Use domain events to invalidate cache across services.
|
||||
|
||||
```java
|
||||
public class ProductUpdatedEvent extends ApplicationEvent {
|
||||
private final Long productId;
|
||||
private final String changeType; // UPDATED, DELETED, CREATED
|
||||
|
||||
public ProductUpdatedEvent(Object source, Long productId, String changeType) {
|
||||
super(source);
|
||||
this.productId = productId;
|
||||
this.changeType = changeType;
|
||||
}
|
||||
}
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class ProductService {
|
||||
private final ProductRepository productRepository;
|
||||
private final ApplicationEventPublisher eventPublisher;
|
||||
|
||||
public Product updateProduct(Long id, UpdateProductRequest request) {
|
||||
Product product = productRepository.findById(id)
|
||||
.orElseThrow(() -> new ResourceNotFoundException("Product not found"));
|
||||
|
||||
product.setName(request.getName());
|
||||
product.setPrice(request.getPrice());
|
||||
Product updated = productRepository.save(product);
|
||||
|
||||
// Publish event to invalidate cache
|
||||
eventPublisher.publishEvent(new ProductUpdatedEvent(this, id, "UPDATED"));
|
||||
|
||||
return updated;
|
||||
}
|
||||
}
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class CacheInvalidationListener {
|
||||
private final CacheManager cacheManager;
|
||||
|
||||
@EventListener
|
||||
public void onProductUpdated(ProductUpdatedEvent event) {
|
||||
log.info("Invalidating cache for product {}", event.getProductId());
|
||||
|
||||
Cache productsCache = cacheManager.getCache("products");
|
||||
if (productsCache != null) {
|
||||
productsCache.evict(event.getProductId());
|
||||
}
|
||||
|
||||
Cache productsListCache = cacheManager.getCache("productsList");
|
||||
if (productsListCache != null) {
|
||||
productsListCache.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Example 9: Distributed Caching with Caffeine
|
||||
|
||||
Using Caffeine for local caching with advanced features.
|
||||
|
||||
```java
|
||||
@Configuration
|
||||
@EnableCaching
|
||||
public class CaffeineCacheConfig {
|
||||
|
||||
@Bean
|
||||
public CacheManager cacheManager() {
|
||||
CaffeineCacheManager cacheManager = new CaffeineCacheManager("products", "users");
|
||||
cacheManager.setCaffeine(Caffeine.newBuilder()
|
||||
.maximumSize(1000)
|
||||
.expireAfterWrite(10, TimeUnit.MINUTES)
|
||||
.recordStats());
|
||||
return cacheManager;
|
||||
}
|
||||
}
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class CacheMetricsService {
|
||||
private final CacheManager cacheManager;
|
||||
|
||||
@GetMapping("/cache/metrics")
|
||||
public ResponseEntity<Map<String, Object>> getCacheMetrics() {
|
||||
Map<String, Object> metrics = new HashMap<>();
|
||||
|
||||
cacheManager.getCacheNames().forEach(cacheName -> {
|
||||
Cache cache = cacheManager.getCache(cacheName);
|
||||
if (cache != null && cache.getNativeCache() instanceof com.github.benmanes.caffeine.cache.Cache) {
|
||||
com.github.benmanes.caffeine.cache.Cache<?, ?> caffeineCache =
|
||||
(com.github.benmanes.caffeine.cache.Cache<?, ?>) cache.getNativeCache();
|
||||
|
||||
com.github.benmanes.caffeine.cache.stats.CacheStats stats = caffeineCache.stats();
|
||||
metrics.put(cacheName, Map.of(
|
||||
"hitCount", stats.hitCount(),
|
||||
"missCount", stats.missCount(),
|
||||
"hitRate", stats.hitRate(),
|
||||
"size", caffeineCache.estimatedSize()
|
||||
));
|
||||
}
|
||||
});
|
||||
|
||||
return ResponseEntity.ok(metrics);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Example 10: Testing Cache-Related Scenarios
|
||||
|
||||
```java
|
||||
@SpringBootTest
|
||||
class CacheIntegrationTest {
|
||||
|
||||
@Autowired
|
||||
private ProductService productService;
|
||||
|
||||
@Autowired
|
||||
private CacheManager cacheManager;
|
||||
|
||||
@MockBean
|
||||
private ProductRepository productRepository;
|
||||
|
||||
@Test
|
||||
void shouldDemonstrateCachingLifecycle() {
|
||||
// Given
|
||||
Product product = Product.builder()
|
||||
.id(1L)
|
||||
.name("Test Product")
|
||||
.price(BigDecimal.TEN)
|
||||
.build();
|
||||
|
||||
when(productRepository.findById(1L)).thenReturn(Optional.of(product));
|
||||
|
||||
// Verify cache is empty
|
||||
Cache cache = cacheManager.getCache("products");
|
||||
assertThat(cache.get(1L)).isNull();
|
||||
|
||||
// First call - populates cache
|
||||
Product result1 = productService.getProductById(1L);
|
||||
verify(productRepository, times(1)).findById(1L);
|
||||
|
||||
// Cache is now populated
|
||||
assertThat(cache.get(1L)).isNotNull();
|
||||
|
||||
// Second call - uses cache
|
||||
Product result2 = productService.getProductById(1L);
|
||||
verify(productRepository, times(1)).findById(1L); // Still 1x
|
||||
assertThat(result1).isEqualTo(result2);
|
||||
|
||||
// Manual eviction
|
||||
cache.evict(1L);
|
||||
assertThat(cache.get(1L)).isNull();
|
||||
|
||||
// Next call queries database again
|
||||
Product result3 = productService.getProductById(1L);
|
||||
verify(productRepository, times(2)).findById(1L);
|
||||
}
|
||||
}
|
||||
```
|
||||
381
skills/spring-boot-cache/references/spring-cache-doc-snippet.md
Normal file
381
skills/spring-boot-cache/references/spring-cache-doc-snippet.md
Normal file
@@ -0,0 +1,381 @@
|
||||
The Spring Framework provides support for transparently adding caching
|
||||
to an application. At its core, the abstraction applies caching to
|
||||
methods, thus reducing the number of executions based on the information
|
||||
available in the cache. The caching logic is applied transparently,
|
||||
without any interference to the invoker. Spring Boot auto-configures the
|
||||
cache infrastructure as long as caching support is enabled by using the
|
||||
`org.springframework.cache.annotation.EnableCaching`[format=annotation]
|
||||
annotation.
|
||||
|
||||
> [!NOTE]
|
||||
> Check the {url-spring-framework-docs}/integration/cache.html[relevant
|
||||
> section] of the Spring Framework reference for more details.
|
||||
|
||||
In a nutshell, to add caching to an operation of your service add the
|
||||
relevant annotation to its method, as shown in the following example:
|
||||
|
||||
include-code::MyMathService[]
|
||||
|
||||
This example demonstrates the use of caching on a potentially costly
|
||||
operation. Before invoking `computePiDecimal`, the abstraction looks for
|
||||
an entry in the `piDecimals` cache that matches the `precision`
|
||||
argument. If an entry is found, the content in the cache is immediately
|
||||
returned to the caller, and the method is not invoked. Otherwise, the
|
||||
method is invoked, and the cache is updated before returning the value.
|
||||
|
||||
> [!CAUTION]
|
||||
> You can also use the standard JSR-107 (JCache) annotations (such as
|
||||
> `javax.cache.annotation.CacheResult`[format=annotation])
|
||||
> transparently. However, we strongly advise you to not mix and match
|
||||
> the Spring Cache and JCache annotations.
|
||||
|
||||
If you do not add any specific cache library, Spring Boot
|
||||
auto-configures a [simple
|
||||
provider](io/caching.xml#io.caching.provider.simple) that uses
|
||||
concurrent maps in memory. When a cache is required (such as
|
||||
`piDecimals` in the preceding example), this provider creates it for
|
||||
you. The simple provider is not really recommended for production usage,
|
||||
but it is great for getting started and making sure that you understand
|
||||
the features. When you have made up your mind about the cache provider
|
||||
to use, please make sure to read its documentation to figure out how to
|
||||
configure the caches that your application uses. Nearly all providers
|
||||
require you to explicitly configure every cache that you use in the
|
||||
application. Some offer a way to customize the default caches defined by
|
||||
the configprop:spring.cache.cache-names[] property.
|
||||
|
||||
> [!TIP]
|
||||
> It is also possible to transparently
|
||||
> {url-spring-framework-docs}/integration/cache/annotations.html#cache-annotations-put[update]
|
||||
> or
|
||||
> {url-spring-framework-docs}/integration/cache/annotations.html#cache-annotations-evict[evict]
|
||||
> data from the cache.
|
||||
|
||||
# Supported Cache Providers
|
||||
|
||||
The cache abstraction does not provide an actual store and relies on
|
||||
abstraction materialized by the
|
||||
`org.springframework.cache.Cache[] and
|
||||
`org.springframework.cache.CacheManager[] interfaces.
|
||||
|
||||
If you have not defined a bean of type
|
||||
`org.springframework.cache.CacheManager[] or a
|
||||
`org.springframework.cache.interceptor.CacheResolver[] named
|
||||
`cacheResolver` (see
|
||||
`org.springframework.cache.annotation.CachingConfigurer[]),
|
||||
Spring Boot tries to detect the following providers (in the indicated
|
||||
order):
|
||||
|
||||
1. [io/caching.xml](io/caching.xml#io.caching.provider.generic)
|
||||
|
||||
2. [io/caching.xml](io/caching.xml#io.caching.provider.jcache) (EhCache
|
||||
3, Hazelcast, Infinispan, and others)
|
||||
|
||||
3. [io/caching.xml](io/caching.xml#io.caching.provider.hazelcast)
|
||||
|
||||
4. [io/caching.xml](io/caching.xml#io.caching.provider.infinispan)
|
||||
|
||||
5. [io/caching.xml](io/caching.xml#io.caching.provider.couchbase)
|
||||
|
||||
6. [io/caching.xml](io/caching.xml#io.caching.provider.redis)
|
||||
|
||||
7. [io/caching.xml](io/caching.xml#io.caching.provider.caffeine)
|
||||
|
||||
8. [io/caching.xml](io/caching.xml#io.caching.provider.cache2k)
|
||||
|
||||
9. [io/caching.xml](io/caching.xml#io.caching.provider.simple)
|
||||
|
||||
Additionally, {url-spring-boot-for-apache-geode-site}[Spring Boot for
|
||||
Apache Geode] provides
|
||||
{url-spring-boot-for-apache-geode-docs}#geode-caching-provider[auto-configuration
|
||||
for using Apache Geode as a cache provider].
|
||||
|
||||
> [!TIP]
|
||||
> If the `org.springframework.cache.CacheManager[] is
|
||||
> auto-configured by Spring Boot, it is possible to *force* a particular
|
||||
> cache provider by setting the configprop:spring.cache.type[]
|
||||
> property. Use this property if you need to [use no-op
|
||||
> caches](io/caching.xml#io.caching.provider.none) in certain
|
||||
> environments (such as tests).
|
||||
|
||||
> [!TIP]
|
||||
> Use the `spring-boot-starter-cache` starter to quickly add basic
|
||||
> caching dependencies. The starter brings in `spring-context-support`.
|
||||
> If you add dependencies manually, you must include
|
||||
> `spring-context-support` in order to use the JCache or Caffeine
|
||||
> support.
|
||||
|
||||
If the `org.springframework.cache.CacheManager[] is
|
||||
auto-configured by Spring Boot, you can further tune its configuration
|
||||
before it is fully initialized by exposing a bean that implements the
|
||||
`org.springframework.boot.autoconfigure.cache.CacheManagerCustomizer[]
|
||||
interface. The following example sets a flag to say that `null` values
|
||||
should not be passed down to the underlying map:
|
||||
|
||||
include-code::MyCacheManagerConfiguration[]
|
||||
|
||||
> [!NOTE]
|
||||
> In the preceding example, an auto-configured
|
||||
> `org.springframework.cache.concurrent.ConcurrentMapCacheManager[]
|
||||
> is expected. If that is not the case (either you provided your own
|
||||
> config or a different cache provider was auto-configured), the
|
||||
> customizer is not invoked at all. You can have as many customizers as
|
||||
> you want, and you can also order them by using
|
||||
> `org.springframework.core.annotation.Order`[format=annotation]
|
||||
> or `org.springframework.core.Ordered[].
|
||||
|
||||
## Generic
|
||||
|
||||
Generic caching is used if the context defines *at least* one
|
||||
`org.springframework.cache.Cache[] bean. A
|
||||
`org.springframework.cache.CacheManager[] wrapping all beans of
|
||||
that type is created.
|
||||
|
||||
## JCache (JSR-107)
|
||||
|
||||
[JCache](https://jcp.org/en/jsr/detail?id=107) is bootstrapped through
|
||||
the presence of a `javax.cache.spi.CachingProvider[] on the
|
||||
classpath (that is, a JSR-107 compliant caching library exists on the
|
||||
classpath), and the
|
||||
`org.springframework.cache.jcache.JCacheCacheManager[] is
|
||||
provided by the `spring-boot-starter-cache` starter. Various compliant
|
||||
libraries are available, and Spring Boot provides dependency management
|
||||
for Ehcache 3, Hazelcast, and Infinispan. Any other compliant library
|
||||
can be added as well.
|
||||
|
||||
It might happen that more than one provider is present, in which case
|
||||
the provider must be explicitly specified. Even if the JSR-107 standard
|
||||
does not enforce a standardized way to define the location of the
|
||||
configuration file, Spring Boot does its best to accommodate setting a
|
||||
cache with implementation details, as shown in the following example:
|
||||
|
||||
# Only necessary if more than one provider is present
|
||||
spring:
|
||||
cache:
|
||||
jcache:
|
||||
provider: "com.example.MyCachingProvider"
|
||||
config: "classpath:example.xml"
|
||||
|
||||
> [!NOTE]
|
||||
> When a cache library offers both a native implementation and JSR-107
|
||||
> support, Spring Boot prefers the JSR-107 support, so that the same
|
||||
> features are available if you switch to a different JSR-107
|
||||
> implementation.
|
||||
|
||||
> [!TIP]
|
||||
> Spring Boot has [general support for Hazelcast](io/hazelcast.xml). If
|
||||
> a single `com.hazelcast.core.HazelcastInstance[] is
|
||||
> available, it is automatically reused for the
|
||||
> `javax.cache.CacheManager[] as well, unless the
|
||||
> configprop:spring.cache.jcache.config[] property is specified.
|
||||
|
||||
There are two ways to customize the underlying
|
||||
`javax.cache.CacheManager[]:
|
||||
|
||||
- Caches can be created on startup by setting the
|
||||
configprop:spring.cache.cache-names[] property. If a custom
|
||||
`javax.cache.configuration.Configuration[] bean is defined,
|
||||
it is used to customize them.
|
||||
|
||||
- `org.springframework.boot.autoconfigure.cache.JCacheManagerCustomizer[]
|
||||
beans are invoked with the reference of the
|
||||
`javax.cache.CacheManager[] for full customization.
|
||||
|
||||
> [!TIP]
|
||||
> If a standard `javax.cache.CacheManager[] bean is defined, it
|
||||
> is wrapped automatically in an
|
||||
> `org.springframework.cache.CacheManager[] implementation that
|
||||
> the abstraction expects. No further customization is applied to it.
|
||||
|
||||
## Hazelcast
|
||||
|
||||
Spring Boot has [general support for Hazelcast](io/hazelcast.xml). If a
|
||||
`com.hazelcast.core.HazelcastInstance[] has been
|
||||
auto-configured and `com.hazelcast:hazelcast-spring` is on the
|
||||
classpath, it is automatically wrapped in a
|
||||
`org.springframework.cache.CacheManager[].
|
||||
|
||||
> [!NOTE]
|
||||
> Hazelcast can be used as a JCache compliant cache or as a Spring
|
||||
> `org.springframework.cache.CacheManager[] compliant cache.
|
||||
> When setting configprop:spring.cache.type[] to `hazelcast`, Spring
|
||||
> Boot will use the `org.springframework.cache.CacheManager[]
|
||||
> based implementation. If you want to use Hazelcast as a JCache
|
||||
> compliant cache, set configprop:spring.cache.type[] to `jcache`. If
|
||||
> you have multiple JCache compliant cache providers and want to force
|
||||
> the use of Hazelcast, you have to [explicitly set the JCache
|
||||
> provider](io/caching.xml#io.caching.provider.jcache).
|
||||
|
||||
## Infinispan
|
||||
|
||||
[Infinispan](https://infinispan.org/) has no default configuration file
|
||||
location, so it must be specified explicitly. Otherwise, the default
|
||||
bootstrap is used.
|
||||
|
||||
spring:
|
||||
cache:
|
||||
infinispan:
|
||||
config: "infinispan.xml"
|
||||
|
||||
Caches can be created on startup by setting the
|
||||
configprop:spring.cache.cache-names[] property. If a custom
|
||||
`org.infinispan.configuration.cache.ConfigurationBuilder[] bean
|
||||
is defined, it is used to customize the caches.
|
||||
|
||||
To be compatible with Spring Boot’s Jakarta EE 9 baseline, Infinispan’s
|
||||
`-jakarta` modules must be used. For every module with a `-jakarta`
|
||||
variant, the variant must be used in place of the standard module. For
|
||||
example, `infinispan-core-jakarta` and `infinispan-commons-jakarta` must
|
||||
be used in place of `infinispan-core` and `infinispan-commons`
|
||||
respectively.
|
||||
|
||||
## Couchbase
|
||||
|
||||
If Spring Data Couchbase is available and Couchbase is
|
||||
[configured](data/nosql.xml#data.nosql.couchbase), a
|
||||
`org.springframework.data.couchbase.cache.CouchbaseCacheManager[]
|
||||
is auto-configured. It is possible to create additional caches on
|
||||
startup by setting the configprop:spring.cache.cache-names[] property
|
||||
and cache defaults can be configured by using `spring.cache.couchbase.*`
|
||||
properties. For instance, the following configuration creates `cache1`
|
||||
and `cache2` caches with an entry *expiration* of 10 minutes:
|
||||
|
||||
spring:
|
||||
cache:
|
||||
cache-names: "cache1,cache2"
|
||||
couchbase:
|
||||
expiration: "10m"
|
||||
|
||||
If you need more control over the configuration, consider registering a
|
||||
`org.springframework.boot.autoconfigure.cache.CouchbaseCacheManagerBuilderCustomizer[]
|
||||
bean. The following example shows a customizer that configures a
|
||||
specific entry expiration for `cache1` and `cache2`:
|
||||
|
||||
include-code::MyCouchbaseCacheManagerConfiguration[]
|
||||
|
||||
## Redis
|
||||
|
||||
If [Redis](https://redis.io/) is available and configured, a
|
||||
`org.springframework.data.redis.cache.RedisCacheManager[] is
|
||||
auto-configured. It is possible to create additional caches on startup
|
||||
by setting the configprop:spring.cache.cache-names[] property and
|
||||
cache defaults can be configured by using `spring.cache.redis.*`
|
||||
properties. For instance, the following configuration creates `cache1`
|
||||
and `cache2` caches with a *time to live* of 10 minutes:
|
||||
|
||||
spring:
|
||||
cache:
|
||||
cache-names: "cache1,cache2"
|
||||
redis:
|
||||
time-to-live: "10m"
|
||||
|
||||
> [!NOTE]
|
||||
> By default, a key prefix is added so that, if two separate caches use
|
||||
> the same key, Redis does not have overlapping keys and cannot return
|
||||
> invalid values. We strongly recommend keeping this setting enabled if
|
||||
> you create your own
|
||||
> `org.springframework.data.redis.cache.RedisCacheManager[].
|
||||
|
||||
> [!TIP]
|
||||
> You can take full control of the default configuration by adding a
|
||||
> `org.springframework.data.redis.cache.RedisCacheConfiguration[]
|
||||
> `org.springframework.context.annotation.Bean`[format=annotation]
|
||||
> of your own. This can be useful if you need to customize the default
|
||||
> serialization strategy.
|
||||
|
||||
If you need more control over the configuration, consider registering a
|
||||
`org.springframework.boot.autoconfigure.cache.RedisCacheManagerBuilderCustomizer[]
|
||||
bean. The following example shows a customizer that configures a
|
||||
specific time to live for `cache1` and `cache2`:
|
||||
|
||||
include-code::MyRedisCacheManagerConfiguration[]
|
||||
|
||||
## Caffeine
|
||||
|
||||
[Caffeine](https://github.com/ben-manes/caffeine) is a Java 8 rewrite of
|
||||
Guava’s cache that supersedes support for Guava. If Caffeine is present,
|
||||
a `org.springframework.cache.caffeine.CaffeineCacheManager[]
|
||||
(provided by the `spring-boot-starter-cache` starter) is
|
||||
auto-configured. Caches can be created on startup by setting the
|
||||
configprop:spring.cache.cache-names[] property and can be customized
|
||||
by one of the following (in the indicated order):
|
||||
|
||||
1. A cache spec defined by `spring.cache.caffeine.spec`
|
||||
|
||||
2. A `com.github.benmanes.caffeine.cache.CaffeineSpec[] bean
|
||||
is defined
|
||||
|
||||
3. A `com.github.benmanes.caffeine.cache.Caffeine[] bean is
|
||||
defined
|
||||
|
||||
For instance, the following configuration creates `cache1` and `cache2`
|
||||
caches with a maximum size of 500 and a *time to live* of 10 minutes
|
||||
|
||||
spring:
|
||||
cache:
|
||||
cache-names: "cache1,cache2"
|
||||
caffeine:
|
||||
spec: "maximumSize=500,expireAfterAccess=600s"
|
||||
|
||||
If a `com.github.benmanes.caffeine.cache.CacheLoader[] bean is
|
||||
defined, it is automatically associated to the
|
||||
`org.springframework.cache.caffeine.CaffeineCacheManager[].
|
||||
Since the `com.github.benmanes.caffeine.cache.CacheLoader[] is
|
||||
going to be associated with *all* caches managed by the cache manager,
|
||||
it must be defined as `CacheLoader<Object, Object>`. The
|
||||
auto-configuration ignores any other generic type.
|
||||
|
||||
## Cache2k
|
||||
|
||||
[Cache2k](https://cache2k.org/) is an in-memory cache. If the Cache2k
|
||||
spring integration is present, a `SpringCache2kCacheManager` is
|
||||
auto-configured.
|
||||
|
||||
Caches can be created on startup by setting the
|
||||
configprop:spring.cache.cache-names[] property. Cache defaults can be
|
||||
customized using a
|
||||
`org.springframework.boot.autoconfigure.cache.Cache2kBuilderCustomizer[]
|
||||
bean. The following example shows a customizer that configures the
|
||||
capacity of the cache to 200 entries, with an expiration of 5 minutes:
|
||||
|
||||
include-code::MyCache2kDefaultsConfiguration[]
|
||||
|
||||
## Simple
|
||||
|
||||
If none of the other providers can be found, a simple implementation
|
||||
using a `java.util.concurrent.ConcurrentHashMap[] as the cache
|
||||
store is configured. This is the default if no caching library is
|
||||
present in your application. By default, caches are created as needed,
|
||||
but you can restrict the list of available caches by setting the
|
||||
`cache-names` property. For instance, if you want only `cache1` and
|
||||
`cache2` caches, set the `cache-names` property as follows:
|
||||
|
||||
spring:
|
||||
cache:
|
||||
cache-names: "cache1,cache2"
|
||||
|
||||
If you do so and your application uses a cache not listed, then it fails
|
||||
at runtime when the cache is needed, but not on startup. This is similar
|
||||
to the way the "real" cache providers behave if you use an undeclared
|
||||
cache.
|
||||
|
||||
## None
|
||||
|
||||
When
|
||||
`org.springframework.cache.annotation.EnableCaching`[format=annotation]
|
||||
is present in your configuration, a suitable cache configuration is
|
||||
expected as well. If you have a custom `
|
||||
org.springframework.cache.CacheManager`, consider defining it in a
|
||||
separate
|
||||
`org.springframework.context.annotation.Configuration`[format=annotation]
|
||||
class so that you can override it if necessary. None uses a no-op
|
||||
implementation that is useful in tests, and slice tests use that by
|
||||
default via
|
||||
`org.springframework.boot.test.autoconfigure.core.AutoConfigureCache`[format=annotation].
|
||||
|
||||
If you need to use a no-op cache rather than the auto-configured cache
|
||||
manager in a certain environment, set the cache type to `none`, as shown
|
||||
in the following example:
|
||||
|
||||
spring:
|
||||
cache:
|
||||
type: "none"
|
||||
@@ -0,0 +1,116 @@
|
||||
# Spring Framework Cache Reference (Official)
|
||||
|
||||
Curated excerpts from the official Spring Framework reference documentation
|
||||
covering caching fundamentals and annotation usage. Source pages are from the
|
||||
[Spring Framework Reference Guide 6.2](https://docs.spring.io/spring-framework/reference/6.2/-SNAPSHOT/integration/cache/).
|
||||
|
||||
## Cache Abstraction Overview
|
||||
|
||||
- **Purpose**: Transparently wrap expensive service methods and reuse results
|
||||
resolved from configured cache managers.
|
||||
- **Enablement**:
|
||||
|
||||
```java
|
||||
@Configuration
|
||||
@EnableCaching
|
||||
public class CacheConfig {
|
||||
@Bean
|
||||
public CacheManager cacheManager() {
|
||||
return new ConcurrentMapCacheManager("books");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Source: [/integration/cache/annotations](https://docs.spring.io/spring-framework/reference/6.2/-SNAPSHOT/integration/cache/annotations)
|
||||
|
||||
- **Supported return types**: `CompletableFuture`, Reactor `Mono`/`Flux`,
|
||||
blocking objects, and collections are all cacheable. For async/reactive types,
|
||||
Spring stores the resolved value and rehydrates it on retrieval.
|
||||
|
||||
## Core Annotations
|
||||
|
||||
### `@Cacheable`
|
||||
|
||||
- Cache the method invocation result using the provided cache name and key.
|
||||
- Supports conditional caching with `condition` (pre-invocation) and `unless`
|
||||
(post-invocation, access `#result`).
|
||||
|
||||
```java
|
||||
@Cacheable(cacheNames = "book", condition = "#isbn.length() == 13", unless = "#result.hardback")
|
||||
public Book findBook(String isbn) { ... }
|
||||
```
|
||||
|
||||
Source: [/integration/cache/annotations](https://docs.spring.io/spring-framework/reference/6.2/-SNAPSHOT/integration/cache/annotations)
|
||||
|
||||
### `@CachePut` and `@CacheEvict`
|
||||
|
||||
- `@CachePut`: Always run the method and update cache entry with fresh result.
|
||||
- `@CacheEvict`: Remove entries; use `allEntries = true` or `beforeInvocation`
|
||||
for pre-call eviction.
|
||||
|
||||
```java
|
||||
@CacheEvict(cacheNames = "books", key = "#isbn", beforeInvocation = true)
|
||||
public void reset(String isbn) { ... }
|
||||
```
|
||||
|
||||
Source: [/integration/cache/annotations](https://docs.spring.io/spring-framework/reference/6.2/-SNAPSHOT/integration/cache/annotations)
|
||||
|
||||
### `@Caching`
|
||||
|
||||
- Bundle multiple cache operations on a single method:
|
||||
|
||||
```java
|
||||
@Caching(evict = {
|
||||
@CacheEvict("primary"),
|
||||
@CacheEvict(cacheNames = "secondary", key = "#isbn")
|
||||
})
|
||||
public Book importBooks(String isbn) { ... }
|
||||
```
|
||||
|
||||
Source: [/integration/cache/annotations](https://docs.spring.io/spring-framework/reference/6.2/-SNAPSHOT/integration/cache/annotations)
|
||||
|
||||
## Store Configuration Highlights
|
||||
|
||||
- **Caffeine**: Configure `CaffeineCacheManager` to create caches on demand.
|
||||
|
||||
```java
|
||||
@Bean
|
||||
CacheManager cacheManager() {
|
||||
return new CaffeineCacheManager();
|
||||
}
|
||||
```
|
||||
|
||||
Source: [/integration/cache/store-configuration](https://docs.spring.io/spring-framework/reference/6.2/-SNAPSHOT/integration/cache/store-configuration)
|
||||
|
||||
- **XML alternative**: Use `<cache:annotation-driven cache-manager="..."/>`
|
||||
when annotation configuration is not feasible.
|
||||
|
||||
```xml
|
||||
<cache:annotation-driven cache-manager="cacheManager"/>
|
||||
<bean id="cacheManager" class="org.springframework.cache.caffeine.CaffeineCacheManager"/>
|
||||
```
|
||||
|
||||
Source: [/integration/cache/declarative-xml](https://docs.spring.io/spring-framework/reference/6.2/-SNAPSHOT/integration/cache/declarative-xml)
|
||||
|
||||
## Reactive and Async Support
|
||||
|
||||
- `@Cacheable` works with asynchronous signatures:
|
||||
|
||||
```java
|
||||
@Cacheable("books")
|
||||
public Mono<Book> findBook(ISBN isbn) { ... }
|
||||
```
|
||||
|
||||
```java
|
||||
@Cacheable(cacheNames = "foos", sync = true)
|
||||
public CompletableFuture<Foo> executeExpensiveOperation(String id) { ... }
|
||||
```
|
||||
|
||||
Source: [/integration/cache/annotations](https://docs.spring.io/spring-framework/reference/6.2/-SNAPSHOT/integration/cache/annotations)
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- [`spring-cache-doc-snippet.md`](spring-cache-doc-snippet.md): Excerpt of the
|
||||
narrative caching overview from the Spring documentation.
|
||||
- Refer to [`cache-core-reference.md`](cache-core-reference.md) for expanded
|
||||
API reference material and `cache-examples.md` for progressive examples.
|
||||
Reference in New Issue
Block a user