Caching is a technology that stores data copies in a high-speed storage layer known as a cache, so future requests for that data can be served faster. In this topic, you will cover the basics of caching using the tools provided by Spring Boot.
Long operation
You will create a demo project to imitate a service that carries out a long operation to fetch data.
First, you create a component that returns some data with a 2 seconds delay:
@Component
class DataDao {
private final Map<String, String> data = new ConcurrentHashMap<>();
{
data.put("Malta", "Valletta");
}
public String getCapital(String capital) {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException ignored) {
// it happens
}
return data.getOrDefault(capital, "No data");
}
public String update(String country, String capital) {
data.put(country, capital);
return capital;
}
}You then inject this data access component into a service:
@Service
class DataService {
private final DataDao dao;
DataService(DataDao dao) {
this.dao = dao;
}
public String getCapital(String country) {
return dao.getCapital(country);
}
public String update(String country, String capital) {
return dao.update(country, capital);
}
}The service will pass requests to the data access object, which will return it after 2 seconds. Lastly, you create a controller that will ask the service for data and provide the client with data including information about how long it takes to fetch it:
@RestController
class DemoController {
private final DataService service;
DemoController(DataService service) {
this.service = service;
}
@GetMapping("/data")
public String getData(@RequestParam String country) {
return getWithExecutionTime(() -> service.getCapital(country));
}
@GetMapping("/update")
public String putData() {
return service.update("Bhutan", "Thimphu");
}
private String getWithExecutionTime(Supplier<String> supplier) {
var start = Instant.now();
var result = supplier.get();
var duration = Duration.between(start, Instant.now()).toMillis();
return result + ", took " + duration + "ms";
}
}If you run this application and open the address http://localhost:8080/data?country=Malta in a web browser, you receive the following string each time you refresh the page:
Valletta, took 2002msSo, how can you enhance the service performance? You can cache the data returned by the data access object. Let's do that!
Enable caching
To enable caching, you must add the @EnableCaching annotation to any class annotated as @Configuration. In our case, you can add it to the Application class:
@EnableCaching
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}Next, you need to annotate the appropriate method as @Cacheable designating the name of the cache and a key:
@Service
class DataService {
private final DataDao dao;
DataService(DataDao dao) {
this.dao = dao;
}
@Cacheable(cacheNames = "data", key = "#country")
public String getCapital(String country) {
return dao.getCapital(country);
}
public String update(String country, String capital) {
return dao.update(country, capital);
}
}When you restart the service, the first query runs for 2 seconds as before but any following query is executed instantly:
Valletta, took 0msThis happens because during the first query, the data is obtained from the data access object, and after that, the data is fetched from the cache.
Cache management
The first question that comes to mind is; how can you keep the data in the cache updated when it changes in the data access object? Let's demonstrate this problem by sending a request to the /data endpoint to populate the cache and then sending a request to the /update endpoint to add a new country-capital pair to the data store.
However, when you try to query the capital of Bhutan, you will instantly get "no data":
Request : http://localhost:8080/data?country=Bhutan
Response: No data, took 0msThe data set was updated, but the cache wasn't. This can be fixed by annotating the update method as @CachePut:
@Service
class DataService {
private final DataDao dao;
DataService(DataDao dao) {
this.dao = dao;
}
@Cacheable(cacheNames = "data", key = "#country")
public String getCapital(String country) {
return dao.getCapital(country);
}
@CachePut(cacheNames = "data", key = "#country")
public String update(String country, String capital) {
return dao.update(country, capital);
}
}Now if you restart the service, query the Bhutan capital for the first time to populate the cache, update the data and make the same query for the second time, you will get the requested data from the cache:
Thimphu, took 0msHow is this achieved? Notice that each cache annotation designates a cache name and a key. The cache name is the name of the cache used by these methods, and you can manage multiple caches independently. The key allows the cache to know which data to store for each argument passed to the annotated method. By default, the key is the method argument. If there are multiple arguments, a combined key is created. You identified the same key for the getCapital and update methods to associate the same result with the same argument value.
In addition to populating and updating the cache, you can clear the cache by annotating a method with the @CacheEvict annotation.
CacheManager
Beyond using annotations, you can manage caching using the CacheManager interface. Here's an example:
@Service
class DataService {
private final DataDao dao;
private final CacheManager cacheManager;
DataService(DataDao dao, CacheManager cacheManager) {
this.dao = dao;
this.cacheManager = cacheManager;
}
@Cacheable(cacheNames = "data", key = "#country")
public String getCapital(String country) {
Cache cache = cacheManager.getCache("data");
var name = cache.getNativeCache().getClass().getSimpleName();
System.out.println(name);
return dao.getCapital(country);
}
@CachePut(cacheNames = "data", key = "#country")
public String update(String country, String capital) {
return dao.update(country, capital);
}
}You inject the CacheManager into the service class and use it to get a Cache object by the cache name. The Cache interface has a number of methods to manipulate the cache, including get(Object key), put(Object key, Object value), clear() and more, which mirrors the Map interface. In this example, you print the name of the Cache implementation, and by default, it's ConcurrentHashMap. Spring Boot allows for integration with different cache storages, including Caffeine.
Conclusion
In this topic, you have learned about enhancing the performance of web services, the basics of caching in Spring Boot, and discovered different methods of cache management, including annotation-based cache management and using CacheManager.