Shahzad Bhatti Welcome to my ramblings and rants!

August 7, 2007

Annotation based Caching in Java

Filed under: Computing — admin @ 3:33 pm

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:

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.
 */

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(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:

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;
        }

        @Override
        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));
    }

    @Override
    public int size() {
        return map.size();
    }

    @Override
    public boolean isEmpty() {
        return map.isEmpty();
    }

    @Override
    public boolean containsKey(Object key) {
        return map.containsKey(key);
    }

    @Override
    public boolean containsValue(Object value) {
        return map.containsValue(value);
    }

    @Override
    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;
    }

    @Override
    public V remove(Object key) {
        Pair<Date, V> old = map.remove(key);
        if (old != null) {
            return old.second;
        }
        return null;
    }

    @Override
    public void putAll(Map<? extends K, ? extends V> m) {
        for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
            put(e.getKey(), e.getValue());
        }
    }

    @Override
    public void clear() {
        map.clear();
    }

    @Override
    public Set<K> keySet() {
        return map.keySet();
    }

    @Override
    public Collection<V> values() {
        List<V> list = new ArrayList<>();
        for (Pair<Date, V> e : map.values()) {
            list.add(e.getSecond());
        }
        return list;
    }

    @Override
    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>() {
                @Override
                public K getKey() {
                    return e.getKey();
                }

                @Override
                public V getValue() {
                    return e.getValue().getSecond();
                }

                @Override
                public V setValue(Object value) {
                    return null;
                }
            });
        }
        return set;
    }

    /**
     * This method is simple get without any loading behavior
     *
     * @param key to fetch
     */
    @Override
    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:

import java.lang.reflect.*;
public interface CacheKeyable<K> {
    K toKey(Method method, Object[] args);
}
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):

public class SimpleKeyable implements CacheKeyable<String> {
    @Override
    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:

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:

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;
        }

        @Override
        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.

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;

@Aspect
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;
        }
        @Override
        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();
    }

    @Around("@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:

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);
    }
    @Cacheable(maxCapacity=100)
    class ServiceImpl implements IService {
        @Cacheable(timeoutInSecs = 1, synchronizeAccess = true)
        public Date getCacheTime(boolean junk) {
            return new Date();
        }
        @Cacheable(timeoutInSecs = 1, synchronizeAccess = true)
        public Date getCacheTime(int junk) {
            return new Date();
        }
        @Cacheable(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));
    }
}

No Comments

No comments yet.

RSS feed for comments on this post. TrackBack URL

Sorry, the comment form is closed at this time.

Powered by WordPress