A Guide To Caching in Spring

1. The Cache Abstraction?

In this article, we’re going to show how to use the Caching Abstraction in Spring – and generally, improve the performance of your 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.

Further reading:

Spring Boot Ehcache Example

A quick and practical guide to using Spring with Ehcache.

Read more

Cache Eviction in Spring Boot

Learn how to invalidate caches with Spring Boot.

Read more

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.1.8.RELEASE</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 you’re going to use those as your cache storage, then use the spring-context-support module instead:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context-support</artifactId>
    <version>5.1.8.RELEASE</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 you’re a Spring Boot user, then use 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>
</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.

The caching feature can be declaratively enabled by simply adding the @EnableCaching annotation to any of the configuration classes:

@Configuration
@EnableCaching
public class CachingConfig {

    @Bean
    public CacheManager cacheManager() {
        return new ConcurrentMapCacheManager("addresses");
    }
}

You 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.

Using XML does enable more flexible options to configure caching – you can specify your own Cache-Manager, Cache-Resolver, Error-Handler and generally, use more advanced customization options (refer to the Javadoc for more details).

3.1. 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<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.

4. Use Caching With Annotations

Once you’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 contains 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 – we 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.

The @CacheEvict annotation is used 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 – to 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, ideally, you want to avoid evicting too much data out of the cache.

Instead, you’d want to selectively and intelligently update the entries whenever they’re altered.

With the @CachePut annotation, you can update the content of the cache without interfering the method execution. That is, the method would 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 you want to use multiple annotations of the same type for caching a method. Look at the incorrect example below:

@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, you can group multiple caching annotations with @Caching, and use it to implement your own customized caching logic.

4.5. @CacheConfig

With the @CacheConfig annotation, you can streamline some of the cache configuration into a single place – at the class level – so that you 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.

For example – 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

Now – if we want more control over when the annotation is active – @CachePut can be parametrized with a condition parameter that takes a SpEL expression to ensure 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 are 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 useful for managing large results and customizing behavior based on input parameters instead of enforcing a generic behavior to all operations.

6. Declarative XML-based Caching

In case you don’t have access to your application’s source code or want to inject the caching behavior externally, you 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. The Java-based Caching

And 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 – this is an Eclipse based project, so it should be easy to import and run as it is.

Leave a Reply

Your email address will not be published.