Caching in Spring
In this tutorial, we will have a look at how we can cache data in Spring and make our apps faster to respond when similar requests are made.
Spring framework is an open source Java platform that provides MVC infrastructure support for developing robust Java applications very easily and very rapidly using recommended MVC pattern.
Caching is a method to store frequently required data so that it’s readily available when requested. In this article, we’ll look at Spring’s cache abstraction. Although Spring doesn’t implement a cache solution, it offers declarative support for caching that integrates with several popular caching implementations.
Enable Cache support
Caching in Spring can be done using:
- Annotation-driven Caching.
- XML declared caching.
The most common way to use Spring’s cache abstraction is to annotate methods with annotations like @Cacheable and @CacheEvict.
Before we start using caching annotations in our project, we must enable annotation-driven caching support. Using Java configuration, we can enable annotation-driven caching by adding @EnableCaching to one of our configuration classes. The following example shows the @EnableCaching annotation:
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.concurrent.ConcurrentMapCacheManager; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@EnableCaching
public class CachingConfig {
@Bean
public CacheManager cacheManager() {
return new ConcurrentMapCacheManager(“user”);
}
}
We also define a CacheManager Bean, which are the heart of Spring’s cache abstraction, enabling integration with one of several popular caching implementations.
In this case, a ConcurrentMapCacheManager is declared. This cache manager uses a java.util.concurrent.ConcurrentHashMap as its cache store. Its simplicity makes it a good option for development, testing, or basic applications. But because its cache storage is memory-based and thus tied to the lifecycle of the application, it’s probably not an ideal choice for larger production applications.
Caching with Annotations
After we have enabled caching in our project, we must bind our methods with annotations to define the Cache behaviour. Let’s do this next.
@Cacheable
The simplest way to introduce caching behavior in a method is to annotate it with @Cacheable and parameterize it with the name of the cache where the results should be stored:
@Cacheable("user")
public User getUser(long ids) {...}
The getUser() 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("user", “admin”)
public User getUser(long ids) {...}
In this case, if any of the caches contain the required result, the result is returned and the method is not invoked.
This is worth noting that @Cacheable is only put on an implemented getUser(...) method. If same method is implemented somewhere else again, its result won’t be cacheable.
Therefore, we might consider placing the annotation on the method declaration in UserRepository instead of the implementation:
@Cacheable("user")
User getUser(long id);
When we annotate the interface method, the @Cacheable annotation will be inherited by all implementations of UserRepository, and the same caching rules will be applied.
@CacheEvict
@Cacheable was easy. We can even consider making all the methods as @Cacheable. So what’s the problem?
The problem is size; we don’t want to populate the cache with values that we don’t need that often. Caches can grow very fast and they can fill with useless data.
The @CacheEvict annotation is used to indicate the deletion of one, some, or all of the values, so that fresh values can be saved into the cache again:
@CacheEvict(value="user", allEntries=true)
public String getUser(long id) {...}
Here we used another parameter called allEntries with the cache to be emptied in order to clear all the entries in the cache user and prepare it for fresh data.
>>> Unlike @Cacheable and @CachePut, @CacheEvict can be used on void methods. @Cacheable and @CachePut require a non-void return value, which is the item to place in the cache. But because @CacheEvict is only removing items from the cache, it can be placed on any method, even a void one.
@CachePut
While @CacheEvict decreases the overhead of finding entries in a large cache by removing stale and unused entries, ideally we want to avoid evicting too much data out of the cache.
Instead, we would want to selectively and intelligently update the entries whenever they’re changed.
With the @CachePut annotation, we can update the content of the cache without disturbing the method execution. That is, the method would always be executed and the result cached.
@CachePut(value="user")
public User getUser(long id) {...}
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.
@Caching
We might want to use multiple Caching techniques at the same time. Look at the incorrect example below:
@CacheEvict("user")
@CacheEvict(value="directory", key=user.name)
public String getUser(long id) {...}
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("user"),
@CacheEvict(value="directory", key="#user.name") })
public String getUser(long id) {...}
Similarly, we can group multiple caching annotations with @Caching, and use it to implement our own customized caching logic.
Conditional Caching
There are situations when we want to give fresh data even if the same parameters are provided. The good thing is, we can impose some conditions when a result should be provided from cache and when the actual underlying method should be invoked.
Conditional parameter
When we want more control over when an annotation is active or not, @CachePut can be parametrized with a condition parameter that takes a SpEL expression to indicate that the results are cached based on evaluating that expression. Let’s look at a supporting example:
@CachePut(value="user", condition="#user.name=='Steven'")
public User getUser(long id) {...}
Unless Parameter
We can also control the caching based on the output of the method instead of the input via the unless parameter:
@CachePut(value="user", unless="#result.length() > 24")
public String getUserName(long id) {...}
The above annotation would cache only those user names that have really long names (>24 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 useful for managing large results and customizing behavior based on input parameters instead of enforcing a generic behavior to all operations.
Conclusion
Caching is a great option to keep our application logic from having to find, calculate, or retrieve the same result over and over again for the same request. When a method is initially invoked with a given set of parameters, the return value can be saved in a cache and retrieved from that cache later when the same method is called with the same parameters. In many cases, looking up a value from a cache is a cheaper operation then looking it up otherwise (for example, performing a database query). Therefore, caching can have a positive impact on application performance.
Recent Stories
Top DiscoverSDK Experts
Compare Products
Select up to three two products to compare by clicking on the compare icon () of each product.
{{compareToolModel.Error}}
{{CommentsModel.TotalCount}} Comments
Your Comment