I probably had to write cache to store results of expensive operations or queries a dozens of times over last ten years of Java. And this week, I had to redo again. Though, there are a lot of libraries such as JCS , Terracota or Tangosol, but I just decided to write a simple implementation myself.
First, I defined an annotation that will be used to mark any class or methods cachable:
Java
x
import java.lang.reflect.*;
import java.lang.annotation.*;
/**
* Annotation for any class that wish to be cacheable. This annotation can be defined
* at the class level which will allow caching of all methods or at methods or both.
*/
ElementType.TYPE, ElementType.METHOD}) ({
RetentionPolicy.RUNTIME) (
public @interface Cacheable {
/**
* This method allows disabling cache for certain methods.
*/
boolean cache() default true;
/**
* This method defines maximum number of items that will be stored in cache. If
* number of items exceed this, then old items will be removed (LRU). This value
* should only be defined at class level and not at the method level.
*/
int maxCapacity() default 1000;
/**
* @return true if block of [ if (in cache) load ] is protected with lock. This
* will prevent two threads from executing the same load
*/
boolean synchronizeAccess() default false;
int timeoutInSecs() default 300;
/**
* @return true if cache will be populated asynchronously when it's about to expire.
*/
public boolean canReloadAsynchronously() default false;
}
I then defined a simple map class to store cache:
Java
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Method;
import java.util.*;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.ReentrantLock;
/**
* CacheMap - provides lightweight caching based on LRU size and timeout
* and asynchronous reloads.
*/
public class CacheMap<K, V> implements Map<K, V> {
private final static int MAX_THREADS = 10; // for all cache items across VM
private final static int MAX_ITEMS = 10000; // for all cache items across VM
private final static ExecutorService executorService = Executors.newFixedThreadPool(MAX_THREADS);
static class FixedSizeLruLinkedHashMap<K, V> extends LinkedHashMap<K, V> {
private final int maxSize;
public FixedSizeLruLinkedHashMap(int initialCapacity, float loadFactor, int maxSize) {
super(initialCapacity, loadFactor, true);
this.maxSize = maxSize;
}
protected boolean removeEldestEntry(java.util.Map.Entry<K, V> eldest) {
return size() > maxSize;
}
}
private final Cacheable classCacheable;
private final Map<K, Pair<Date, V>> map;
private final Map<Object, ReentrantLock> locks = new HashMap<Object, ReentrantLock>();
public CacheMap(Cacheable cacheable) {
this.classCacheable = cacheable;
int maxCapacity = cacheable != null && cacheable.maxCapacity() > 0
&& cacheable.maxCapacity() < MAX_ITEMS ? cacheable.maxCapacity() : MAX_ITEMS;
this.map = Collections.synchronizedMap(
new FixedSizeLruLinkedHashMap<K, Pair<Date, V>>(
maxCapacity / 10, 0.75f, maxCapacity));
}
public int size() {
return map.size();
}
public boolean isEmpty() {
return map.isEmpty();
}
public boolean containsKey(Object key) {
return map.containsKey(key);
}
public boolean containsValue(Object value) {
return map.containsValue(value);
}
public V put(K key, V value) {
Pair<Date, V> old = map.put(key, new Pair<>(new Date(), value));
if (old != null) {
return old.second;
}
return null;
}
public V remove(Object key) {
Pair<Date, V> old = map.remove(key);
if (old != null) {
return old.second;
}
return null;
}
public void putAll(Map<? extends K, ? extends V> m) {
for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
put(e.getKey(), e.getValue());
}
}
public void clear() {
map.clear();
}
public Set<K> keySet() {
return map.keySet();
}
public Collection<V> values() {
List<V> list = new ArrayList<>();
for (Pair<Date, V> e : map.values()) {
list.add(e.getSecond());
}
return list;
}
public Set<Entry<K, V>> entrySet() {
Set<Entry<K, V>> set = new HashSet<>();
for (final Map.Entry<K, Pair<Date, V>> e : map.entrySet()) {
set.add(new Map.Entry<K, V>() {
public K getKey() {
return e.getKey();
}
public V getValue() {
return e.getValue().getSecond();
}
public V setValue(Object value) {
return null;
}
});
}
return set;
}
/**
* This method is simple get without any loading behavior
*
* @param key to fetch
*/
public V get(Object key) {
Pair<Date, V> item = this.map.get(key);
if (item == null) {
return null;
}
return item.getSecond();
}
/**
* This method is simple get with loading behavior
*
* @param key to fetch
*/
public V get(K key, Cacheable cacheable, CacheLoader<K, V> loader) {
Pair<Date, V > item = this.map.get(key);
V value;
ReentrantLock lock = null;
try {
synchronized (this) {
if (cacheable.synchronizeAccess()) {
lock = lock(key);
}
}
if (item == null) {
// load initial value
value = reloadSynchronously(key, loader);
} else if (cacheable.timeoutInSecs() > 0
&& System.currentTimeMillis() - item.getFirst().getTime() > cacheable.timeoutInSecs() * 1000L) {
// the element has expired, reload it
if (cacheable.canReloadAsynchronously()) {
reloadAsynchronously(key, loader);
value = item.getSecond();
} else {
value = reloadSynchronously(key, loader);
}
} else {
value = item.getSecond();
}
} finally {
if (lock != null) {
lock.unlock();
}
}
return value;
}
private ReentrantLock lock(Object key) {
ReentrantLock lock;
synchronized (locks) {
lock = locks.get(key);
if (lock == null) lock = new ReentrantLock();
}
lock.lock();
return lock;
}
private V reloadSynchronously(final K key, final CacheLoader<K, V> loader) {
V value = loader.loadCache(key);
put(key, value);
return value;
}
private void reloadAsynchronously(final K key, final CacheLoader< K, V> loader) {
executorService.submit(new Callable<V>() {
public V call() {
return reloadSynchronously(key, loader);
}
});
}
}
A couple of interfaces to load or search key/values:
Java
import java.lang.reflect.*;
public interface CacheKeyable<K> {
K toKey(Method method, Object[] args);
}
Java
public interface CacheLoader<K, V> {
/**
* This mehod loads value for the object by calling underlying
* method
* @param key - key of the cache
* @return value for the key
*/
public V loadCache(K key);
}
Following is a simple class to convert method and its args into a unique key (I suppose we can use MD5 SHA1 instead here):
Java
public class SimpleKeyable implements CacheKeyable<String> {
public String toKey(Method method, Object[] args) {
StringBuilder sb = new StringBuilder(method.getName());
for (int i = 0; args != null && i < args.length; i++) {
sb.append(".");
if (isPrimitive(args[i])) {
sb.append(args[i].toString());
} else {
sb.append(System.identityHashCode(args[i]));
}
}
return sb.toString();
}
// checks if object is primitive type
private boolean isPrimitive(Object arg) {
return arg.getClass().isPrimitive() ||
arg.getClass().isEnum() ||
arg instanceof Number ||
arg instanceof CharSequence ||
arg instanceof Character ||
arg instanceof Boolean ||
arg instanceof Date;
}
}
A utility class for storing pair of objects:
Java
class Pair<FIRST, SECOND> {
final FIRST first;
final SECOND second;
Pair(FIRST first, SECOND second) {
this.first = first;
this.second = second;
}
public FIRST getFirst() {
return first;
}
public SECOND getSecond() {
return second;
}
}
Following code adds support for parsing cache annotation:
Java
import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class CacheProxy<K, V> implements InvocationHandler {
private final Object delegate;
private final CacheMap<K, V> cache;
private final Cacheable classcacheable;
private final CacheKeyable<String> keyable;
class CacheLoaderImpl implements CacheLoader<K, V> {
Method method;
Object[] args;
CacheLoaderImpl(Method method, Object[] args) {
this.method = method;
this.args = args;
}
public V loadCache(Object key) {
try {
return (V) method.invoke(delegate, args);
} catch (RuntimeException e) {
throw e;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
// Constructs cache proxy
public CacheProxy(Object aDelegate) {
this.delegate = aDelegate;
this.classcacheable = getCacheable(aDelegate.getClass().getAnnotations());
this.cache = new CacheMap<>(classcacheable);
this.keyable = new SimpleKeyable();
}
// creates proxy object for cache
public static Object proxy(Object delegate, Class<?> iface) {
return Proxy.newProxyInstance(iface.getClassLoader(),
new Class<?>[]{iface}, new CacheProxy(delegate));
}
public V invoke(
final Object proxy,
final Method method,
final Object[] args) throws Exception {
Cacheable cacheable = getCacheable(method);
if (cacheable != null) {
K key = toKey(method, args);
return cache.get(key, cacheable, new CacheLoaderImpl(method, args));
} else {
return (V) method.invoke(delegate, args);
}
}
private Cacheable getCacheable(Annotation[] ann) {
for (int i = 0; ann != null && i < ann.length; i++) {
if (ann[i] instanceof Cacheable) {
return (Cacheable) ann[i];
}
}
return null;
}
//
private Cacheable getCacheable(Method method) throws
Exception {
// get method from actual class because interface method will not be annotated.
method = delegate.getClass().getMethod(method.getName(), method.getParameterTypes());
// first search method level annotation
Cacheable cacheable = getCacheable(method.getDeclaredAnnotations());
if (cacheable == null) {
cacheable = classcacheable; // if not found search class level annotation
}
if (cacheable != null && !cacheable.cache()) {
cacheable = null;
}
return cacheable;
}
private K toKey(Method method, Object[] args) {
if (delegate instanceof CacheKeyable) {
return (K) ((CacheKeyable) delegate).toKey(method, args);
} else {
return (K) keyable.toKey(method, args);
}
}
}
Instead of Java 1.3 proxy, we can also use aspects, e.g.
Java
import java.util.*;
import java.lang.reflect.*;
import java.lang.annotation.*;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.reflect.MethodSignature;
public class CacheAspect<K, V> {
private final CacheMap<K, V> cache;
private final CacheKeyable<String> keyable;
class CacheLoaderImpl implements CacheLoader<K, V> {
ProceedingJoinPoint pjp;
CacheLoaderImpl(ProceedingJoinPoint pjp) {
this.pjp = pjp;
}
public V loadCache(Object key) {
try {
return pjp.proceed();
} catch (RuntimeException e) {
throw e;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
// Constructor
public CacheAspect() {
this.cache = new CacheMap<>(null);
this.keyable = new SimpleKeyable();
}
"@target(com.plexobject.Cacheable)") (
public Object proxy(ProceedingJoinPoint pjp) throws Exception {
Object target = pjp.getTarget();
Cacheable cacheable = getCacheable(pjp);
//
if (cacheable != null) {
Method method = ((MethodSignature)pjp.getStaticPart().getSignature()).getMethod();
Object key = toKey(target, method, pjp.getArgs());
return cache.get(key, cacheable, new CacheLoaderImpl(pjp)); 51
} else {
try {
return pjp.proceed();
} catch (Exception e) {
throw e;
} catch (Throwable e) {
throw new Error(e);
}
}
}
private Cacheable getCacheable(Annotation[] ann) {
for (int i=0; ann != null && i<ann.length; i++) {
if (ann[i] instanceof Cacheable) {
return (Cacheable) ann[i];
}
}
return null;
}
private Cacheable getCacheable(ProceedingJoinPoint pjp) throws Exception {
if (pjp.getStaticPart().getSignature() instanceof MethodSignature == false) {
return null;
}
Method method = ((MethodSignature)pjp.getStaticPart().getSignature()).getMethod();
// first search method level annotation
Cacheable cacheable = getCacheable(method.getDeclaredAnnotations());
if (cacheable != null && cacheable.cache() == false) {
cacheable = null;
}
return cacheable;
}
private K toKey(Object target, Method method, Object[] args) {
if (target instanceof CacheKeyable) {
return (K) ((CacheKeyable) target).toKey(method, args);
} else {
return (K) keyable.toKey(method, args);
}
}
}
And finally a unit test to test everything:
Java
import junit.framework.*;
import java.lang.reflect.*;
import java.util.*;
public class CacheProxyTest extends TestCase {
interface IService {
Date getCacheTime(boolean junk);
Date getCacheTime(int junk);
Date getRealtime(boolean junk);
}
maxCapacity=100) (
class ServiceImpl implements IService {
timeoutInSecs = 1, synchronizeAccess = true) (
public Date getCacheTime(boolean junk) {
return new Date();
}
timeoutInSecs = 1, synchronizeAccess = true) (
public Date getCacheTime(int junk) {
return new Date();
}
cache=false) (
public Date getRealtime(boolean junk) {
return new Date();
}
}
public void testCache() throws Exception {
ServiceImpl real = new ServiceImpl();
Date started = new Date();
IService svc = (IService) CacheProxy.proxy(real, IService.class);
Date cacheDate = svc.getCacheTime(true);
assertTrue(cacheDate.getTime() >= started.getTime());
Date realDate = svc.getRealtime(true);
assertTrue(realDate.getTime() >= started.getTime());
Thread.currentThread().sleep(200);
assertTrue(cacheDate.getTime() == svc.getCacheTime(true).getTime());
assertTrue(svc.getCacheTime(false).getTime() > cacheDate.getTime());
assertTrue(svc.getRealtime(true).getTime() > realDate.getTime());
cacheDate = svc.getCacheTime(0);
for (int i=0; i<100; i++) {
svc.getCacheTime(i).getTime();
}
// after max capacity
assertFalse(cacheDate == svc.getCacheTime(0));
}
}