Caching made easy with CDI and Infinispan

When CDI and Inifispan meet you’ve got the chance to improve your code a lot. Let’s combine both to make all of your CDI beans @Cacheable.

Caching of values usually goes this way:

public String getSomething(String input) {
    String result = cache.contains(input);
    
	if(result == null) {
        result = getValueFromDatabase(input);
        cache.put(input, result);
    }
	
    return result;    
}

This pattern repeats for every value which is cached/retrieved. Methods like the one above contain repetitive conditionals and value retrievals. Using the caching interceptor pattern eliminates the need for repetition. Business methods will be reduced back to their essence and caching becomes an aspect.

@Cacheable
public String getSomething(@CacheKey String input) {
    return getValueFromDatabase(input);
}

How to:

  1. You need CDI (and Infinispan if you re-use the code without altering it)
  2. Grab the code from the Gist and put it into your project
  3. Annotate the methods you want to use the caching
  4. Enable the caching interceptor in your beans.xml
    <beans xmlns="http://java.sun.com/xml/ns/javaee"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://jboss.org/schema/cdi/beans_1_0.xsd">
        <interceptors>
            <class>...CachingInterceptor</class>
        </interceptors>
    </beans>
  5. Build yourself a clear operation to manually evict the cache, you’ll need it.

Gist

import javax.interceptor.InterceptorBinding;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Marks a method as Cacheable. The outcome of the method will be cached. A key is used to distinguish between different
* invocations/requests. Use @CacheKey to mark an argument as part of a cache key. Every outcome will be cached, even
* null values.
*
*/
@InterceptorBinding
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD, ElementType.TYPE })
public @interface Cacheable {
}
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Marker for a method argument to use it as part of the cache key (multiple arguments can be are annotated with @CacheKey)
*
*/
@Target({ ElementType.PARAMETER })
@Retention(RetentionPolicy.RUNTIME)
public @interface CacheKey {
}
import static com.google.common.base.Preconditions.*;
import javax.interceptor.AroundInvoke;
import javax.interceptor.Interceptor;
import javax.interceptor.InvocationContext;
import javax.naming.InitialContext;
import java.io.Serializable;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import com.google.common.collect.Lists;
import org.apache.commons.lang3.StringUtils;
import org.infinispan.Cache;
import org.infinispan.manager.CacheContainer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Caching interceptor to perform caching on @Cacheable methods.
*
* Use @Cacheable on any method you want to cache (for it's output). Add @CacheKey annotations to every parameter,
* that is relevant for the caching key.
*
* {@code
* @Cacheable
* public int[] fibonacci(@CacheKey int base){...}
* }
*
* The values are cached per class, method and per cacheKey. So you can use your backing cache to store data from multiple, @Cacheable methods.
* Currently @UseInfinispan is supported as backing cache implementation.
*
*/
@Interceptor
@Cacheable
public class CachingInterceptor implements Serializable {
private static final Logger LOGGER = LoggerFactory.getLogger(CachingInterceptor.class);
@AroundInvoke
public Object perform(InvocationContext ctx) throws Exception {
LOGGER.debug("perform invocation for " + ctx.getMethod().getDeclaringClass().getSimpleName() + "#"
+ ctx.getMethod().getName());
CompositeCacheKey cacheKey = getCacheKeys(ctx.getMethod(), ctx.getParameters());
Cache<CompositeCacheKey, CacheEntry> cache = lookupCache(ctx.getMethod());
CacheEntry entry = cache.get(cacheKey);
if (entry == null) {
LOGGER.debug("perform|no cached value, performing invocation");
Object value = ctx.proceed();
entry = new CacheEntry(value);
cache.put(cacheKey, entry);
} else {
LOGGER.debug("perform|using cached value");
}
return entry.getValue();
}
private Cache<CompositeCacheKey, CacheEntry> lookupCache(Method method) throws Exception{
UseInfinispan useInfinispan = method.getAnnotation(UseInfinispan.class);
if (useInfinispan == null) {
useInfinispan = method.getDeclaringClass().getAnnotation(UseInfinispan.class);
}
checkState(useInfinispan != null, "@" + UseInfinispan.class.getSimpleName() + " is not declared on " + method
+ " or its class.");
String jndiName = useInfinispan.value();
if (StringUtils.isEmpty(jndiName)) {
jndiName = useInfinispan.mappedName();
}
if (StringUtils.isEmpty(jndiName)) {
checkArgument(StringUtils.isNotEmpty(useInfinispan.value()), "@" + UseInfinispan.class.getSimpleName()
+ ".mappedName is empty");
}
LOGGER.debug("lookupCache|infinispanJdniName=" + jndiName);
CacheContainer cacheContainer = InitialContext.doLookup(jndiName);
if (StringUtils.isEmpty(useInfinispan.cacheName())) {
return cacheContainer.getCache();
}
return cacheContainer.getCache(useInfinispan.cacheName());
}
private CompositeCacheKey getCacheKeys(Method method, Object[] parameters) {
checkState(method.getParameterTypes().length == parameters.length,
"Method-Parameter and Invocation-Parameter-Count do not match.");
List<Object> result = Lists.newArrayList();
Annotation[][] parameterAnnotations = method.getParameterAnnotations();
result.add(method.getDeclaringClass().getName());
result.add(method.getName());
for (int i = 0; i < parameterAnnotations.length; i++) {
Annotation[] annotations = parameterAnnotations[i];
if (isCacheKey(annotations)) {
result.add(parameters[i]);
}
}
LOGGER.debug("getCacheKeys|cacheKeys=" + result);
return new CompositeCacheKey(result);
}
private boolean isCacheKey(Annotation[] annotations) {
for (Annotation annotation : annotations) {
if (annotation.annotationType().equals(CacheKey.class)) {
return true;
}
}
return false;
}
private class CacheEntry implements Serializable {
private Object value;
private CacheEntry(Object value) {
this.value = value;
}
public Object getValue() {
return value;
}
}
private class CompositeCacheKey implements Serializable {
private List<Object> objects;
private CompositeCacheKey(List<Object> objects) {
this.objects = objects;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof CompositeCacheKey)) {
return false;
}
CompositeCacheKey that = (CompositeCacheKey) o;
if (objects != null ? !objects.equals(that.objects) : that.objects != null) {
return false;
}
return true;
}
@Override
public int hashCode() {
return objects != null ? objects.hashCode() : 0;
}
}
}
@ApplicationScoped
@UseInfinispan("java:/cache/infinispan")
public class Example {
@Cacheable
public int[] fibonacci(@CacheKey int base){
return ...
}
@Cacheable
public int logN(@CacheKey int base){
return ...
}
@UseInfinispan(mappedName = "java:/cache/different/infinispan", cacheName = "otherCache")
@Cacheable
public String calculateSomething(@CacheKey String inputParameter, String notCacheRelevantParameter){
return ...
}
}
import javax.interceptor.InterceptorBinding;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Value is a substitute for mappedName. If cacheName is empty, the default cache is used.
*
*/
@InterceptorBinding
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD, ElementType.TYPE })
public @interface UseInfinispan {
public String value() default "";
public String mappedName() default "";
public String cacheName() default "";
}

You may also enjoy…