Last Update: 15.01.2020. By Jens in Spring Boot
One of the features of the Spring Framework is a cache you can transparently add to your code. All you need to do is set an annotation on a method you want to cache and configure the caching mechanism itself. Setting up the cache wasn’t a big deal before, and with Spring Boot it got even more straightforward. Let’s dive into it with an example.
Last changes: Updated to Spring Boot 2
Contents:
The example is a simple web service with two endpoints. One retrieves a product by id, and the other to update the product. We simulate the storage with a variable identifier with an otherwise fixed product.
The source code for this tutorial is available on GitHub. Let’s do a quick walkthrough.
The main application:
@SpringBootApplication
public class CachingTutorialApplication {
public static void main(String[] args) {
SpringApplication.run(CachingTutorialApplication.class, args);
}
}
The model:
public class Product implements Serializable {
private String id;
private String ean;
private String name;
//constructors and getter, setters omitted
}
The service:
@Service
public class ProductService {
private static final Logger LOGGER = LoggerFactory.getLogger(ProductService.class);
public Product getProduct(String id) {
LOGGER.info("getProduct called for id {}", id);
return new Product(id, "0826663141405", "The Angry Beavers: The Complete Series");
}
public void updateProduct(Product product) {
LOGGER.info("updateProduct called for id {}", product.getId());
//do nothing, we just simulate the update
}
}
Also, last but not least, a controller:
@RestController
public class ProductController {
private static final Logger LOGGER = LoggerFactory.getLogger(ProductController.class);
@Autowired
private ProductService productService;
@GetMapping("product/{id}")
public Product getProduct(@PathVariable("id") String id) {
LOGGER.info("getProduct called for id {}", id);
return productService.getProduct(id);
}
@PostMapping("product/{id}")
public Product updateProduct(@PathVariable("id") String id, @RequestBody Product product) {
LOGGER.info("updateProduct called for id {}", id);
product.setId(id);
productService.updateProduct(product);
return productService.getProduct(id);
}
}
With a GET to /product/{id} we retrieve a product as JSON and with a POST to the same URL, we change the product.
The service and controller classes do log, so you can follow on the console that the cache works in the next steps.
Let’s call the endpoint now twice in a row with:
curl http://localhost:8080/product/4711
The log output looks like:
2017-09-20 10:59:46,461 - ProductController : getProduct called for id 4711
2017-09-20 10:59:46,465 - ProductService : getProduct called for id 4711
2017-09-20 10:59:53,767 - ProductController : getProduct called for id 4711
2017-09-20 10:59:53,768 - ProductService : getProduct called for id 4711
Every time, getProduct is called on the controller it is also called on the service.
The cache mechanism in Spring is abstract and not bound to a particular implementation. We can choose between several backends, e.g., in-memory, Redis, Eh-Cache and more. In the center of it are the CacheManager and Cache interfaces. Cache is an interface for working with a concrete cache instance.
CacheManager is used for obtaining and working with the _Cache_s. Both are transparent for the application, and you must declare and set up once, which cache provider to use.
To get started, add the cache starter to the dependencies.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
Next, you must enable the caching feature for the application by adding the @EnableCaching annotation to a @Configuration class like:
@SpringBootApplication
@EnableCaching
public class CachingTutorialApplication {
public static void main(String[] args) {
SpringApplication.run(CachingTutorialApplication.class, args);
}
}
The auto-configuration now enables caching for us and sets up a CacheManager for us if it does not find another already defined instance (i.e., one defined by us). It first scans in a particular order for specific providers, and when it does not find one, it creates a simple in-memory cache using concurrent maps.
In the first variation, we use the in-memory cache.
Now that we have a working cache backend, we can start to use it. For that, Spring Caches offers a few annotations. We focus on the most commonly used.
All annotations are used on methods.
We use the cache in the ProductService so we can see the effect of the cache better in the console. However, we could also use the cache annotations on the controller methods.
To create a cache for the getProduct method add @Cacheable to it like:
@Cacheable("product")
public Product getProduct(String id) {
LOGGER.info("getProduct called for id {}", id);
return new Product(id, "0826663141405", "The Angry Beavers: The Complete Series");
}
You can give the cache a name either by using the value parameter shortcut or cacheNames. It is possible to assign the object to multiple caches at once by just declaring their names in these parameters.
A unique key identifies each entry in the cache. As we have not specified a key in the @Cacheable annotation above, it uses a default mechanism to create the key.
When the method has:
The parameter types should implement valid hashCode() and equals() methods. String already implements those, so nothing to do on our side.
You can also specify the key explicitly by using the key parameter of the annotation. How to do that is covered in the cache clearing section later.
When you get the product now a few time, you’ll receive the cached version aftter the first call. On the console it looks like:
2017-09-20 11:58:48,050 - ProductController : getProduct called for id 4711
2017-09-20 11:58:48,066 - ProductService : getProduct called for id 4711
2017-09-20 11:58:49,378 - ProductController : getProduct called for id 4711
2017-09-20 11:58:50,399 - ProductController : getProduct called for id 4711
Notice the missing second call to ProductService.
What happens internally now, is that Spring detects when our service method is called, that it should put the result in a cache. It uses the id as the key value for our product instance and places it into the product cache accordingly. If the cache does not exist to this point, it also creates it for use (thanks, auto-magic).
With our current in-memory cache the product is cached as long as the application lives. So, if we would change our product in a database, which we simulate here, it won’t be updated in the cache, as the implementation of our getProduct method isn’t called anymore.
In this step, we are going to delete entries from the cache once we update our product.
To do so, add the @CacheEvict annotation to the updateProduct method like:
@CacheEvict(cacheNames="product")
public void updateProduct(Product product) {
LOGGER.info("updateProduct called for id {}", product.getId());
//do nothing, we just simulate the update
}
When you only declare the cache name to evict, it clears the whole cache every time our method is invoked. It is a valid approach but might be not suited in your context. Let’s change it and delete only the updated record.
@CacheEvict(cacheNames="product", key ="#root.args[0].id")
public void updateProduct(Product product) {
LOGGER.info("updateProduct called for id {}", product.getId());
//do nothing, we just simulate the update
}
The key parameter defines the key to use for the cache based on the method parameters. It is the same mechanism as in the @Cacheable annotation above. In the key value, the Spring Expression Language (SpEL) is used, and you can use it to construct the cache key. The method name, return type, parameters and such are made available in the SpEL under the root variable. For example, with #root.methodName we could retrieve the method name, with #root.args the methods’ parameters.
In the sample case, we use the id of the product as our cache key. As we only pass in a Product in the update method, we must access the actual id by #root.args[0].id. Spring evaluates this to take the first argument of the method and get the field id of it and use this as the key.
When you update the product now, Spring deletes only this product entry from the cache. On the next getProduct request, it loads the new value and put it into the cache again.
curl --header "Content-Type: application/json" --request POST --data '{"id":"4711","ean":"0826663141405","name":"The Angry Beavers: The Complete Serie2"}' -i http://localhost:8080/product/4711
On the console it looks like:
2017-09-20 11:58:50,399 - ProductController : getProduct called for id 4711
2017-09-20 12:18:28,853 - ProductController : getProduct called for id 4711
2017-09-20 12:18:32,384 - ProductController : updateProduct called for id 4711
2017-09-20 12:18:32,386 - ProductService : updateProduct called for id 4711
2017-09-20 12:18:32,455 - ProductService : getProduct called for id 4711
2017-09-20 12:18:35,719 - ProductController : getProduct called for id 4711
Two calls of getProduct but the product is already in the cache. One call to updateProduct updates the product and evict it from the cache. As the updateProduct in the controller also calls _getProduct on the service and return it to JSON, it is already cached with this request. In the following GET calls it comes out of the cache again.
The simple cache using a concurrent map is excellent for merely short-term usage. Now, let’s switch the backend to another solution and more suitable for long-term caching like Redis.
For using Redis as a cache provider, you only add the Spring Data Redis starter to our pom:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
The auto-configuration now sets up the Spring infrastructure for working with Redis and also as a cache provider. By default, it uses a Redis instance on localhost; which is fine for testing.
The Redis cache is higher in the search order for a cache provider than the concurrent map, so it is automatically picked up.
To use a different instance, configure it in application.properties like:
spring.redis.host=192.168.0.100
spring.redis.port=6379
When you start the application now, it uses the Redis instance as a cache. Everything else stays the same.
The caching feature also provides some new metrics and stats, when the Spring Boot Actuators are active in the application.
It provides two actuator endpoints:
The output of caches looks like:
{
cacheManagers: {
cacheManager: {
caches: {
product: {
target: "java.util.concurrent.ConcurrentHashMap"
}
}
}
}
}
Spring exposes cache details as metrics. Namely:
Be aware that not all cache providers provide the above metrics.
Adding a transparent cache in your Spring Boot application is done in a breeze. Most of the time, it will work with the basics we covered above. And for the case it doesn’t, the feature is powerful enough to support more complex situations.