Caffeine is an open-source, high-performance Java caching library providing high hit rates and excellent concurrency. This Spring boot tutorial will teach us to configure and work with Caffeine cache with simple examples.
1. Introduction to Caffeine
Caffeine is the Java 8 successor to ConcurrentLinkedHashMap and Guava’s cache. Caffeine Cache is similar to JDK ConcurrentMap except that it can be configured to evict entries automatically to constrain its memory footprint.
A cache’s eviction policy tries to predict which entries are most likely to be used again in the near future, thereby maximizing the hit ratio. Caffeine uses the Window TinyLfu eviction policy.
Caffeine has three strategies for value eviction:
- Size-based: Eviction occurs when the configured size limit of the cache is exceeded. The cache will try to evict entries that have not been used recently or very often.
- Time-based: Caffeine provides three approaches to timed eviction
- Expire After Access: Expires the entry after specified duration is passed since the last read or write occurs
- Expire After Write: Expires the entry after specified duration is passed since the write occurs
- Expire After: Custom expiry for each entry based on Expiry implementation.
- Reference-based: Caffeine allows to set up the cache to allow the garbage collection of entries, by using weak references for keys or values, and soft references for values.
2. Setup
Let’s configure caching in our Spring Boot application. Since Caffeine is an external library, add its dependency from Maven repo. We’ll also need to add spring-boot-starter-cache to import the Spring Caching support.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>3.1.1</version>
</dependency>
3. Caffeine Cache Configuration
The Spring framework provides support for transparently adding caching to an application. Spring boot autoconfigures the cache infrastructure as long as caching support is enabled via the @EnableCaching annotation.
@EnableCaching
@SpringBootApplication
public class CaffeineApplication {
public static void main(String[] args) {
SpringApplication.run(CaffeineApplication.class, args);
}
}
There are two ways in which Caffeine can be configured in our Spring Boot application.
3.1. Java Configuration
To enable Caffeine cache, we will need to create the Caffeine
bean which will control the caching parameters like cache size and expiry. Here we are configuring cache which would have an initial capacity of 10 items and an expiry of 60 seconds.
Note that Spring Boot auto-configures the CaffeineCacheManager if Caffeine is present in the classpath. We can define CaffeineCacheManager bean if we need any customization.
@Bean
public Caffeine<Object, Object> caffeineConfig() {
return Caffeine.newBuilder()
.expireAfterWrite(300, TimeUnit.SECONDS)
.initialCapacity(10);
}
@Bean
public CacheManager cacheManager(Caffeine caffeine) {
CaffeineCacheManager caffeineCacheManager = new CaffeineCacheManager();
caffeineCacheManager.setCaffeine(caffeine);
return caffeineCacheManager;
}
3.2. Properties Configuration
We can define the cache configuration in application.properties file. Note that when expireAfterWrite and expireAfterAccess coexist, expireAfterWrite takes priority.
spring.cache.cache-names=cacheName, someOtherCacheName
spring.cache.caffeine.spec=initialCapacity=50,maximumSize=10,expireAfterAccess=300s
4. Managing Cache
Let us walk through how to work with Caffeine.
4.1. Adding Cache with @Cacheable
Let’s create a StudentService which will fetch the Student name given an id. We will enable caching on getName() method using @Cacheable annotation. We will also provide a name for this cache where the results would be stored.
@Service
public class StudentService {
@Autowired
private StudentDAO studentDAO;
@Cacheable("nameById")
public String getName(int id) {
return studentDAO.fetchName(id);
}
}
Each time the method is invoked, caching behavior will be applied, checking whether the method has already been invoked for the given arguments. By default, it will use the method params to compute the cache key. If no value is found in the cache for the computed key, the target method will be invoked, and the returned value will be stored in the associated cache.
4.2. Custom Key Names
We can customize the cache name using a SpEL expression via the key attribute, or a custom org.springframework.cache.interceptor.KeyGenerator implementation.
4.2.1 Using SpEL
Let’s create a new getId() method that will get the Student Id from the student’s name and surname. By default, the cache key will be a combination of both name and surname. If we want to customize it and use only the surname of the cache key, we can do it by specifying the SpEL using the key
attribute.
@Cacheable(value = "studentId", key="{#surname}")
public int getId(String name, String surname) {
return studentDAO.fetchId(name, surname);
}
Here, we only use the surname to construct the cache key. Note that if the surnames of two students are the same it may lead to overwrites.
4.2.2 Using Custom KeyGenerator
We can define a KeyGenerator and pass it to the @Cacheable annotation. A KeyGenerator needs to implement just one single method: generate()
.
public class SurnameKeyGenerator implements KeyGenerator {
public Object generate(Object target, Method method, Object... params) {
return params[1];
}
}
Let’s create a KeyGenerator bean and name it using a qualifier.
@Bean("surnameKeyGenerator")
public KeyGenerator keyGenerator() {
return new SurnameKeyGenerator();
}
Use this custom key in the method using the keyGenerator attribute.
@Cacheable(value = "id", keyGenerator = "surnameKeyGenerator")
public int getId(String name, String surname) {
return studentDAO.fetchId(name, surname);
}
4.3. Conditional Caching
We can have a conditional cache by providing a SpEL expression in the condition
or unless
attribute.
4.3.1 Using condition Attribute
Using the same method shown above, we will only cache if the surname is not “abc“
@Cacheable(value = "id", condition = "#surname != 'abc'")
public int getId(String name, String surname) {
return studentDAO.fetchId(name, surname);
}
4.3.2 Using unless Attribute
Unlike the conditional attribute, unless expressions are evaluated after the method execution. It means we would also have access to the result to construct the unless expressions. Here, we cache unless the surname is “ABC“
@Cacheable(value = "id", unless = "#surname == 'abc'")
public int getId(String name, String surname) {
return studentDAO.fetchId(name, surname);
}
5. Difference between @Cacheable and @CachePut
The @Cacheable will be executed only once for the given cache key, and subsequent requests won’t execute the method until the cache expires or gets flushed.
The @CachePut, on the other hand, does not cause the advised method to be skipped. Rather, it always causes the method to be invoked and its result to be stored in the associated cache if the condition() and unless() expressions match accordingly.
6. Inspecting Cache Content
Inspecting the cache to see possible cached keys/value may help especially while debugging an application. We can inject the CacheManager in any component to inspect the cache.
@GetMapping(value = "/inspectCache")
public void inspectCache(String cacheName) {
CaffeineCache caffeineCache = (CaffeineCache) cacheManager.getCache(cacheName);
Cache<Object, Object> nativeCache = caffeineCache.getNativeCache();
for (Map.Entry<Object, Object> entry : nativeCache.asMap().entrySet()) {
System.out.println("Key = " + entry.getKey());
System.out.println("Value = " + entry.getValue());
}
}
If we want to list all the cache names known to the CacheManager we can use getCacheNames() which returns a Collection<String>.
cacheManager.getCacheNames();
7. Conclusion
This tutorial taught us about Caffeine Cache and how we can use it with Spring Boot. We learned how to add to the cache conditionally or unconditionally and also generate custom keys. We also learned how to inspect the cache.
Happy Learning!!