In this article, we’ll talk about how you can enable, configure and evict cache in Spring Boot.
1. Why Use Cache in Spring Boot?
We mainly use an in-memory cache in order to enhance the performance of our application.
For instance, let’s say you want a computational-heavy method that takes a lot of time to return the result:
public double calculate(int x, int y) { //heavy work return score; }
If you do not use caching, you will have to calculate the same things every time the method gets called, however, if you do use caching, you can save the result to a Map data structure and get the result in O(1) computing time.
So let’s say the first time you call the method with parameters (0,1), the following will happen if you use cache:
- The application will look in the map if there is any entry with key (0,1)
- If it is not, the method will be normally called and then the result will be stored inside the map, e.g. (0,1) -> 52
- Else, it will just retrieve the value of the entry and return that without actually doing all the hard work
As a result, we will achieve much better performance as the next time the method is called with parameters (0,1), we will have a cache-hit and retrieve the result in O(1)
Nevertheless, nothing comes without a price, as with caching it is possible to have memory overhead, and at the same time, we must be careful with the cache configuration so that we do not serve stale data.
2. Setting Up the Project
First of all, you need to have Java installed, if you do not you can go through these tutorials depending on the OS your machine runs:
- Linux users: Install Java on Linux
- macOS users: Install Java on macOS
- Windows users: Install Java on Windows
For this tutorial, we are using the IntelliJ IDEA Community edition which you can download here.
If you are not using this IDE, you also need to download Maven here.
The next step is to visit Spring initializr and generate a project according to these settings shown in the image:
![Spring Boot Initializr](https://youlearncode.com/wp-content/uploads/2022/10/image-21-1024x831.png)
After having chosen the above options, press the GENERATE button, and a new spring boot project will be downloaded for you. Then you have to unzip the compressed archive and open the folder with your favorite IDE.
Note that we will just include the Spring Web and Spring Cache Abstraction dependency. Afterward, we will use the Caffeine Cache to configure our cache.
3. Enabling Caching Using @EnableCaching
The first thing that we need to do to enable caching, by creating a configuration class.
@Configuration @EnableCaching public class CacheConfig {}
This will allow us the make use of annotations like @Cacheable
, @CacheEvict
and @CachePut
as it autoconfigures the cache for us.
4. Project Base
The setup consists of a simple model, a controller, and a service:
Car.java
package com.codelearnhub.springbootrestcontrollertutorial.model; public class Car { private Long id; private String model; private String brand; private Integer horses; private Double price; public Car() { } public Car(Long id, String model, String brand, Integer horses, Double price) { this.id = id; this.model = model; this.brand = brand; this.horses = horses; this.price = price; } //setters, getters, equas, hashCode and toString }
CarRestController.java
package com.youlearncode.springbootcache.controller; import com.youlearncode.springbootcache.model.Car; import com.youlearncode.springbootcache.service.CarService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.util.List; /** * Rest Controller for cars */ @RestController @RequestMapping(value = "/cars", produces = MediaType.APPLICATION_JSON_VALUE) public class CarRestController { private final CarService carService; @Autowired public CarRestController(CarService carService) { this.carService = carService; } @GetMapping(params = {"minPrice", "maxPrice"}) public List<Car> getAllFilteredByPrice( @RequestParam Double minPrice, @RequestParam Double maxPrice ){ return carService.getCarsWithPriceFilter(minPrice, maxPrice); } @GetMapping(params = {"brand"}) public List<Car> getAllFilteredByBrand( @RequestParam String brand ){ return carService.getCarsWithBrandFilter(brand); } }
and finally, the CarService.java
package com.youlearncode.springbootcache.service; import com.youlearncode.springbootcache.model.Car; import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; import java.util.ArrayList; import java.util.List; @Service public class CarService { private List<Car> cars = new ArrayList<>( List.of( new Car(1L, "Astra", "Opel", 100, 18000d), new Car(2L, "Insignia", "Opel", 120, 22000d), new Car(3L, "Golf", "VW", 90, 17000d), new Car(4L, "Golf", "VW", 120, 19000d), new Car(5L, "Gallardo", "Lamborghini", 400, 100_000d) )); public List<Car> getCarsWithPriceFilter(Double min, Double max) { return cars.stream() .filter(car -> car.getPrice() >= min && car.getPrice() <= max) .toList(); } public List<Car> getCarsWithBrandFilter(String brand) { return cars.stream() .filter(car -> car.getBrand().equals(brand)) .toList(); } }
5. Using @Cacheable to Cache a Method’s Result in Spring Boot
The @Cacheable
annotation is used to mark a method as cacheable. A very important thing about cache in Spring Boot is that you should never call a @Cacheable
, @CacheEvict
or @CachePut
annotated method from the same class as it will never work.
@Cacheable
has the following attributes:
String[] value
– the names that will identify this cacheString[] cacheNames
– exactly the same as value, it is an aliasString key
– The default will be all parameters of the cacheable method unless a SpEL expression specified as keykeyGenerator
– The bean name of the generator if we specify onecacheManager
– The bean name of thecacheManager
for this cache, can be useful if you need different configs for each cachecacheResolver
– The bean name of thecacheResolver
for this cache, it is mutually exclusive with thecacheManager
attributecondition
– a SpEL expression that if it is met, the method will be cachedunless
– a SpEL expression that if it is not met, the method will be cachedsync
– It defaults to false, it should be true if multiple threads will attempt to load a value for the same key
Now before we see how these attributes work, we will change cache logging to TRACE level to completely understand what really happens, by adding the following line in application.properties
:
logging.level.org.springframework.cache= TRACE
5.1 Using @Cacheable with default attributes
Let’s mark the getCarsWithPriceFilter
as @Cacheable
and send try sending some requests via postman:
@Cacheable public List<Car> getCarsWithPriceFilter(Double min, Double max) { return cars.stream() .filter(car -> car.getPrice() >= min && car.getPrice() <= max) .toList(); }
GET /cars?minPrice=18000&maxPrice=20000
We will get the following exception as we did not add any names and at least one should be provided:
java.lang.IllegalStateException: No cache could be resolved for 'Builder[public java.util.List com.youlearncode.springbootcache.service.CarService.getCarsWithPriceFilter(java.lang.Double,java.lang.Double)] caches=[] | key='' | keyGenerator='' | cacheManager='' | cacheResolver='' | condition='' | unless='' | sync='false'' using resolver 'org.springframework.cache.interceptor.SimpleCacheResolver@641259f9'. At least one cache should be provided per cache operation.
Let’s change the @Cacheable
to @Cacheable("priceFilterCache")
, restart the app and hit the same request:
If you check the logs, you will see two cache-related lines:
2022-10-25 22:53:37.504 TRACE 10924 --- [nio-8080-exec-2] o.s.cache.interceptor.CacheInterceptor : No cache entry for key 'SimpleKey [18000.0,20000.0]' in cache(s) [priceFilterCache] 2022-10-25 22:53:37.504 TRACE 10924 --- [nio-8080-exec-2] o.s.cache.interceptor.CacheInterceptor : Computed cache key 'SimpleKey [18000.0,20000.0]' for operation Builder[public java.util.List com.youlearncode.springbootcache.service.CarService.getCarsWithPriceFilter(java.lang.Double,java.lang.Double)] caches=[priceFilterCache] | key='' | keyGenerator='' | cacheManager='' | cacheResolver='' | condition='' | unless='' | sync='false'
This means that no entry was found for these keys and it was computed. If you hit the same request again, the logs will differ:
2022-10-25 22:56:19.574 TRACE 10924 --- [nio-8080-exec-5] o.s.cache.interceptor.CacheInterceptor : Computed cache key 'SimpleKey [18000.0,20000.0]' for operation Builder[public java.util.List com.youlearncode.springbootcache.service.CarService.getCarsWithPriceFilter(java.lang.Double,java.lang.Double)] caches=[priceFilterCache] | key='' | keyGenerator='' | cacheManager='' | cacheResolver='' | condition='' | unless='' | sync='false' 2022-10-25 22:56:19.574 TRACE 10924 --- [nio-8080-exec-5] o.s.cache.interceptor.CacheInterceptor : Cache entry for key 'SimpleKey [18000.0,20000.0]' found in cache 'priceFilterCache'
This time, the result was returned from the cache and the block of code:
return cars.stream() .filter(car -> car.getPrice() >= min && car.getPrice() <= max) .toList();
Never run!
5.2 Using The Key Attribute
You can specify a custom key by using a SpEL expression, in order to override the default one(the parameters of the method)
5.2.1 #root.method, #root.target and #root.caches
@Cacheable(value = "priceFilterCache", key = "#root.method") public List<Car> getCarsWithPriceFilter(Double min, Double max) { return cars.stream() .filter(car -> car.getPrice() >= min && car.getPrice() <= max) .toList(); }
If you check the logs again, after you hit the request, the key will be shown as
public java.util.List com.youlearncode.springbootcache.service.CarService.getCarsWithPriceFilter(java.lang.Double,java.lang.Double)
which is the signature of the method.
Now let’s set the key to #root.target
:
@Cacheable(value = "priceFilterCache", key = "#root.target") public List<Car> getCarsWithPriceFilter(Double min, Double max) { return cars.stream() .filter(car -> car.getPrice() >= min && car.getPrice() <= max) .toList(); }
This will set the key as the reference of the object to which this method belongs
Finally, if we set the key to #root.caches
, the key will be the object reference of the data structure that holds the cache, in this case, it will be:
org.springframework.cache.concurrent.ConcurrentMapCache@68dbe50b
5.2.2 #root.methodName and #root.targetClass
If we set the #root.methodName
as the key, the key will just be the method name without anything else.
However, if we set the key as #root.targetClass
, the key will look like this:
class com.youlearncode.springbootcache.service.CarService
5.2.3 Setting custom key based on arguments
We can also set a value of a specific argument as a key, by using the #root.args[n]
, #an or #pn
So to set the key as the second argument we can write any of these:
#root.args[1]
#a1
#p1
5.3 Creating a Custom KeyGenerator for Cache Key
As you can infer, this attribute is mutually exclusive with the key attribute of the previous section.
Firstly, let’s take a look at the KeyGenerator interface:
@FunctionalInterface public interface KeyGenerator { /** * Generate a key for the given method and its parameters. * @param target the target instance * @param method the method being called * @param params the method parameters (with any var-args expanded) * @return a generated key */ Object generate(Object target, Method method, Object... params); }
In our specific case, meaning the getCarsWithPriceFilter
method, the target
will be the CarService
object, the method
will be the signature of the method and the params
will be the value passed to the method.
Additionally, since it is a Functional Interface, we can just return a lambda expression, which will be the implementation of this interface.
Now, to create a custom KeyGenerator, you can go back to CacheConfig.java
and create a new bean as shown below:
@Bean("keyGenerator") public KeyGenerator keyGenerator(){ return (target, method, params) -> target + method.getName() + Arrays.toString(params); }
Then, we need to change the @Cacheable
annotation to:
@Cacheable(value = "priceFilterCache", keyGenerator = "keyGenerator")
Finally, if we send the same request as before(GET /cars?minPrice=18000&maxPrice=20000
) we can see from the logs that the cache key will be:
com.youlearncode.springbootcache.service.CarService@4e49ce2bgetCarsWithPriceFilter[18000.0, 20000.0]
5.4 Configuring All Existing Caches
First, let’s add the Caffeine Cache dependency:
<dependency> <groupId>com.github.ben-manes.caffeine</groupId> <artifactId>caffeine</artifactId> <version>3.1.1</version> </dependency>
Then, we can just configure size and expiry time by adding the following in the pom.xml:
spring.cache.caffeine.spec=maximumSize=100,expireAfterWrite=5s
We can make another method cacheable and see if this config will apply to both caches:
@Cacheable("brandFilterCache") public List<Car> getCarsWithBrandFilter(String brand) { return cars.stream() .filter(car -> car.getBrand().equals(brand)) .toList(); }
We can now try calling the request GET /cars?minPrice=18000&maxPrice=20000
twice in a less than 5s timespan:
2022-10-26 23:55:03.330 TRACE 21280 --- [nio-8080-exec-1] o.s.cache.interceptor.CacheInterceptor : Computed cache key 'com.youlearncode.springbootcache.service.CarService@fd53053getCarsWithPriceFilter[18000.0, 20000.0]' for operation Builder[public java.util.List com.youlearncode.springbootcache.service.CarService.getCarsWithPriceFilter(java.lang.Double,java.lang.Double)] caches=[priceFilterCache] | key='' | keyGenerator='keyGenerator' | cacheManager='' | cacheResolver='' | condition='' | unless='' | sync='false' 2022-10-26 23:55:03.330 TRACE 21280 --- [nio-8080-exec-1] o.s.cache.interceptor.CacheInterceptor : No cache entry for key 'com.youlearncode.springbootcache.service.CarService@fd53053getCarsWithPriceFilter[18000.0, 20000.0]' in cache(s) [priceFilterCache] 2022-10-26 23:55:03.330 TRACE 21280 --- [nio-8080-exec-1] o.s.cache.interceptor.CacheInterceptor : Computed cache key 'com.youlearncode.springbootcache.service.CarService@fd53053getCarsWithPriceFilter[18000.0, 20000.0]' for operation Builder[public java.util.List com.youlearncode.springbootcache.service.CarService.getCarsWithPriceFilter(java.lang.Double,java.lang.Double)] caches=[priceFilterCache] | key='' | keyGenerator='keyGenerator' | cacheManager='' | cacheResolver='' | condition='' | unless='' | sync='false' 2022-10-26 23:55:06.184 TRACE 21280 --- [nio-8080-exec-3] o.s.cache.interceptor.CacheInterceptor : Computed cache key 'com.youlearncode.springbootcache.service.CarService@fd53053getCarsWithPriceFilter[18000.0, 20000.0]' for operation Builder[public java.util.List com.youlearncode.springbootcache.service.CarService.getCarsWithPriceFilter(java.lang.Double,java.lang.Double)] caches=[priceFilterCache] | key='' | keyGenerator='keyGenerator' | cacheManager='' | cacheResolver='' | condition='' | unless='' | sync='false' 2022-10-26 23:55:06.185 TRACE 21280 --- [nio-8080-exec-3] o.s.cache.interceptor.CacheInterceptor : Cache entry for key 'com.youlearncode.springbootcache.service.CarService@fd53053getCarsWithPriceFilter[18000.0, 20000.0]' found in cache 'priceFilterCache'
If we let more than 5s pass:
2022-10-26 23:56:06.458 TRACE 21280 --- [nio-8080-exec-4] o.s.cache.interceptor.CacheInterceptor : Computed cache key 'com.youlearncode.springbootcache.service.CarService@fd53053getCarsWithPriceFilter[18000.0, 20000.0]' for operation Builder[public java.util.List com.youlearncode.springbootcache.service.CarService.getCarsWithPriceFilter(java.lang.Double,java.lang.Double)] caches=[priceFilterCache] | key='' | keyGenerator='keyGenerator' | cacheManager='' | cacheResolver='' | condition='' | unless='' | sync='false' 2022-10-26 23:56:06.458 TRACE 21280 --- [nio-8080-exec-4] o.s.cache.interceptor.CacheInterceptor : No cache entry for key 'com.youlearncode.springbootcache.service.CarService@fd53053getCarsWithPriceFilter[18000.0, 20000.0]' in cache(s) [priceFilterCache] 2022-10-26 23:56:06.458 TRACE 21280 --- [nio-8080-exec-4] o.s.cache.interceptor.CacheInterceptor : Computed cache key 'com.youlearncode.springbootcache.service.CarService@fd53053getCarsWithPriceFilter[18000.0, 20000.0]' for operation Builder[public java.util.List com.youlearncode.springbootcache.service.CarService.getCarsWithPriceFilter(java.lang.Double,java.lang.Double)] caches=[priceFilterCache] | key='' | keyGenerator='keyGenerator' | cacheManager='' | cacheResolver='' | condition='' | unless='' | sync='false'
You can see that the key was not found, which is expected.
5.4 Setting a Different Config for Each Cache by Using CacheManager Attribute
We can also have different configurations for each cache, by taking advantage of the cacheManager
attribute.
You just have to create a 2 cacheManager
beans, one for each cache as shown below:
@Bean("brandFilterCacheManager") public CacheManager brandCacheManager(){ return getCacheManager("brandCacheManager", 5, 5); } @Bean("priceFilterCacheManager") public CacheManager priceCacheManager(){ return getCacheManager("priceCacheManager", 2, 3); } private CacheManager getCacheManager(String cacheName, int secondsExpiry, long entriesSize) { CaffeineCache cache = buildCache(cacheName, secondsExpiry, entriesSize); SimpleCacheManager manager = new SimpleCacheManager(); manager.setCaches(Collections.singleton(cache)); return manager; } private CaffeineCache buildCache(String cacheName, int secondsExpiry, long entriesSize){ return new CaffeineCache( cacheName, Caffeine .newBuilder() .expireAfterWrite(secondsExpiry, TimeUnit.SECONDS) .maximumSize(entriesSize) .build() ); }
Let’s explain what we did here:
- We created a new CaffeineCache Object based on the cacheName, seconds to expire after write, and size of entries
- Then we added this cache object to a new
cacheManager
- Finally, we added 2
@Bean
annotated methods to include these 2 cache managers in the spring context.
Now let’s jump back to CarService
and change the 2 @Cacheable
annotated methods:
@Cacheable(value = "priceFilterCache", cacheManager = "priceFilterCacheManager") public List<Car> getCarsWithPriceFilter(Double min, Double max) { return cars.stream() .filter(car -> car.getPrice() >= min && car.getPrice() <= max) .toList(); } @Cacheable(value = "brandFilterCache", cacheManager = "brandFilterCacheManager") public List<Car> getCarsWithBrandFilter(String brand) { return cars.stream() .filter(car -> car.getBrand().equals(brand)) .toList(); }
If you start the app now, you will see this exception:
java.lang.IllegalStateException: No CacheResolver specified, and no unique bean of type CacheManager found. Mark one as primary or declare a specific CacheManager to use.
Although we declared which cacheManager
should be used for every @Cacheable method, it seems that spring does not know which bean to inject. A solution to this is to mark with @Primary
any of the two beans that we created, e.g. :
@Bean("brandFilterCacheManager") @Primary public CacheManager brandCacheManager(){ return getCacheManager("brandFilterCache", 5, 1); } @Bean("priceFilterCacheManager") public CacheManager priceCacheManager(){ return getCacheManager("priceFilterCache", 2, 3); }
Now let’s hit GET /cars?minPrice=18000&maxPrice=20000
three times, remember this cache has an expiry time of 2 secs:
- The first one is at
2022-10-28 20:18:45.606
- The second one is at
2022-10-28 20:18:46.517
(<2 secs after write, so we should have a cache hit) - And the third one at
2022-10-28 20:18:50.087
(>2 secs after write, so we should have a cache miss)
As we can see from the logs, it worked:
2022-10-28 20:18:45.606 TRACE 22004 --- [nio-8080-exec-1] o.s.cache.interceptor.CacheInterceptor : Computed cache key 'SimpleKey [18000.0,20000.0]' for operation Builder[public java.util.List com.youlearncode.springbootcache.service.CarService.getCarsWithPriceFilter(java.lang.Double,java.lang.Double)] caches=[priceFilterCache] | key='' | keyGenerator='' | cacheManager='priceFilterCacheManager' | cacheResolver='' | condition='' | unless='' | sync='false' 2022-10-28 20:18:45.607 TRACE 22004 --- [nio-8080-exec-1] o.s.cache.interceptor.CacheInterceptor : No cache entry for key 'SimpleKey [18000.0,20000.0]' in cache(s) [priceFilterCache] 2022-10-28 20:18:45.607 TRACE 22004 --- [nio-8080-exec-1] o.s.cache.interceptor.CacheInterceptor : Computed cache key 'SimpleKey [18000.0,20000.0]' for operation Builder[public java.util.List com.youlearncode.springbootcache.service.CarService.getCarsWithPriceFilter(java.lang.Double,java.lang.Double)] caches=[priceFilterCache] | key='' | keyGenerator='' | cacheManager='priceFilterCacheManager' | cacheResolver='' | condition='' | unless='' | sync='false' 2022-10-28 20:18:46.517 TRACE 22004 --- [nio-8080-exec-3] o.s.cache.interceptor.CacheInterceptor : Computed cache key 'SimpleKey [18000.0,20000.0]' for operation Builder[public java.util.List com.youlearncode.springbootcache.service.CarService.getCarsWithPriceFilter(java.lang.Double,java.lang.Double)] caches=[priceFilterCache] | key='' | keyGenerator='' | cacheManager='priceFilterCacheManager' | cacheResolver='' | condition='' | unless='' | sync='false' 2022-10-28 20:18:46.518 TRACE 22004 --- [nio-8080-exec-3] o.s.cache.interceptor.CacheInterceptor : Cache entry for key 'SimpleKey [18000.0,20000.0]' found in cache 'priceFilterCache' 2022-10-28 20:18:50.087 TRACE 22004 --- [nio-8080-exec-4] o.s.cache.interceptor.CacheInterceptor : Computed cache key 'SimpleKey [18000.0,20000.0]' for operation Builder[public java.util.List com.youlearncode.springbootcache.service.CarService.getCarsWithPriceFilter(java.lang.Double,java.lang.Double)] caches=[priceFilterCache] | key='' | keyGenerator='' | cacheManager='priceFilterCacheManager' | cacheResolver='' | condition='' | unless='' | sync='false' 2022-10-28 20:18:50.087 TRACE 22004 --- [nio-8080-exec-4] o.s.cache.interceptor.CacheInterceptor : No cache entry for key 'SimpleKey [18000.0,20000.0]' in cache(s) [priceFilterCache] 2022-10-28 20:18:50.087 TRACE 22004 --- [nio-8080-exec-4] o.s.cache.interceptor.CacheInterceptor : Computed cache key 'SimpleKey [18000.0,20000.0]' for operation Builder[public java.util.List com.youlearncode.springbootcache.service.CarService.getCarsWithPriceFilter(java.lang.Double,java.lang.Double)] caches=[priceFilterCache] | key='' | keyGenerator='' | cacheManager='priceFilterCacheManager' | cacheResolver='' | condition='' | unless='' | sync='false'
Now let’s do the same for GET /cars?brand=Opel
- The first one is at
2022-10-28 20:24:12.004
- The second one is at
2022-10-28 20:24:15.474
( >2 secs(the config from the previous cache) and <5 secs after writing, so we should have a cache hit) - And the third one at
2022-10-28 20:18:50.087
(>5 secs after writing, so we should have a cache miss)
Again, if we see the logs, we can see that this config also worked.
2022-10-28 20:24:12.004 TRACE 22004 --- [nio-8080-exec-7] o.s.cache.interceptor.CacheInterceptor : Computed cache key 'Opel' for operation Builder[public java.util.List com.youlearncode.springbootcache.service.CarService.getCarsWithBrandFilter(java.lang.String)] caches=[brandFilterCache] | key='' | keyGenerator='' | cacheManager='brandFilterCacheManager' | cacheResolver='' | condition='' | unless='' | sync='false' 2022-10-28 20:24:12.004 TRACE 22004 --- [nio-8080-exec-7] o.s.cache.interceptor.CacheInterceptor : No cache entry for key 'Opel' in cache(s) [brandFilterCache] 2022-10-28 20:24:12.004 TRACE 22004 --- [nio-8080-exec-7] o.s.cache.interceptor.CacheInterceptor : Computed cache key 'Opel' for operation Builder[public java.util.List com.youlearncode.springbootcache.service.CarService.getCarsWithBrandFilter(java.lang.String)] caches=[brandFilterCache] | key='' | keyGenerator='' | cacheManager='brandFilterCacheManager' | cacheResolver='' | condition='' | unless='' | sync='false' 2022-10-28 20:24:15.474 TRACE 22004 --- [nio-8080-exec-8] o.s.cache.interceptor.CacheInterceptor : Computed cache key 'Opel' for operation Builder[public java.util.List com.youlearncode.springbootcache.service.CarService.getCarsWithBrandFilter(java.lang.String)] caches=[brandFilterCache] | key='' | keyGenerator='' | cacheManager='brandFilterCacheManager' | cacheResolver='' | condition='' | unless='' | sync='false' 2022-10-28 20:24:15.474 TRACE 22004 --- [nio-8080-exec-8] o.s.cache.interceptor.CacheInterceptor : Cache entry for key 'Opel' found in cache 'brandFilterCache' 2022-10-28 20:24:22.226 TRACE 22004 --- [nio-8080-exec-9] o.s.cache.interceptor.CacheInterceptor : Computed cache key 'Opel' for operation Builder[public java.util.List com.youlearncode.springbootcache.service.CarService.getCarsWithBrandFilter(java.lang.String)] caches=[brandFilterCache] | key='' | keyGenerator='' | cacheManager='brandFilterCacheManager' | cacheResolver='' | condition='' | unless='' | sync='false' 2022-10-28 20:24:22.227 TRACE 22004 --- [nio-8080-exec-9] o.s.cache.interceptor.CacheInterceptor : No cache entry for key 'Opel' in cache(s) [brandFilterCache] 2022-10-28 20:24:22.227 TRACE 22004 --- [nio-8080-exec-9] o.s.cache.interceptor.CacheInterceptor : Computed cache key 'Opel' for operation Builder[public java.util.List com.youlearncode.springbootcache.service.CarService.getCarsWithBrandFilter(java.lang.String)] caches=[brandFilterCache] | key='' | keyGenerator='' | cacheManager='brandFilterCacheManager' | cacheResolver='' | condition='' | unless='' | sync='false'
5.5 Using the CacheResolver Attribute
If you choose to use the cacheResolver
attribute, you can just declare a bean that uses the priceCacheManager
that we previously created by using the NamedCacheResolver
class:
@Bean("priceFilterCacheResolver") public CacheResolver priceCacheResolver(){ return new NamedCacheResolver(priceCacheManager(), "priceFilterCache"); }
And for it to work, we must also change the @Cacheable
annotation as shown below:
@Cacheable(value = "priceFilterCache", cacheResolver = "priceFilterCacheResolver")
Of course, you can create your own custom CacheResolver
class and implement resolveCaches
method by adding custom logic.
5.6 Using the Condition Attribute
This attribute is used to cache a method by providing a SpEL expression that will evaluate to true. Note that this method is evaluated before the method has been called, therefore, we cannot refer to the result of the method.
Let’s say that for some reason, we do not want to cache any result of getCarsWithPriceFilter
that derives from passing minPrice
that is > 18000
, then the annotation will be the following:
@Cacheable(value = "priceFilterCache", cacheResolver = "priceFilterCacheResolver", condition = "#a0 > 18000")
Now, if we do hit GET /cars?minPrice=18000&maxPrice=20000
we will see the following in the logs:
2022-10-28 21:28:41.484 TRACE 12688 --- [nio-8080-exec-1] o.s.cache.interceptor.CacheInterceptor : Cache condition failed on method public java.util.List com.youlearncode.springbootcache.service.CarService.getCarsWithPriceFilter(java.lang.Double,java.lang.Double) for operation Builder[public java.util.List com.youlearncode.springbootcache.service.CarService.getCarsWithPriceFilter(java.lang.Double,java.lang.Double)] caches=[priceFilterCache] | key='' | keyGenerator='' | cacheManager='' | cacheResolver='priceFilterCacheResolver' | condition='#a0 > 18000' | unless='' | sync='false'
If we set any number <= 18000, it will cache the result as expected.
5.7 Using the Unless Attribute
This attribute is used to cache a method by providing a SpEL expression that will evaluate to false. Note that this method is evaluated after the method has been called, therefore, we can refer to the result of the method.
Let’s say that for some reason, we do not want to cache any result of getCarsWithPriceFilter
that if the size of the returned list is 2.
@Cacheable(value = "priceFilterCache", cacheResolver = "priceFilterCacheResolver", condition = "#result.size() == 2")
Now, if we do hit GET /cars?minPrice=18000&maxPrice=20000
, although the logs will not show us a failure as before, you can see that nothing gets inserted into the cache and the “No cache key” message will always be shown
6. Using @CacheEvict to Invalidate Cache in Spring Boot
@CacheEvict
annotation is used to evict all entries or specific entries of a cache. You can use it to invalidate stale or old cache entries.
@CacheEvict
has the following attributes, many of which are the same as @Cacheable
, so you can jump back to section 5 if you miss anything:
String[] value
– the names that will identify this cacheString[] cacheNames
– exactly the same as value, it is an aliasString key
– The key that will be used to evict specific cache entries. The default will be all parameters of the@CacheEvict
method unless a SpEL expression specified as keykeyGenerator
– The bean name of the generator if we specify onecacheManager
– The bean name of thecacheManager
for this cache, can be useful if you need different configs for each cachecacheResolver
– The bean name of thecacheResolver
for this cache, it is mutually exclusive with thecacheManager
attributecondition
– a SpEL expression that if it is met, the cache will be evictedallEntries
– It defaults to false and it specifies if all or specific entries will be evictedbeforeInvocation
– It defaults to false. If we set it to true, and the annotated method fails for any reason(e.g. exception), the eviction will still happen since it will occur before the block inside the method is executed.
6.1 Evicting all entries of brandFilterCache
First of all, let’s change 2 things:
- Make the cache expiry longer(10s) for the
brandFilterCache
- Create a new endpoint
/evictBrandFilter
to call it via GET request - Create a
@CacheEvict
annotated method to clear all entries ofbrandFilterCache
So in CarRestController
, we will add:
@GetMapping("/evictAllBrandFilterCache") public void evictAllBrandFilterCache(){ carService.evictAllBrandCacheEntries(); }
And in CarService
:
@CacheEvict(value = "brandFilterCache", cacheManager = "brandFilterCacheManager", allEntries = true) public void evictAllBrandCacheEntries() {}
Now we can try it by first calling GET /cars?brand=Opel
and then GET /cars/evictAllBrandFilterCache
2022-10-28 22:19:10.162 TRACE 5384 --- [nio-8080-exec-3] o.s.cache.interceptor.CacheInterceptor : Computed cache key 'Opel' for operation Builder[public java.util.List com.youlearncode.springbootcache.service.CarService.getCarsWithBrandFilter(java.lang.String)] caches=[brandFilterCache] | key='' | keyGenerator='' | cacheManager='brandFilterCacheManager' | cacheResolver='' | condition='' | unless='' | sync='false' 2022-10-28 22:19:10.163 TRACE 5384 --- [nio-8080-exec-3] o.s.cache.interceptor.CacheInterceptor : No cache entry for key 'Opel' in cache(s) [brandFilterCache] 2022-10-28 22:19:10.163 TRACE 5384 --- [nio-8080-exec-3] o.s.cache.interceptor.CacheInterceptor : Computed cache key 'Opel' for operation Builder[public java.util.List com.youlearncode.springbootcache.service.CarService.getCarsWithBrandFilter(java.lang.String)] caches=[brandFilterCache] | key='' | keyGenerator='' | cacheManager='brandFilterCacheManager' | cacheResolver='' | condition='' | unless='' | sync='false' 2022-10-28 22:19:14.168 TRACE 5384 --- [nio-8080-exec-2] o.s.cache.interceptor.CacheInterceptor : Invalidating entire cache for operation Builder[public void com.youlearncode.springbootcache.service.CarService.evictAllBrandCacheEntries()] caches=[brandFilterCache] | key='' | keyGenerator='' | cacheManager='brandFilterCacheManager' | cacheResolver='' | condition='',true,false on method public void com.youlearncode.springbootcache.service.CarService.evictAllBrandCacheEntries()
You can see from the logs that all entries of the cache were invalidated. This means that even though the cache hasn’t expired yet, if we call the same request again, we will get fresh results.
6.2 Evicting Specific Entries of brandFilterCache
Now we will make another two changes:
- Create a new endpoint
/evictBrandFilterCache
with a request parameterbrands
- Create a new
@CacheEvict
annotated method to remove all entries with matching keys
So in CarRestController
, we will add:
@GetMapping(value = "/evictBrandFilterCache", params = "brands") public void evictBrandFilterCache(@RequestParam List<String> brands){ brands.forEach(carService::evictSpecificCaches); }
And in CarService
:
@CacheEvict(value = "brandFilterCache", cacheManager = "brandFilterCacheManager", key = "#brand") public void evictSpecificCaches(String brand) {}
Now we can try it by first calling GET /cars?brand=Opel
and then GET /cars/evictBrandFilterCache?brands=Opel
and then again GET /cars?brand=Opel
2022-10-28 22:43:50.627 TRACE 20724 --- [nio-8080-exec-4] o.s.cache.interceptor.CacheInterceptor : Computed cache key 'Opel' for operation Builder[public java.util.List com.youlearncode.springbootcache.service.CarService.getCarsWithBrandFilter(java.lang.String)] caches=[brandFilterCache] | key='' | keyGenerator='' | cacheManager='brandFilterCacheManager' | cacheResolver='' | condition='' | unless='' | sync='false' 2022-10-28 22:43:50.627 TRACE 20724 --- [nio-8080-exec-4] o.s.cache.interceptor.CacheInterceptor : No cache entry for key 'Opel' in cache(s) [brandFilterCache] 2022-10-28 22:43:50.627 TRACE 20724 --- [nio-8080-exec-4] o.s.cache.interceptor.CacheInterceptor : Computed cache key 'Opel' for operation Builder[public java.util.List com.youlearncode.springbootcache.service.CarService.getCarsWithBrandFilter(java.lang.String)] caches=[brandFilterCache] | key='' | keyGenerator='' | cacheManager='brandFilterCacheManager' | cacheResolver='' | condition='' | unless='' | sync='false' 2022-10-28 22:43:52.388 TRACE 20724 --- [nio-8080-exec-5] o.s.cache.interceptor.CacheInterceptor : Computed cache key 'Opel' for operation Builder[public void com.youlearncode.springbootcache.service.CarService.evictSpecificCaches(java.lang.String)] caches=[brandFilterCache] | key='#brand' | keyGenerator='' | cacheManager='brandFilterCacheManager' | cacheResolver='' | condition='',false,false 2022-10-28 22:43:52.388 TRACE 20724 --- [nio-8080-exec-5] o.s.cache.interceptor.CacheInterceptor : Invalidating cache key [Opel] for operation Builder[public void com.youlearncode.springbootcache.service.CarService.evictSpecificCaches(java.lang.String)] caches=[brandFilterCache] | key='#brand' | keyGenerator='' | cacheManager='brandFilterCacheManager' | cacheResolver='' | condition='',false,false on method public void com.youlearncode.springbootcache.service.CarService.evictSpecificCaches(java.lang.String) 2022-10-28 22:43:54.701 TRACE 20724 --- [nio-8080-exec-6] o.s.cache.interceptor.CacheInterceptor : Computed cache key 'Opel' for operation Builder[public java.util.List com.youlearncode.springbootcache.service.CarService.getCarsWithBrandFilter(java.lang.String)] caches=[brandFilterCache] | key='' | keyGenerator='' | cacheManager='brandFilterCacheManager' | cacheResolver='' | condition='' | unless='' | sync='false' 2022-10-28 22:43:54.701 TRACE 20724 --- [nio-8080-exec-6] o.s.cache.interceptor.CacheInterceptor : No cache entry for key 'Opel' in cache(s) [brandFilterCache] 2022-10-28 22:43:54.701 TRACE 20724 --- [nio-8080-exec-6] o.s.cache.interceptor.CacheInterceptor : Computed cache key 'Opel' for operation Builder[public java.util.List com.youlearncode.springbootcache.service.CarService.getCarsWithBrandFilter(java.lang.String)] caches=[brandFilterCache] | key='' | keyGenerator='' | cacheManager='brandFilterCacheManager' | cacheResolver='' | condition='' | unless='' | sync='false'
You can see from the logs that only specific entries were invalidated, more specifically, only the ones with key
‘Opel’
7. Using @CachePut to Update a Cache in Spring Boot
@CachePut
annotation is used to update cache entries that might be old or stale. The big difference with @Cacheable
, is that when a @CachePut
annotated method is called, it will be executed as any other method and insert/update the respective cache entry.
@CachPut
has the following attributes, many of which are the same as @Cacheable
and @CacheEvict,
so you can jump back to section 5 or 6 if you miss anything:
String[] value
– the names that will identify this cache that we will updateString[] cacheNames
– exactly the same as value, it is an aliasString key
– The key that will be used to update cache entries. The default will be all parameters of the@CachePut
method unless a SpEL expression specified as keykeyGenerator
– The bean name of the generator if we specify onecacheManager
– The bean name of thecacheManager
for this cache, can be useful if you need different configs for each cachecacheResolver
– The bean name of thecacheResolver
for this cache, it is mutually exclusive with thecacheManager
attributecondition
– a SpEL expression that if it is met, then the cache will be updated. Note that unlike@Cacheable
, we can refer to the result of the method.unless
– a SpEL expression that if it is false, then the cache will be updated
Now let’s create 2 new endpoints:
- A new
GET /updateBrandFilterCache
with parameter brands - A new
POST /cars
to create a new car
@GetMapping(value = "/updateBrandFilterCache", params = "brands") public void updateBrandFilterCache(@RequestParam List<String> brands){ brands.forEach(carService::putBrandFilterCache); } @PostMapping public Car create(@RequestBody Car car) { return carService.create(car); }
And the next step is to create the two methods that these endpoints will call:
@CachePut(value = "brandFilterCache", cacheManager = "brandFilterCacheManager", key = "#brand") public List<Car> putBrandFilterCache(String brand) { return cars.stream() .filter(car -> car.getBrand().equals(brand)) .toList(); } public Car create(Car car) { Long newId = cars.stream().mapToLong(Car::getId).max().orElse(0L) + 1L; car.setId(newId); cars.add(car); return cars.stream() .filter(car_ -> car_.getId().equals(newId)) .findAny() .orElseThrow(); }
Once, the @CachePut
annotated method is called the cache entry will be updated with fresh results.
Now we will check how it works by running two scenarios:
7.1 Without @CachePut
All of the following steps must happen before the cache expires(currently it is set to 10s)
Firstly, we will hit GET /cars?brands=Opel
![GET /cars?brand=Opel](https://youlearncode.com/wp-content/uploads/2022/10/image-22-1024x820.png)
Secondly, we will add a new car with Opel
brand:
![POST /cars](https://youlearncode.com/wp-content/uploads/2022/10/image-24-1024x454.png)
The final step is to call again GET /cars?brands=Opel
and you will see that the new car we added is not there.
7.2 With @CachePut
Now the only difference with the previous scenario is that we will call GET /cars/updateBrandFilterCache?brands=Opel
before we call for a second time GET /cars?brands=Opel
.
As a result, the second time you call GET /cars?brands=Opel
you will see three cars and not two, which means that the cache was updated successfully.
8. Cache Without Annotations in Spring Boot
Of course, if for any reason you do not like to use the annotation we described in sections 5 – 7, you can autowire
one or more cacheManager
objects and call methods for adding/removing and updating cache.
Before we begin, we will create a new CarServiceWithoutAnnotations
as shown below:
@Service public class CarServiceWithoutAnnotations { @Qualifier("brandFilterCacheManager") private final CacheManager brandFilterCacheManager; @Qualifier("priceFilterCacheManager") private final CacheManager priceFilterCacheManager; private List<Car> cars = new ArrayList<>( List.of( new Car(1L, "Astra", "Opel", 100, 18000d), new Car(2L, "Insignia", "Opel", 120, 22000d), new Car(3L, "Golf", "VW", 90, 17000d), new Car(4L, "Golf", "VW", 120, 19000d), new Car(5L, "Gallardo", "Lamborghini", 400, 100_000d) )); public CarServiceWithoutAnnotations(CacheManager brandFilterCacheManager, CacheManager priceFilterCacheManager) { this.brandFilterCacheManager = brandFilterCacheManager; this.priceFilterCacheManager = priceFilterCacheManager; } }
8.1 Cacheable Method without @Cacheable
We can implement a logic where we will look for a key inside the cache, and if it is found, we will return this instead of going through the car list:
For example, getCarsWithBrandFilter
, could look like this:
public List<Car> getCarsWithBrandFilter(String brand) { Cache cache = brandFilterCacheManager.getCache("brandFilterCache"); var result = Optional .ofNullable(cache) .map(cache_ -> cache_.get(new SimpleKey(brand))) .orElseGet(() -> insertInCacheAndReturn(brand, cache)) .get(); return (List<Car>) result; } private Cache.ValueWrapper insertInCacheAndReturn(String brand, Cache cache) { return () -> { List<Car> resultToBePut = cars.stream() .filter(car -> car.getBrand().equals(brand)) .toList(); cache.put(new SimpleKey(brand), resultToBePut); return resultToBePut; }; }
Here we just look into the brandFilterCache and then:
- If it does not exist, we calculate the result, put it in a cache entry, and return the result
- If it does exist, we just return the value from the cache
Note that here it is important to use orElseGet()
since orElse()
will calculate the result regardless of the result of the previous .map
.
8.2 Evicting Specific or All Cache Entries Without @CacheEvict
To evict all cache entries for brandFilterCache
you just have to do the following:
public void evictAllBrandCacheEntries() { brandFilterCacheManager.getCache("brandFilterCache").clear(); }
If you like to evict specific entries, you have to call evict()
or evictIfPresent()
methods:
public void evictSpecificCaches(String brand) { brandFilterCacheManager.getCache("brandFilterCache").evictIfPresent(new SimpleKey(brand)); }
8.3 Updating Cache Entries Without @CachePut
To implement @CachePut
functionality, we only have to calculate the result and put it into the cache as shown below:
public void putBrandFilterCache(String brand) { Cache cache = brandFilterCacheManager.getCache("brandFilterCache"); List<Car> resultToBePut = cars.stream() .filter(car -> car.getBrand().equals(brand)) .toList(); cache.put(new SimpleKey(brand), resultToBePut); }
9. Conclusion
By now, you should have understood how to use @Cacheable
, @CacheEvict
and @CachePu
t. Moreover, you should be able to configure multiple caches by using a cacheManager
or a cacheResolver
. Finally, you should have a deep understanding of how cache works as you are able to implement a cache mechanism without even using the annotations. You can find the source code on our GitHub page.