Implement a cache for proxy classes.
diff --git a/src/main/java/com/google/dexmaker/stock/ProxyBuilder.java b/src/main/java/com/google/dexmaker/stock/ProxyBuilder.java
index 2b40160..f5b9c39 100644
--- a/src/main/java/com/google/dexmaker/stock/ProxyBuilder.java
+++ b/src/main/java/com/google/dexmaker/stock/ProxyBuilder.java
@@ -37,6 +37,7 @@
import static java.lang.reflect.Modifier.STATIC;
import java.lang.reflect.UndeclaredThrowableException;
import java.util.Arrays;
+import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
@@ -116,6 +117,14 @@
private static final String FIELD_NAME_HANDLER = "$__handler";
private static final String FIELD_NAME_METHODS = "$__methodArray";
+ /**
+ * A cache of all proxy classes ever generated. At the time of writing,
+ * Android's runtime doesn't support class unloading so there's little
+ * value in using weak references.
+ */
+ private static final Map<Class<?>, Class<?>> generatedProxyClasses
+ = Collections.synchronizedMap(new HashMap<Class<?>, Class<?>>());
+
private final Class<T> baseClass;
// TODO: make DexMaker do the defaulting here
private ClassLoader parentClassLoader = ProxyBuilder.class.getClassLoader();
@@ -177,31 +186,12 @@
check(handler != null, "handler == null");
check(constructorArgTypes.length == constructorArgValues.length,
"constructorArgValues.length != constructorArgTypes.length");
- DexMaker dexMaker = new DexMaker();
- String generatedName = getMethodNameForProxyOf(baseClass);
- TypeId<? extends T> generatedType = TypeId.get("L" + generatedName + ";");
- TypeId<T> superType = TypeId.get(baseClass);
- generateConstructorsAndFields(dexMaker, generatedType, superType, baseClass);
- Method[] methodsToProxy = getMethodsToProxy(baseClass);
- generateCodeForAllMethods(dexMaker, generatedType, methodsToProxy, superType);
- dexMaker.declare(generatedType, generatedName + ".generated", PUBLIC, superType);
- ClassLoader classLoader = dexMaker.generateAndLoad(parentClassLoader, dexCache);
- Class<? extends T> proxyClass;
- try {
- proxyClass = loadClass(classLoader, generatedName);
- } catch (IllegalAccessError e) {
- // Thrown when the base class is not accessible.
- throw new UnsupportedOperationException("cannot proxy inaccessible classes", e);
- } catch (ClassNotFoundException e) {
- // Should not be thrown, we're sure to have generated this class.
- throw new AssertionError(e);
- }
- setMethodsStaticField(proxyClass, methodsToProxy);
+ Class<? extends T> proxyClass = getProxyClass();
Constructor<? extends T> constructor;
try {
constructor = proxyClass.getConstructor(constructorArgTypes);
} catch (NoSuchMethodException e) {
- // Thrown when the ctor to be called does not exist.
+ // Thrown when the constructor to be called does not exist.
throw new IllegalArgumentException("could not find matching constructor", e);
}
T result;
@@ -214,13 +204,45 @@
// Should not be thrown, the generated constructor is accessible.
throw new AssertionError(e);
} catch (InvocationTargetException e) {
- // Thrown when the base class ctor throws an exception.
+ // Thrown when the base class constructor throws an exception.
throw launderCause(e);
}
setHandlerInstanceField(result, handler);
return result;
}
+ private Class<? extends T> getProxyClass() throws IOException {
+ // try the cache to see if we've generated this one before
+ @SuppressWarnings("unchecked") // we only populate the map with matching types
+ Class<? extends T> proxyClass = (Class) generatedProxyClasses.get(baseClass);
+ if (proxyClass != null && proxyClass.getClassLoader().getParent() == parentClassLoader) {
+ return proxyClass; // cache hit!
+ }
+
+ // the cache missed; generate the class
+ DexMaker dexMaker = new DexMaker();
+ String generatedName = getMethodNameForProxyOf(baseClass);
+ TypeId<? extends T> generatedType = TypeId.get("L" + generatedName + ";");
+ TypeId<T> superType = TypeId.get(baseClass);
+ generateConstructorsAndFields(dexMaker, generatedType, superType, baseClass);
+ Method[] methodsToProxy = getMethodsToProxy(baseClass);
+ generateCodeForAllMethods(dexMaker, generatedType, methodsToProxy, superType);
+ dexMaker.declare(generatedType, generatedName + ".generated", PUBLIC, superType);
+ ClassLoader classLoader = dexMaker.generateAndLoad(parentClassLoader, dexCache);
+ try {
+ proxyClass = loadClass(classLoader, generatedName);
+ } catch (IllegalAccessError e) {
+ // Thrown when the base class is not accessible.
+ throw new UnsupportedOperationException("cannot proxy inaccessible classes", e);
+ } catch (ClassNotFoundException e) {
+ // Should not be thrown, we're sure to have generated this class.
+ throw new AssertionError(e);
+ }
+ setMethodsStaticField(proxyClass, methodsToProxy);
+ generatedProxyClasses.put(baseClass, proxyClass);
+ return proxyClass;
+ }
+
// The type cast is safe: the generated type will extend the base class type.
@SuppressWarnings("unchecked")
private Class<? extends T> loadClass(ClassLoader classLoader, String generatedName)
diff --git a/src/test/java/com/google/dexmaker/stock/ProxyBuilderTest.java b/src/test/java/com/google/dexmaker/stock/ProxyBuilderTest.java
index 84937ae..ce2f9e4 100644
--- a/src/test/java/com/google/dexmaker/stock/ProxyBuilderTest.java
+++ b/src/test/java/com/google/dexmaker/stock/ProxyBuilderTest.java
@@ -398,12 +398,17 @@
public void testIllegalCacheDirectory() throws Exception {
try {
- proxyFor(Object.class).dexCache(new File("//////")).build();
+ proxyFor(ProxyForIllegalCacheDirectory.class)
+ .dexCache(new File("/poop/"))
+ .build();
fail();
} catch (IOException expected) {
}
}
+ public static class ProxyForIllegalCacheDirectory {
+ }
+
public void testInvalidConstructorSpecification() throws Exception {
try {
proxyFor(Object.class)
@@ -501,9 +506,18 @@
} catch (ClassCastException expected) {}
}
- public void testCaching_ShouldWork() {
- // TODO: We're not supporting caching yet. But we should as soon as possible.
- fail();
+ public void testCaching() throws Exception {
+ SimpleClass proxy1 = proxyFor(SimpleClass.class).build();
+ SimpleClass proxy2 = proxyFor(SimpleClass.class).build();
+ assertSame(proxy1.getClass(), proxy2.getClass());
+ }
+
+ public void testCachingWithMultipleConstructors() throws Exception {
+ fail("TODO");
+ }
+
+ public void testCachingWithDifferentParentClassLoaders() throws Exception {
+ fail("TODO");
}
public void testSubclassOfRandom() throws Exception {