27. Cache Abstraction

27.1 Introduction

Since version 3.1, Spring Framework provides support for transparently adding caching into an existing Spring application. Similar to the transaction support, the caching abstraction allows consistent use of various caching solutions with minimal impact on the code.

27.2 Understanding the cache abstraction

At its core, the abstraction applies caching to Java methods, reducing thus the number of executions based on the information available in the cache. That is, each time a targeted method is invoked, the abstraction will apply a caching behaviour checking whether the method has been already executed for the given arguments. If it has, then the cached result is returned without having to execute the actual method; if it has not, then method is executed, the result cached and returned to the user so that, the next time the method is invoked, the cached result is returned. This way, expensive methods (whether CPU or IO bound) can be executed only once for a given set of parameters and the result reused without having to actually execute the method again. The caching logic is applied transparently without any interference to the invoker.

[Important]Important
Obviously this approach works only for methods that are guaranteed to return the same output (result) for a given input (or arguments) no matter how many times it is being executed.

To use the cache abstraction, the developer needs to take care of two aspects:

  • caching declaration - identify the methods that need to be cached and their policy
  • cache configuration - the backing cache where the data is stored and read from

Note that just like other services in Spring Framework, the caching service is an abstraction (not a cache implementation) and requires the use of an actual storage to store the cache data - that is, the abstraction frees the developer from having to write the caching logic but does not provide the actual stores. There are two integrations available out of the box, for JDK java.util.concurrent.ConcurrentMap and Ehcache - see Section 27.5, “Plugging-in different back-end caches” for more information on plugging in other cache stores/providers.

27.3 Declarative annotation-based caching

For caching declaration, the abstraction provides two Java annotations: @Cacheable and @CacheEvict which allow methods to trigger cache population or cache eviction. Let us take a closer look at each annotation:

27.3.1 @Cacheable annotation

As the name implies, @Cacheable is used to demarcate methods that are cacheable - that is, methods for whom the result is stored into the cache so on subsequent invocations (with the same arguments), the value in the cache is returned without having to actually execute the method. In its simplest form, the annotation declaration requires the name of the cache associated with the annotated method:

@Cacheable("books")
public Book findBook(ISBN isbn) {...}

In the snippet above, the method findBook is associated with the cache named books. Each time the method is called, the cache is checked to see whether the invocation has been already executed and does not have to be repeated. While in most cases, only one cache is declared, the annotation allows multiple names to be specified so that more then one cache are being used. In this case, each of the caches will be checked before executing the method - if at least one cache is hit, then the associated value will be returned:

[Note]Note
All the other caches that do not contain the method will be updated as well event though the cached method was not actually executed.
@Cacheable({ "books", "isbns" })
public Book findBook(ISBN isbn) {...}

27.3.1.1 Default Key Generation

Since caches are essentially key-value stores, each invocation of a cached method needs to be translated into a suitable key for cache access. Out of the box, the caching abstraction uses a simple hash-code based KeyGenerator that computes the key based on the hashes of all objects used for method invocation. This approach works well for objects with natural keys as long as the hashCode() reflects that. If that is not the case then for distributed or persistent environments, the strategy needs to be changed as the objects hashCode is not preserved. In fact, depending on the JVM implementation or running conditions, the same hashCode can be reused for different objects, in the same VM instance.

To provide a different default key generator, one needs to implement the org.springframework.cache.KeyGenerator interface. Once configured, the generator will be used for each declaration that doesn not specify its own key generation strategy (see below).

27.3.1.2 Custom Key Generation Declaration

Since caching is generic, it is quite likely the target methods have various signatures that cannot be simply mapped on top of the cache structure. This tends to become obvious when the target method has multiple arguments out of which only some are suitable for caching (while the rest are used only by the method logic). For example:

@Cacheable("books")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed

At first glance, while the two boolean arguments influence the way the book is found, they are no use for the cache. Further more what if only one of the two is important while the other is not?

For such cases, the @Cacheable annotation allows the user to specify how the key is generated through its key attribute. The developer can use SpEL to pick the arguments of interest (or their nested properties), perform operations or even invoke arbitrary methods without having to write any code or implement any interface. This is the recommended approach over the default generator since methods tend to be quite different in signatures as the code base grows; while the default strategy might work for some methods, it rarely does for all methods.

Below are some examples of various SpEL declarations - if you are not familiar with it, do yourself a favour and read Chapter 6, Spring Expression Language (SpEL):

@Cacheable(value="book", key="isbn"
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)


@Cacheable(value="book", key="isbn.rawNumber")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)


@Cacheable(value="book", key="T(someType).hash(isbn)")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)

The snippets above, show how easy it is to select a certain argument, one of its properties or even an arbitrary (static) method.

27.3.1.3 Conditional caching

Sometimes, a method might not be suitable for caching all the time (for example, it might depend on the given arguments). The cache annotations support such functionality through the conditional parameter which takes a SpEL expression that is evaluated to either true or false. If true, the method is cached - if not, it behaves as if the method is not cached, that is executed every since time no matter what values are in the cache or what arguments are used. A quick example - the following method will be cached, only if the argument name has a length shorter then 32:

@Cacheable(value="book", condition="name.length < 32")
public Book findBook(String name)

27.3.1.4 Available caching SpEL evaluation context

Each SpEL expression evaluates again a dedicated context. In addition to the build in parameters, the framework provides dedicated caching related metadata such as the argument names. The next table lists the items made available to the context so one can use them for key and conditional(see next section) computations:

Table 27.1. Cache SpEL available metadata

NameLocationDescriptionExample
methodNameroot objectThe name of the method being invoked
#root.methodName
cachesroot objectCollection of caches against which the current method is executed
#root.caches[0].name
parameter nameevaluation contextName of any of the method parameter. If for some reason the names are not available (ex: no debug information), the parameter names are also available under the p<#arg> where #arg stands for the parameter index (starting from 0).
iban
or
p0

27.3.2 @CacheEvict annotation

The cache abstraction allows not just population of a cache store but also eviction. This process is useful for removing stale or unused data from the cache. Opposed to @Cacheable, annotation @CacheEvict demarcates methods that perform cache eviction, that is methods that act as triggers for removing data from the cache. Just like its sibling, @CacheEvict requires one to specify one (or multiple) caches that are affected by the action, allows a key or a condition to be specified but in addition, features an extra parameter allEntries which indicates whether a cache-wide eviction needs to be performed rather then just an entry one (based on the key):

@CacheEvict(value = "books", allEntries=true)
public void loadBooks(InputStream batch)

This option comes in handy when an entire cache region needs to be cleared out - rather then evicting each entry (which would take a long time since it is inefficient), all the entires are removed in one operation as shown above. Note that the framework will ignore any key specified in this scenario as it does not apply (the entire cache is evicted not just one entry).

It is important to note that void methods can be used with @CacheEvict - as the methods act as triggers, the return values are ignored (as they don't interact with the cache) - this is not the case with @Cacheable which adds/update data into the cache and thus requires a result.

27.3.3 Enable caching annotations

It is important to note that even though declaring the cache annotations does not automatically triggers their actions - like many things in Spring, the feature has to be declaratively enabled (which means if you ever suspect caching is to blame, you can disable it by removing only one configuration line rather then all the annotations in your code). In practice, this translates to one line that informs Spring that it should process the cache annotations, namely:

<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:cache="http://www.springframework.org/schema/cache"
   xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/cache http://www.springframework.org/schema/cache/spring-cache.xsd">
  <cache:annotation-driven />

The namespace allows various options to be specified that influence the way the caching behaviour is added to the application through AOP. The configuration is similar (on purpose) with that of tx:annotation-driven:

Table 27.2. <cache:annotation-driven/> settings

AttributeDefaultDescription
cache-managercacheManager

Name of cache manager to use. Only required if the name of the cache manager is not cacheManager, as in the example above.

modeproxy

The default mode "proxy" processes annotated beans to be proxied using Spring's AOP framework (following proxy semantics, as discussed above, applying to method calls coming in through the proxy only). The alternative mode "aspectj" instead weaves the affected classes with Spring's AspectJ transaction aspect, modifying the target class byte code to apply to any kind of method call. AspectJ weaving requires spring-aspects.jar in the classpath as well as load-time weaving (or compile-time weaving) enabled. (See Section 7.8.4.5, “Spring configuration” for details on how to set up load-time weaving.)

proxy-target-classfalse

Applies to proxy mode only. Controls what type of transactional proxies are created for classes annotated with the @Cacheable or @CacheEvict annotations. If the proxy-target-class attribute is set to true, then class-based proxies are created. If proxy-target-class is false or if the attribute is omitted, then standard JDK interface-based proxies are created. (See Section 7.6, “Proxying mechanisms” for a detailed examination of the different proxy types.)

orderOrdered.LOWEST_PRECEDENCE

Defines the order of the cache advice that is applied to beans annotated with @Cacheable or @CacheEvict. (For more information about the rules related to ordering of AOP advice, see Section 7.2.4.7, “Advice ordering”.) No specified ordering means that the AOP subsystem determines the order of the advice.


[Note]Note

The proxy-target-class attribute on the <cache:annotation-driven/> element controls what type of caching proxies are created for classes annotated with the @[email protected] annotation. If proxy-target-class attribute is set to true, class-based proxies are created. If proxy-target-class is false or if the attribute is omitted, standard JDK interface-based proxies are created. (See Section 7.6, “Proxying mechanisms” for a discussion of the different proxy types.)

[Note]Note

<cache:annotation-driven/> only looks for @[email protected] on beans in the same application context it is defined in. This means that, if you put <cache:annotation-driven/> in a WebApplicationContext for a DispatcherServlet, it only checks for @[email protected] beans in your controllers, and not your services. See Section 15.2, “The DispatcherServlet” for more information.

27.3.4 Using custom annotations

The caching abstraction allows one to use her own annotations to identify what method trigger cache population or eviction. This is quite handy as a template mechanism as it eliminates the need to duplicate cache annotation declarations (especially useful if the key or condition are specified) or if the foreign imports (org.springframework) are not allowed in your code base. Similar to the rest of the stereotype annotations, both @Cacheable and @CacheEvict can be used as meta-annotations, that is annotations that can annotate other annotations. To wit, let us replace a common @Cacheable declaration with our own, custom annotation:

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Cacheable(value=“books”, key="isbn")
public @interface SlowService {
}

Above, we have defined our own SlowService annotation which itself is annotated with @Cacheable - now we can replace the following code:

@Cacheable(value="books", key="isbn"
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)

with:

@SlowService
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)

Even though @SlowService is not a Spring annotation, the container automatically picks up its declaration at runtime and understands its meaning. Note that as mentined above, the annotation-driven behaviour needs to be enabled.

27.4 Configuring the cache storage

Out of the box, the cache abstraction provides integration with two storages - one on top of the JDK ConcurrentMap and one for ehcache library. To use them, one needs to simply declare an appropriate CacheManager - an entity that controls and manages Caches and can be used to retrieve these for storage.

27.4.1 JDK ConcurrentMap-based Cache

The JDK-based Cache implementation resides under org.springframework.cache.concurrent package. It allows one to use ConcurrentHashMap as a backing Cache store.

<!-- generic cache manager -->
<bean id="cacheManager" class="org.springframework.cache.support.SimpleCacheManager">
  <property name="caches">
    <set>
      <bean class="org.springframework.cache.concurrent.ConcurrentCacheFactoryBean" p:name="default"/>
      <bean class="org.springframework.cache.concurrent.ConcurrentCacheFactoryBean" p:name="books"/>
    </set>
  </property>
</bean>

The snippet above uses the SimpleCacheManager to create a CacheManager for the two, nested Concurrent Cache implementations named default and books. Note that the names are configured directly for each cache.

As the cache is created by the application, it is bound to its lifecycle, making it suitable for basic use cases, tests or simple applications. The cache scales well and is very fast but it does not provide any management or persistence capabilities nor eviction contracts.

27.4.2 Ehcache-based Cache

The Ehcache implementation is located under org.springframework.cache.ehcache package. Again, to use it, one simply needs to declare the appropriate CacheManager:

<bean id="cacheManager" class="org.springframework.cache.ehcache.EhcacheCacheManager" p:cache-manager="ehcache"/>

<!-- Ehcache library setup -->
<bean id="ehcache" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean" p:config-location="ehcache.xml"/>

This setup bootstraps ehcache library inside Spring IoC (through bean ehcache) which is then wired into the dedicated CacheManager implementation. Note the entire ehcache-specific configuration is read from the resource ehcache.xml.

27.5 Plugging-in different back-end caches

Clearly there are plenty of caching products out there that can be used as a backing store. To plug them in, one needs to provide a CacheManager and Cache implementation since unfortunately there is no available standard that we can use instead. This may sound harder then it is since in practice, the classes tend to be simple adapters that map the caching abstraction framework on top of the storage API as the ehcache classes can show. Most CacheManager classes can use the classes in org.springframework.cache.support package, such as AbstractCacheManager which takes care of the boiler-plate code leaving only the actual mapping to be completed. We hope that in time, the libraries that provide integration with Spring can fill in this small configuration gap.