Engineering Full Stack Apps with Java and JavaScript
Spring Framework provides support for transparently adding caching into an existing Spring application, with minimal impact on the code, similar to the transaction support. Spring also makes good use of annotations. This note is a summary of Spring’s documentation (see reference) on Caching, for quick reading and reference.
The cache abstraction in Spring applies caching to Java methods. Each time a targeted method is invoked, the abstraction checks if the method has been already executed for the given arguments. If it has, then the cached result is returned; if it has not, then method is executed, the result cached and returned to the user. Spring’s caching approach works only for methods that are guaranteed to return the same output for a given arguments always.
The caching service is an abstraction and need an actual storage to store the cache data. The abstraction provides Cache and CacheManager interfaces. Implementations include JDK’s ConcurrentMap based caches, Ehcache 2.x, Gemfire cache, Caffeine, Guava caches and JSR-107 compliant caches (e.g. Ehcache 3.x). Any special handling of multi-threading and multi-process environments are handled by implementation.
Caching declarations identify the methods that need to be cached and their policy. Spring provides annotations for caching declaration and configuration: @Cacheable triggers cache population, @CacheEvict triggers cache eviction, @CachePut updates the cache without interfering with the method execution, @Caching regroups multiple cache operations to be applied on a method and @CacheConfig shares some common cache-related settings at class-level.
@Cacheable
@Cacheable is used to mark methods that are cacheable. The annotation declaration requires the name of the cache associated with the annotated method:
@Cacheable("books")
public Book findBook(ISBN isbn) {...}
The annotation allows multiple cache names to be specified as in: @Cacheable({"books", "isbns"}).
KeyGenerator
Caches are essentially key-value stores, and each invocation needs to have a suitable key for cache access. The abstraction uses a KeyGenerator based on a simple algorithm, which works well for most use-cases; as long as parameters have natural keys and implement valid hashCode() and equals() methods. To provide a different default key generator, one needs to implement Spring’s KeyGenerator interface.
The @Cacheable annotation also allows the user to specify how the key is generated through its key attribute.
@Cacheable(cacheNames="books", key="#isbn")
Similarly you may define a custom keyGenerator on the operation:
@Cacheable(cacheNames="books", keyGenerator="myKeyGenerator")
Note: The key and keyGenerator parameters are mutually exclusive and an operation specifying both will result in an exception.
CacheManager and CacheResolver
Out of the box, the caching abstraction uses a simple CacheResolver that retrieves the cache(s) defined at the operation level using the configured CacheManager.
For applications working with several cache managers, it is possible to set the cacheManager to use per operation:
@Cacheable(cacheNames="books", cacheManager="anotherCacheManager")
It is also possible to replace the CacheResolver entirely:
@Cacheable(cacheResolver="runtimeCacheResolver")
Sync Attribute
The sync attribute can be used to instruct the underlying cache provider to lock the cache entry while the value is being computed: only one thread will be computing the value while the others are blocked until the entry is updated in the cache:
@Cacheable(cacheNames="foos", sync="true")
Conditional caching
@Cacheable also support conditional caching using SpEL:
@Cacheable(cacheNames="book", condition="#name.length() < 32")
@CachePut
@CachePut annotation marks a method to be executed always without interfering with the method execution. Supports the same options as @Cacheable and should be used for cache population:
@CachePut(cacheNames="book", key="#isbn")
@CacheEvict
@CacheEvict demarcates methods that perform cache eviction (removing data from the cache). Similar to @Cacheable in options, but features an extra parameter allEntries which indicates whether a cache-wide eviction needs to be performed rather than just an entry one (based on the key):
@CacheEvict(cacheNames="books", allEntries=true)
@Caching
@Caching annotation allows multiple nested @Cacheable, @CachePut and @CacheEvict to be used on the same method:
@Caching(evict = { @CacheEvict("primary"), @CacheEvict(cacheNames="secondary", key="#p0") })
@CacheConfig
@CacheConfig is a class-level annotation that allows to share the cache names, the custom KeyGenerator, the custom CacheManager and finally the custom CacheResolver.
Placing this annotation on the class does not turn on any caching operation. Need to use Cacheable or other relevant annotations over methods.
An operation-level customization will always override a customization set on @CacheConfig.
@EnableCaching
To enable caching annotations add the annotation @EnableCaching to one of your @Configuration classes.
@Cacheable in proxy mode
In proxy mode (which is the default), only external method calls coming in through the proxy are intercepted. This means that self-invocation, in effect, a method within the target object calling another method of the target object, will not lead to an actual caching at runtime.
JCache standard annotations
Since the Spring Framework 4.1, the caching abstraction fully supports the JCache standard annotations: these are @CacheResult, @CachePut, @CacheRemove and @CacheRemoveAll as well as the @CacheDefaults, @CacheKey and @CacheValue companions.
Both @EnableCaching and the cache:annotation-driven element will enable automatically the JCache support if both the JSR-107 API and the spring-context-support module are present in the classpath.
Common Practices
Spring recommends that you only annotate concrete classes (and methods of concrete classes) with the @Cache* annotation, as opposed to annotating interfaces. Java annotations are not inherited from interfaces.
For testing, we can wire in a simple, dummy cache that performs no caching - that is, forces the cached methods to be executed every time.