Intro
Caches are tremendously useful in a wide variety of use cases. For example, you should consider using caches when a value is expensive to compute or retrieve, and you will need its value on a certain input more than once.
A Cache is similar to ConcurrentMap, but not quite the same. The most fundamental difference is that a ConcurrentMap persists all elements that are added to it until they are explicitly removed. A Cache on the other hand is generally configured to evict entries automatically, in order to constrain its memory footprint.
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.
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.
Declarative caching declaration
Threre are a few annotations in Spring which help us to configure our cache:
- @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. It's useful to note that default cache key generation occurs depending on method arguments. So,
- if method has no arguments, than 0 is used.
- if only one param is given, that instance is used as a key.
- if more than one afrgument is given, a key computed from hashes of all params is used
More info you can find in javavadoc of DefaultKeyGenerator class. There is an option to provide own implementation of key generator.
- @CachePut as opposed to @Cacheable annotation this annotation doesn't cause the method to be skipped - rather it always causes the method to be invoked and its result to be placed into the cache.
- @CacheEvict is useful to remove stale data from cache
But just annotations are not enough to configure our cache. Also we have to enable annotation by specifying <cache:annotation-driven/> in the applicationContext.xml
Method visibility and caching annotations
When using proxies, you should apply caching annotations only to public methods. If you put an annotation on private, protected or package visible method no error is raised, but caching simply will not apply.
Also note that buy default Spring uses JDK proxying mechanism, so implemenation of a service has to be accessed through the service interface. If you access a service directly(without interface) Spring will generate the proxy class(at startup time) but in that case CGLIB 2 should be on classpath(NOTE: Spring 3.2 already has CGLIB2, so if you are the user of that version of Spring, you shouldn't additionally include this library). There is an option to change proxy mode, but be very careful, if you specify proxy-target-class="true on <cache:annotation-driven/>.
Because(from Spring reference):
Multiple <aop:config/>
sections are collapsed into a single unified auto-proxy creator at runtime, which applies the strongestproxy settings that any of the <aop:config/>
sections (typically from different XML bean definition files) specified. This also applies to the <tx:annotation-driven/>
and <aop:aspectj-autoproxy/>
elements.
To be clear: using 'proxy-target-class="true"
' on <tx:annotation-driven/>
, <aop:aspectj-autoproxy/>, <cache:annotation-driven>
or <aop:config/>
elements will force the use of CGLIB proxies for all three of them.
Configuring Guava Cache as Storage
Out of the box, Spring 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.
In project I decided to use Guava's LoadingCache. For that purpose I've created GuavaCacheFactoryBean which helps to configure and create LoadingCache instance. Actually, I used Spring's SimpleCacheManger which uses ConcurrentMap. So, GuavaCacheFactoryBean also converts LoadingCache to ConcurrentMapCache through the Guava's Cache#asMap() method.
Xml configuration:
<cache:annotation-driven cache-manager="cacheManager">
<bean id="cacheManager" class="org.springframework.cache.support.SimpleCacheManager">
<property name="caches">
<set>
<bean parent="parentCacheFactoryBean" p:name="attributes"/>
<bean parent="parentCacheFactoryBean" p:name="history"/>
<bean parent="parentCacheFactoryBean" p:name="actions" p:maxSize="100"/>
</set>
</property>
</bean>
<bean id="parentCacheFactoryBean" abstract="true" class="com.example.GuavaCacheFactoryBean"
p:expirationAccessTime="120"
p:maxSize="200" />
NOTE: cache-manager="cacheManager" is a default attribute in <cache:annotation-driven/>
And the GuavaCacheFactoryBean code(getters and setters are omitted):
public class GuavaCacheFactoryBean implements FactoryBean<ConcurrentMapCache> {
private String name;
private int maxSize;
private int expirationAccessTime;
private ConcurrentMap<Object, Object> store;
private ConcurrentMapCache cache;
@Override
public ConcurrentMapCache getObject() throws Exception {
return cache;
}
@Override
public Class<?> getObjectType() {
return ConcurrentMapCache.class;
}
@Override
public boolean isSingleton() {
return true;
}
@PostConstruct
public void init() {
this.store = CacheBuilder.newBuilder()
.expireAfterAccess(expirationAccessTime, TimeUnit.MINUTES)
.maximumSize(getMaxSize())
.build().asMap();
this.cache = new ConcurrentMapCache(this.getName(), this.store, true);
}
...
}
There are many other options to configure
LoadingCache via
CacheBuilder, but for the purpose of the project I only need expireAfterAccess and maximumSize.
Here you can see examples of using caching annotations:
@Caching(evict = {
@CacheEvict(value = "attributes", key = CACHE_KEY_EXPR),
@CacheEvict(value = "history", key = CACHE_KEY_EXPR),
@CacheEvict(value = "actions", key = "#p0")
})
public void saveAction(long arg1, String arg2, long ar3) throws Exception{
//here I make an access to db
}
In this example I use @Caching annotaion which is simply a grouping annotation for all cache annotations. It is used only if you have to specify a couple caching annotations on method. Also note how I access the method arguments(#p0). It's a preferred way; you can however use argument name, but it may not work since debugging info can not be included in your class files. With key attribute I specify the cache key.
Different storages
To plug another cache back-end you have to provide CacheManager and Cache implementations. That classes tend to be simple adapters that map caching abstraction framework on top of the storage API as the ehcache classes can show.