org.snapscript.dx.stock.ProxyBuilder Maven / Gradle / Ivy
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.snapscript.dx.stock;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import static java.lang.reflect.Modifier.PRIVATE;
import static java.lang.reflect.Modifier.PUBLIC;
import static java.lang.reflect.Modifier.STATIC;
import java.lang.reflect.UndeclaredThrowableException;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import org.snapscript.dx.Code;
import org.snapscript.dx.Comparison;
import org.snapscript.dx.DexMaker;
import org.snapscript.dx.FieldId;
import org.snapscript.dx.Label;
import org.snapscript.dx.Local;
import org.snapscript.dx.MethodId;
import org.snapscript.dx.TypeId;
/**
* Creates dynamic proxies of concrete classes.
*
* This is similar to the {@code java.lang.reflect.Proxy} class, but works for classes instead of
* interfaces.
*
Example
* The following example demonstrates the creation of a dynamic proxy for {@code java.util.Random}
* which will always return 4 when asked for integers, and which logs method calls to every method.
*
* InvocationHandler handler = new InvocationHandler() {
* @Override
* public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
* if (method.getName().equals("nextInt")) {
* // Chosen by fair dice roll, guaranteed to be random.
* return 4;
* }
* Object result = ProxyBuilder.callSuper(proxy, method, args);
* System.out.println("Method: " + method.getName() + " args: "
* + Arrays.toString(args) + " result: " + result);
* return result;
* }
* };
* Random debugRandom = ProxyBuilder.forClass(Random.class)
* .dexCache(getInstrumentation().getTargetContext().getDir("dx", Context.MODE_PRIVATE))
* .handler(handler)
* .build();
* assertEquals(4, debugRandom.nextInt());
* debugRandom.setSeed(0);
* assertTrue(debugRandom.nextBoolean());
*
* Usage
* Call {@link #forClass(Class)} for the Class you wish to proxy. Call
* {@link #handler(InvocationHandler)} passing in an {@link InvocationHandler}, and then call
* {@link #build()}. The returned instance will be a dynamically generated subclass where all method
* calls will be delegated to the invocation handler, except as noted below.
*
* The static method {@link #callSuper(Object, Method, Object...)} allows you to access the original
* super method for a given proxy. This allows the invocation handler to selectively override some
* methods but not others.
*
* By default, the {@link #build()} method will call the no-arg constructor belonging to the class
* being proxied. If you wish to call a different constructor, you must provide arguments for both
* {@link #constructorArgTypes(Class[])} and {@link #constructorArgValues(Object[])}.
*
* This process works only for classes with public and protected level of visibility.
*
* You may proxy abstract classes. You may not proxy final classes.
*
* Only non-private, non-final, non-static methods will be dispatched to the invocation handler.
* Private, static or final methods will always call through to the superclass as normal.
*
* The {@link #finalize()} method on {@code Object} will not be proxied.
*
* You must provide a dex cache directory via the {@link #dexCache(File)} method. You should take
* care not to make this a world-writable directory, so that third parties cannot inject code into
* your application. A suitable parameter for these output directories would be something like
* this:
*
{@code
* getApplicationContext().getDir("dx", Context.MODE_PRIVATE);
* }
*
* If the base class to be proxied leaks the {@code this} pointer in the constructor (bad practice),
* that is to say calls a non-private non-final method from the constructor, the invocation handler
* will not be invoked. As a simple concrete example, when proxying Random we discover that it
* inernally calls setSeed during the constructor. The proxy will not intercept this call during
* proxy construction, but will intercept as normal afterwards. This behaviour may be subject to
* change in future releases.
*
* This class is not thread safe.
*/
public final class ProxyBuilder {
// Version of ProxyBuilder. It should be updated if the implementation
// of the generated proxy class changes.
public static final int VERSION = 1;
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>> generatedProxyClasses
= Collections.synchronizedMap(new HashMap, Class>>());
private final Class baseClass;
private ClassLoader parentClassLoader = ProxyBuilder.class.getClassLoader();
private InvocationHandler handler;
private File dexCache;
private Class>[] constructorArgTypes = new Class[0];
private Object[] constructorArgValues = new Object[0];
private Set> interfaces = new HashSet>();
private ProxyBuilder(Class clazz) {
baseClass = clazz;
}
public static ProxyBuilder forClass(Class clazz) {
return new ProxyBuilder(clazz);
}
/**
* Specifies the parent ClassLoader to use when creating the proxy.
*
* If null, {@code ProxyBuilder.class.getClassLoader()} will be used.
*/
public ProxyBuilder parentClassLoader(ClassLoader parent) {
parentClassLoader = parent;
return this;
}
public ProxyBuilder handler(InvocationHandler handler) {
this.handler = handler;
return this;
}
/**
* Sets the directory where executable code is stored. See {@link
* DexMaker#generateAndLoad DexMaker.generateAndLoad()} for guidance on
* choosing a secure location for the dex cache.
*/
public ProxyBuilder dexCache(File dexCacheParent) {
dexCache = new File(dexCacheParent, "v" + Integer.toString(VERSION));
dexCache.mkdir();
return this;
}
public ProxyBuilder implementing(Class>... interfaces) {
for (Class> i : interfaces) {
if (!i.isInterface()) {
throw new IllegalArgumentException("Not an interface: " + i.getName());
}
this.interfaces.add(i);
}
return this;
}
public ProxyBuilder constructorArgValues(Object... constructorArgValues) {
this.constructorArgValues = constructorArgValues;
return this;
}
public ProxyBuilder constructorArgTypes(Class>... constructorArgTypes) {
this.constructorArgTypes = constructorArgTypes;
return this;
}
/**
* Create a new instance of the class to proxy.
*
* @throws UnsupportedOperationException if the class we are trying to create a proxy for is
* not accessible.
* @throws IOException if an exception occurred writing to the {@code dexCache} directory.
* @throws UndeclaredThrowableException if the constructor for the base class to proxy throws
* a declared exception during construction.
* @throws IllegalArgumentException if the handler is null, if the constructor argument types
* do not match the constructor argument values, or if no such constructor exists.
*/
public T build() throws IOException {
check(handler != null, "handler == null");
check(constructorArgTypes.length == constructorArgValues.length,
"constructorArgValues.length != constructorArgTypes.length");
Class extends T> proxyClass = buildProxyClass();
Constructor extends T> constructor;
try {
constructor = proxyClass.getConstructor(constructorArgTypes);
} catch (NoSuchMethodException e) {
throw new IllegalArgumentException("No constructor for " + baseClass.getName()
+ " with parameter types " + Arrays.toString(constructorArgTypes));
}
T result;
try {
result = constructor.newInstance(constructorArgValues);
} catch (InstantiationException e) {
// Should not be thrown, generated class is not abstract.
throw new AssertionError(e);
} catch (IllegalAccessException e) {
// Should not be thrown, the generated constructor is accessible.
throw new AssertionError(e);
} catch (InvocationTargetException e) {
// Thrown when the base class constructor throws an exception.
throw launderCause(e);
}
setInvocationHandler(result, handler);
return result;
}
// TODO: test coverage for this
/**
* Generate a proxy class. Note that new instances of this class will not automatically have an
* an invocation handler, even if {@link #handler(InvocationHandler)} was called. The handler
* must be set on each instance after it is created, using
* {@link #setInvocationHandler(Object, InvocationHandler)}.
*/
public Class extends T> buildProxyClass() 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
&& interfaces.equals(asSet(proxyClass.getInterfaces()))) {
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 superType = TypeId.get(baseClass);
generateConstructorsAndFields(dexMaker, generatedType, superType, baseClass);
Method[] methodsToProxy = getMethodsToProxyRecursive();
generateCodeForAllMethods(dexMaker, generatedType, methodsToProxy, superType);
dexMaker.declare(generatedType, generatedName + ".generated", PUBLIC, superType,
getInterfacesAsTypeIds());
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 class " + baseClass, 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)
throws ClassNotFoundException {
return (Class extends T>) classLoader.loadClass(generatedName);
}
private static RuntimeException launderCause(InvocationTargetException e) {
Throwable cause = e.getCause();
// Errors should be thrown as they are.
if (cause instanceof Error) {
throw (Error) cause;
}
// RuntimeException can be thrown as-is.
if (cause instanceof RuntimeException) {
throw (RuntimeException) cause;
}
// Declared exceptions will have to be wrapped.
throw new UndeclaredThrowableException(cause);
}
private static void setMethodsStaticField(Class> proxyClass, Method[] methodsToProxy) {
try {
Field methodArrayField = proxyClass.getDeclaredField(FIELD_NAME_METHODS);
methodArrayField.setAccessible(true);
methodArrayField.set(null, methodsToProxy);
} catch (NoSuchFieldException e) {
// Should not be thrown, generated proxy class has been generated with this field.
throw new AssertionError(e);
} catch (IllegalAccessException e) {
// Should not be thrown, we just set the field to accessible.
throw new AssertionError(e);
}
}
/**
* Returns the proxy's {@link InvocationHandler}.
*
* @throws IllegalArgumentException if the object supplied is not a proxy created by this class.
*/
public static InvocationHandler getInvocationHandler(Object instance) {
try {
Field field = instance.getClass().getDeclaredField(FIELD_NAME_HANDLER);
field.setAccessible(true);
return (InvocationHandler) field.get(instance);
} catch (NoSuchFieldException e) {
throw new IllegalArgumentException("Not a valid proxy instance", e);
} catch (IllegalAccessException e) {
// Should not be thrown, we just set the field to accessible.
throw new AssertionError(e);
}
}
/**
* Sets the proxy's {@link InvocationHandler}.
*
* If you create a proxy with {@link #build()}, the proxy will already have a handler set,
* provided that you configured one with {@link #handler(InvocationHandler)}.
*
* If you generate a proxy class with {@link #buildProxyClass()}, instances of the proxy class
* will not automatically have a handler set, and it is necessary to use this method with each
* instance.
*
* @throws IllegalArgumentException if the object supplied is not a proxy created by this class.
*/
public static void setInvocationHandler(Object instance, InvocationHandler handler) {
try {
Field handlerField = instance.getClass().getDeclaredField(FIELD_NAME_HANDLER);
handlerField.setAccessible(true);
handlerField.set(instance, handler);
} catch (NoSuchFieldException e) {
throw new IllegalArgumentException("Not a valid proxy instance", e);
} catch (IllegalAccessException e) {
// Should not be thrown, we just set the field to accessible.
throw new AssertionError(e);
}
}
// TODO: test coverage for isProxyClass
/**
* Returns true if {@code c} is a proxy class created by this builder.
*/
public static boolean isProxyClass(Class> c) {
// TODO: use a marker interface instead?
try {
c.getDeclaredField(FIELD_NAME_HANDLER);
return true;
} catch (NoSuchFieldException e) {
return false;
}
}
private static void generateCodeForAllMethods(DexMaker dexMaker,
TypeId generatedType, Method[] methodsToProxy, TypeId superclassType) {
TypeId handlerType = TypeId.get(InvocationHandler.class);
TypeId methodArrayType = TypeId.get(Method[].class);
FieldId handlerField =
generatedType.getField(handlerType, FIELD_NAME_HANDLER);
FieldId allMethods =
generatedType.getField(methodArrayType, FIELD_NAME_METHODS);
TypeId methodType = TypeId.get(Method.class);
TypeId