Java: Agnostic Cache with Dynamic Proxies and Reflection

Hace tiempo usé un modulo de Perl muy interesante, Memoize. La idea era muy sencilla, tienes una funcion X, si para una entrada A, hay una salida B constantes y ese cálculo es lento, puede usar un caché.

La idea es esa, calcular una vez, usar muchas; sin tener que tener una implementación con cache de cada implementación original.

Aquí está la implementación en Java usando Reflexión:

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.Map;

public class ReflectMemoizer implements InvocationHandler {
  private final Object object;
  private final HashMap<Method, ConcurrentHashMap<List<Object>, Object>> caches;

  /**
   * Memoize object
   * @param object source
   * @return proxied object
   */
  public static Object memoize(final Object object) 
        throws InstantiationException, IllegalAccessException {
    final Class<?> clazz = object.getClass();
    final ReflectMemoizer memoizer = new ReflectMemoizer(object);
    return Proxy.newProxyInstance(clazz.getClassLoader(), 
        clazz.getInterfaces(), memoizer);
  }

  private ReflectMemoizer(final Object object) {
    this.object = object;
    this.caches = new HashMap<Method, ConcurrentHashMap<List<Object>, Object>>();
  }

  public Object invoke(final Object proxy, final Method method, 
        final Object[] args) throws Throwable {
    if (method.getReturnType().equals(Void.TYPE)) {
      // Don't cache void methods
      return invoke(method, args);
    } else {
      final Map<List<Object>, Object> cache = getCache(method);
      final List<Object> key = Arrays.asList(args);
      Object value = cache.get(key);
      if ((value == null) && !cache.containsKey(key)) {
        value = invoke(method, args);
        cache.put(key, value);
      }
      return value;
    }
  }

  private synchronized Map<List<Object>, Object> getCache(final Method m) {
    ConcurrentHashMap<List<Object>, Object> cache = caches.get(m);
    if (cache == null) {
      cache = new ConcurrentHashMap<List<Object>, Object>();
      caches.put(m, cache);
    }
    return cache;
  }

  private Object invoke(final Method method, final Object[] args) 
        throws Throwable {
    try {
      return method.invoke(object, args);
    } catch (InvocationTargetException e) {
      throw e.getTargetException();
    }
  }
}

Y aquí el código de prueba:

import java.nio.charset.Charset;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import javax.xml.bind.DatatypeConverter;

public class TestMemoizer {
  /**
   * Sample Interface to Memoize
   */
  public static interface SampleInterface {
    public String doTest(final String in);
  }

  /**
   * Sample Slow Implementation (MessageDigest with SHA-512)
   */
  public static class SampleSlowImpl implements SampleInterface {
    private static final Charset UTF8 = Charset.forName("UTF-8");
    private final MessageDigest md;

    public SampleSlowImpl() throws NoSuchAlgorithmException {
      md = MessageDigest.getInstance("SHA-512");
    }

    public synchronized String doTest(final String in) {
      md.reset();
      final byte[] buf = md.digest(in.getBytes(UTF8));
      return DatatypeConverter.printBase64Binary(buf);
    }
  }

  /**
   * Sample Fast Implementation (dummy)
   */
  public static class SampleFastImpl implements SampleInterface {
    public String doTest(final String in) {
      return in;
    }
  }

  private static final String getHeader(final Class<?> b1, //
      final Class<?> b2) {
    final String s1 = b1.getSimpleName();
    final String s2 = b2.getSimpleName();
    if (s1.equals(s2))
      return s1 + ":direct";
    return s1 + ":memoize";
  }

  public static void main(final String[] args) throws Throwable {
    final int TOTAL = (int) 1e6;
    final String TEST_TEXT = "hello world";
    final SampleInterface[] samples = new SampleInterface[] {
        new SampleSlowImpl(), //
        (SampleInterface) ReflectMemoizer.memoize(new SampleSlowImpl()), //
        new SampleFastImpl(), //
        (SampleInterface) ReflectMemoizer.memoize(new SampleFastImpl())
    };
    //
    long ts, diff;
    for (int k = 0; k < samples.length; k++) {
      final SampleInterface b = samples[k & ~1];
      final SampleInterface t = samples[k];
      final String test = getHeader(b.getClass(), t.getClass());
      ts = System.currentTimeMillis();
      for (int i = 0; i < TOTAL; i++) {
        t.doTest(TEST_TEXT);
      }
      diff = System.currentTimeMillis() - ts;
      System.out.println(test + "\t" + "diff=" + diff + "ms" + "\t" + //
          t.doTest(TEST_TEXT));
    }
  }
}

El resultado de las pruebas:

MemoizeSlowImpl:direct  diff=5823ms MJ7MSJwS1utMxA9Qy...==
MemoizeSlowImpl:memoize diff=198ms  MJ7MSJwS1utMxA9Qy...==
MemoizeFastImpl:direct  diff=6ms    hello world
MemoizeFastImpl:memoize diff=194ms  hello world

Notas importantes:

  • Esta implementación de ejemplo (los HashMaps) no está limitada, el OutOfMemory es cuestión de tiempo.
  • Los métodos que retornan void no son cacheados.
  • Si el tiempo de ejecución de la funcion original es realmente rápido, la reflexión será más lenta; por tanto, mejor medir los tiempos.

Sourcecode on GitHub:
Memoizer.java

Referencias:
java.lang.reflect.Proxy
java.lang.reflect.Method
java.lang.reflect.InvocationHandler

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s

A %d blogueros les gusta esto: