
Baeldung Pro comes with both absolutely No-Ads as well as finally with Dark Mode, for a clean learning experience:
Once the early-adopter seats are all used, the price will go up and stay at $33/year.
Last updated: February 26, 2023
In this tutorial, we’re going to learn how to use the Caching Abstraction in Spring, and generally improve the performance of our system.
We’ll enable simple caching for some real-world method examples, and we’ll discuss how we can practically improve the performance of these calls through smart cache management.
Before we start implementing caching, we need to ensure our Spring Boot application has the necessary dependencies for caching.
To get started with caching in a Spring Boot application, Spring provides a caching abstraction that can be easily integrated using the spring-boot-starter-cache starter package:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
<version>3.1.5</version>
</dependency>
Under the hood, the starter brings the spring-context-support module.
The core caching abstraction provided by Spring resides in the spring-context module. So when using Maven, our pom.xml should contain the following dependency:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>6.0.13</version>
</dependency>
Interestingly, there is another module named spring-context-support, which sits on top of the spring-context module and provides a few more CacheManagers backed by the likes of EhCache or Caffeine. If we want to use those as our cache storage, then we need to use the spring-context-support module instead:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>6.0.13</version>
</dependency>
Since the spring-context-support module transitively depends on the spring-context module, there is no need for a separate dependency declaration for the spring-context.
To enable caching in Spring, we can leverage annotations, similar to how we activate other configuration-level features within the framework.
In a Spring Boot application, the mere presence of the starter package on the classpath alongside the EnableCaching annotation would register the same ConcurrentMapCacheManager. So there is no need for a separate bean declaration.
Also, we can customize the auto-configured CacheManager using one or more CacheManagerCustomizer<T> beans:
@Component
public class SimpleCacheCustomizer
implements CacheManagerCustomizer<ConcurrentMapCacheManager> {
@Override
public void customize(ConcurrentMapCacheManager cacheManager) {
cacheManager.setCacheNames(asList("users", "transactions"));
}
}
The CacheAutoConfiguration auto-configuration picks up these customizers and applies them to the current CacheManager before its complete initialization.
We can enable the caching feature simply by adding the @EnableCaching annotation to any of the configuration classes:
@Configuration
@EnableCaching
public class CachingConfig {
@Bean
public CacheManager cacheManager() {
return new ConcurrentMapCacheManager("addresses");
}
}
Note: After we enable caching, for the minimal setup, we must register a cacheManager.
Once we’ve enabled caching, the next step is to bind the caching behavior to the methods with declarative annotations.
The simplest way to enable caching behavior for a method is to demarcate it with @Cacheable, and parameterize it with the name of the cache where the results would be stored:
@Cacheable("addresses")
public String getAddress(Customer customer) {...}
The getAddress() call will first check the cache addresses before actually invoking the method and then caching the result.
While in most cases one cache is enough, the Spring framework also supports multiple caches to be passed as parameters:
@Cacheable({"addresses", "directory"})
public String getAddress(Customer customer) {...}
In this case, if any of the caches contain the required result, the result is returned and the method is not invoked.
Now, what would be the problem with making all methods @Cacheable?
The problem is size. We don’t want to populate the cache with values that we don’t need often. Caches can grow quite large, and quite fast, and we could be holding on to a lot of stale or unused data.
We can use the @CacheEvict annotation to indicate the removal of one or more/all values so that fresh values can be loaded into the cache again:
@CacheEvict(value="addresses", allEntries=true)
public String getAddress(Customer customer) {...}
Here we’re using the additional parameter allEntries in conjunction with the cache to be emptied; this will clear all the entries in the cache addresses and prepare it for new data.
While @CacheEvict reduces the overhead of looking up entries in a large cache by removing stale and unused entries, we want to avoid evicting too much data out of the cache.
Instead, we selectively update the entries whenever we alter them.
With the @CachePut annotation, we can update the content of the cache without interfering with the method execution. That is, the method will always be executed and the result cached:
@CachePut(value="addresses")
public String getAddress(Customer customer) {...}
The difference between @Cacheable and @CachePut is that @Cacheable will skip running the method, whereas @CachePut will actually run the method and then put its results in the cache.
What if we want to use multiple annotations of the same type for caching a method? Let’s look at an incorrect example:
@CacheEvict("addresses")
@CacheEvict(value="directory", key=customer.name)
public String getAddress(Customer customer) {...}
The above code would fail to compile since Java does not allow multiple annotations of the same type to be declared for a given method.
The workaround to the above issue would be:
@Caching(evict = {
@CacheEvict("addresses"),
@CacheEvict(value="directory", key="#customer.name") })
public String getAddress(Customer customer) {...}
As shown in the code snippet above, we can group multiple caching annotations with @Caching, and use it to implement our own customized caching logic.
With the @CacheConfig annotation, we can streamline some of the cache configuration into a single place at the class level, so that we don’t have to declare things multiple times:
@CacheConfig(cacheNames={"addresses"})
public class CustomerDataService {
@Cacheable
public String getAddress(Customer customer) {...}
// ...
}
Sometimes, caching might not work well for a method in all situations.
Reusing our example from the @CachePut annotation, this will both execute the method as well as cache the results each and every time:
@CachePut(value="addresses")
public String getAddress(Customer customer) {...}
If we want more control over when the annotation is active, we can parameterize @CachePut with a condition parameter that takes a SpEL expression and ensures that the results are cached based on evaluating that expression:
@CachePut(value="addresses", condition="#customer.name=='Tom'")
public String getAddress(Customer customer) {...}
We can also control the caching based on the output of the method rather than the input via the unless parameter:
@CachePut(value="addresses", unless="#result.length()<64")
public String getAddress(Customer customer) {...}
The above annotation would cache addresses unless they were shorter than 64 characters.
It’s important to know that the condition and unless parameters can be used in conjunction with all the caching annotations.
This kind of conditional caching can prove quite effective for managing large results. It’s also useful for customizing behavior based on input parameters instead of enforcing a generic behavior to all operations.
Here is the equivalent Java Configuration:
@Configuration
@EnableCaching
public class CachingConfig {
@Bean
public CacheManager cacheManager() {
SimpleCacheManager cacheManager = new SimpleCacheManager();
cacheManager.setCaches(Arrays.asList(
new ConcurrentMapCache("directory"),
new ConcurrentMapCache("addresses")));
return cacheManager;
}
}
And here is our CustomerDataService:
@Component
public class CustomerDataService {
@Cacheable(value = "addresses", key = "#customer.name")
public String getAddress(Customer customer) {
return customer.getAddress();
}
}
In this article, we discussed the basics of Caching in Spring, focusing on annotations like @Cacheable, @CacheEvict, and @CachePut. By using caching wisely, we can significantly improve the performance of our applications, especially in areas where there are repetitive data retrievals or expensive method calls.
Follow the Spring Category