In this article, we’ll have a look at the new Spring 3 feature: caching by annotation.
I Cache abstraction layer
Caching is an important feature for all applications needing high performance. Many open-source frameworks are available: EhCache, JBoss Cache, OSCache, Open Terracota …
Their integration with the application is sometimes easy sometimes more complex, depending on the desired features and use (distributed cache ? cache JMX management …)
Spring 3 introduces a new abstraction layer for caching services. The idea is to provide a set of common features, mainly annotations, to activate and manage the caches.
Since it is only an abstract layer, Spring 3 caching still needs a concrete implementation to work. The entry point for cache implementation is the CacheManager interface. By default 2 concrete implementations of CacheManager are provided:
- EhCacheCacheManager: default implementation for EhCache
- ConcurrentMapCacheManager: default implementation using Java ConcurrentHashMap as cache store
II Configuration
For this example, we’ll use EhCache as cache implementation.
First, you need to import the Spring cache namespace and add the &th;cache:annotation-driver> tag.
- cache-manager: id or name of the bean implementing the CacheManager interface. Can be omitted because by convention Spring will look for any bean with the name “cacheManager“
- mode: proxy or aspectj. “proxy” will use Spring AOP framework to proxy any class having a caching annotation (see below). “aspectj” will rely on AspectJ aspect for cache management
- order: optional. If you have more than one aspect declared on the same method, it can be useful to specify the order in which they execute
Then you need to declare the cacheManager bean and implementation instance (or factory bean)
First, we declare the EhCacheCacheManager as the default cache manager. Then we inject the EhCacheManagerFactoryBean so it can retrieve an instance of EhCache class.
Please notice the very confusing naming. The EhCacheCacheManager instance is given the id “cacheManager” as per Spring convention but it also has a cacheManager property. This property is indeed an instance of net.sf.ehcache.CacheManager, different from the org.springframework.cache.CacheManager interface.
Last but not least, you can specify the config file for ehcache with the property configLocation of EhCacheManagerFactoryBean. If not declared it will default to “ehcache.xml“
III Cache annotations
By default Spring proposes several annotations to manage your caches:
- @Cacheable: put the method returned value(s) into the cache
- @CacheEvict: remove an entry from the cache
- @CachePut: force a cache entry to be updated
And that’s pretty much. But don’t be fooled by their apparent simplicity, they offer a lot of possibilities to fine-tune your caching.
A @Cacheable
When annotating a method with @Cacheable, its returned value will be put into the cache provided it meets some condition (if any). Consequently, it does not make sense to annotate a void method.
So what can be put in cache? Pretty much anything, an Object, a collection (List, Map, Set, Array).
When is the cache activated? All subsequent calls on the same method with the same arguments or cache key (we’ll see it later) will trigger the cache. Instead of executing the method, the cache is scanned to check whether a matching entry can be found, if yes then it is returned. Otherwise, the method is really executed and its result is put into the cache.
Below is the pseudo-code:
- Method is called ith arguments args
- Use the args hashCode or extract the cache key from the args to look for an entry in the cache
- If a corresponding entry is found
- Return the cached entry
- Else
- Execute really the method
- Put the method returned value into the cache using the args hashCode of extracted cache key
So what is the cache key and why do we need to use args hashCode? We’ll see it shortly. First, the @Cacheable annotation offers the following attributes:
- value: mandatory, the name of the cache to work on
- key: optional, the key used to store and fetch data from the cache
- condition: optional, specifies the condition to verify before caching an item
Example:
@Cacheable(value = "user-cache", key="#userSearchVO.name", condition="#supportUser == false")
public User findUser(UserSearchVO userSearchVO, boolean supportUser)
{
User foundUser = null;
...
...
...
return foundUser;
}
In the above example
- value = “user-cache” indicates the name of the cache in which entries are stored
- key=”#searchVO.name” defines the key to lookup into the cache. Here it is the name property of the UserSearchVO object
- condition=”#supportUser == false” provides the condition to trigger the cache. The cache lookup and the method result caching are triggered only when the supportUser flag is set to false
Please notice that for the key and condition attributes, we are using SpEL to process the method arguments.
This is very convenient because the SpEL expression language is extremely powerful. For example, if the UserSearchVO has an attribute searchType of enum type, we can do the following:
@Cacheable(value = "user-cache", key="#userSearchVO.name", condition="#userSearchVO.searchType.equals(${T(com.doan.spring.cache.SearchType).CLASSIC)")
public User findUser(UserSearchVO userSearchVO, boolean supportUser)
{
User foundUser = null;
...
...
...
return foundUser;
}
public enum SearchType
{
CLASSIC,
ADVANCED,
SUPPORT
}
In this case, the cache is triggered only when the search is of type CLASSIC.
Another simple example:
@Cacheable(value = {"user-cache1","user-cache2"})
public User findUserByLoginAndName(String login, String name)
{
User foundUser = null;
...
...
...
return foundUser;
}
In this example, we declare more than one cache (“user-cache1”,“user-cache2”) and no key information.
By default, if no cache key is provided Spring will compute a hash code of all method arguments (here it is log in & name) and use it as a key for cache lookup. If your method arguments are not of the primitive type and you want them to be used as the cache key, you should redefine properly the hashCode() method.
Last but not least, Spring will look scan each cache for key lookup, if an entry is found in any declared cache, it will be returned. If all caches are scanned and no entry is found, the method will be executed and the result added to all the declared caches.
B @CacheEvict
The @CacheEvict is used to trigger explicit cache eviction. By default, most of caching frameworks expire the cache data after some defined duration. The @CacheEvict annotation is useful when you want to control explicit cache eviction upon some method calls.
@CacheEvict(value = "user-cache", key = "#user.login")
public void updateUser(User user)
{
...
...
}
The above code is quite self-explanatory. The @CacheEvict annotation exposes the following attributes:
- value: mandatory, the name of the cache to evict entry
- key: optional, the key used to lookup data from the cache
- condition: optional, specifies the condition to verify before evicting a cache entry
- allEntries: optional, indicates that all entries from the cache should be removed
- beforeInvocation: optional, indicates whether the cache evict should be done before or after method call
Obviously, the key and allEntries attributes are mutually exclusive.
C @CachePut
The @CachePut annotation allows you to “update” a cache entry. This is very similar to @Cacheable but for entry update. The @CachePut annotation has exactly the same attributes as @Cacheable.
@CachePut(value = "user-cache", key = "#user.login")
public User updateUserName(User user,String newName)
{
...
...
user.setName(newName);
...
...
return user;
}
IV Gotchas
So far the caching abstraction infrastructure proposed by Spring is very convenient. However, for having used it in a real project, I’ve spotted 2 pain points
- It is not possible to define multiple caching policies easily on the same method
- Object mutability issues thought this point is not Spring’s specific but common to all caching frameworks
Multiple caching policies
Let’s suppose then we have an application with 2 cache regions: “user-cache” and “user-details-cache”.
@Cacheable(value = "user-cache", key="#login")
public User findUserByLogin(String login)
{
...
...
return foundUser;
}
...
...
@Cacheable(value = "user-details-cache", key="#login")
public UserDetails findUserDetailsByLogin(String login)
{
...
...
return userDetails;
}
It is possible to trigger the eviction from both caches with the same key:
@CacheEvict(value =
{
"user-cache",
"user-details-cache"
}, key="#login")
public void updateUserDetails(String login, UserDetails newUserDetails)
{
...
...
}
In this particular example, it’s working very well because we access both caches with the same key. What if we want to evict those caches with different keys?
@CacheEvict(value = "user-cache", key="#login")
@CacheEvict(value = "user-details-cache", key="#newUserDetails.id")
public void updateUserDetails(String login, UserDetails newUserDetails)
{
...
...
}
It’s simply not possible because Java does not allow you to have more than one type of annotation on the same method.
The idea is to create your own custom @UserDetailsEvict annotation and embedding the @CacheEvict annotation inside:
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Cacheable(value="user-details-cache", key="#newUserDetails.id")
public @interface UserDetailsEvict{
}
The major drawback of this solution is the hard-coding of the cache name and cache key into the custom annotation. If your key or cache name changes, you’ll need to change this custom annotation as well. As far as I know, there is no better solution than this hack currently.
B Object mutability
The other major gotcha when using the cached object is mutability issues. Indeed when you get an instance of User after calling findUserByLogin(String login), this instance may come from the cache directly.
If you are modifying this User instance (changing a user property for example), you are indeed modifying the object which is in the cache directly !!!
The immediate consequence is that on the same server if another client looks for the same user, the cache will give him the same User instance that has been modified earlier…
There is no obvious solution for this issue.
The first idea is to use objects returned from the cache as read-only but this rule cannot be enforced easily. Suppose that you put @Cacheable annotation on a Repository or DAO method, the developer that calls this method from the Service layer may not be aware that he’s getting a cached instance and may modify it.
The second fix is to perform a deep copy of the returned object. But again, implementing deep copy is not an easy task (as I mentioned in my article about Object Immutablity) and the deep copy should be done by the caller (Service layer in our example). The same issue with “knowing that we’re dealing with cached instances” mentioned above also applies.
Fortunately, the latest EHCache versions provide a very useful feature: copyOnRead.
With this flag turned on, EHCache will return you a deep copy of the object instance that is in the cache, not the instance itself. So the mutability issue is gone.
Of course, there is no free lunch, this convenient feature comes with its counterparts, the cost of serialization/deserialization because the default implementation is plain Java object serialization.