1. The Cache Abstraction?

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.

2. Getting Started

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>5.3.3</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>5.3.3</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.

2.1. Spring Boot

If we use Spring Boot, then we can utilize the spring-boot-starter-cache starter package to easily add the caching dependencies:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
    <version>2.4.0</version>
</dependency>

Under the hood, the starter brings the spring-context-support module.

3. Enable Caching

To enable caching, Spring makes good use of annotations, much like enabling any other configuration level feature in the framework.

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");
    }
}

We can, of course, enable cache management with XML configuration as well:

<beans>
    <cache:annotation-driven />

    <bean id="cacheManager" class="org.springframework.cache.support.SimpleCacheManager">
        <property name="caches">
            <set>
                <bean 
                  class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean" 
                  name="addresses"/>
            </set>
        </property>
    </bean>
</beans>

Note: After we enable caching, for the minimal setup, we must register a cacheManager.

3.1. Using Spring Boot

When using Spring Boot, 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 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.

4. Use Caching With Annotations

Once we’ve enabled caching, the next step is to bind the caching behavior to the methods with declarative annotations.

4.1. @Cacheable

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.

4.2. @CacheEvict

Now, what would be the problem with making all methods @Cacheable?

The problem is size. W****e don’t want to populate the cache with values that we don’t need often. Caches can grow quite large, 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.

4.3. @CachePut

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.

4.4. @Caching

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.

4.5. @CacheConfig

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) {...}

5. Conditional Caching

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) {...}

5.1. Condition Parameter

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) {...}

5.2. Unless Parameter

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.

6. Declarative XML-Based Caching

If we don’t have access to our application’s source code, or want to inject the caching behavior externally, we can also use declarative XML- based caching.

Here is our XML configuration:

<!-- the service that you wish to make cacheable -->
<bean id="customerDataService" 
  class="com.your.app.namespace.service.CustomerDataService"/>

<bean id="cacheManager" 
  class="org.springframework.cache.support.SimpleCacheManager"> 
    <property name="caches"> 
        <set> 
            <bean 
              class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean" 
              name="directory"/> 
            <bean 
              class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean" 
              name="addresses"/> 
        </set> 
    </property> 
</bean>
<!-- define caching behavior -->
<cache:advice id="cachingBehavior" cache-manager="cacheManager">
    <cache:caching cache="addresses">
        <cache:cacheable method="getAddress" key="#customer.name"/>
    </cache:caching>
</cache:advice>

<!-- apply the behavior to all the implementations of CustomerDataService interface->
<aop:config>
    <aop:advisor advice-ref="cachingBehavior"
      pointcut="execution(* com.your.app.namespace.service.CustomerDataService.*(..))"/>
</aop:config>

7. Java-Based Caching

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();
    }
}

8. Summary

In this article, we discussed the basics of Caching in Spring, and how to make good use of that abstraction with annotations.

The full implementation of this article can be found in the GitHub project.