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:
- You need CDI (and Infinispan if you re-use the code without altering it)
- Grab the code from the Gist and put it into your project
- Annotate the methods you want to use the caching
- 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>
- Build yourself a clear operation to manually evict the cache, you’ll need it.
Gist
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 { | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 { | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@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 ... | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 ""; | |
} |