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 static java.lang.reflect.Modifier.PRIVATE;
import static java.lang.reflect.Modifier.PUBLIC;
import static java.lang.reflect.Modifier.STATIC;
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 java.lang.reflect.UndeclaredThrowableException;
import java.security.MessageDigest;
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 static final Map,ClassLoader> generatedProxyClassesClassLoaders
= Collections.synchronizedMap(new HashMap, ClassLoader>());
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 Set> beanInterfaces = 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 implementingBeans(Class>... beanInterfaces) {
for (Class> i : beanInterfaces) {
if (!i.isInterface()) {
throw new IllegalArgumentException("Not an interface: " + i.getName());
}
this.beanInterfaces.add(i);
}
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);
}
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);
Set> interfaces = determineImplementInterfaces();
if (proxyClass != null
&& proxyClass.getClassLoader().getParent() == parentClassLoader
&& interfaces.equals(asSet(proxyClass.getInterfaces()))) {
return proxyClass; // cache hit!
}
Class bridgeClass = buildBridgeClass( baseClass);
ClassLoader cacheClassLoader = generatedProxyClassesClassLoaders.get(bridgeClass);
return buildProxyClass(bridgeClass, cacheClassLoader == null ? parentClassLoader : cacheClassLoader);
}
private Class extends T> buildProxyClass(Class baseClass, ClassLoader parentClassLoader) throws IOException {
DexMaker dexMaker = new DexMaker();
Class extends T> proxyClass = null;
// the cache missed; generate the class
String generatedName = getNameForProxyOf(baseClass);
TypeId extends T> generatedType = TypeId.get("L" + generatedName + ";");
TypeId superType = TypeId.get(baseClass);
generateConstructorsAndFields(dexMaker, generatedType, superType, baseClass);
Method[] methodsToProxy = getMethodsToProxyRecursive(interfaces);
generateCodeForAllMethods(dexMaker, generatedType, methodsToProxy, superType);
dexMaker.declare(generatedType, generatedName + ".generated", PUBLIC, superType,
getInterfacesAsTypeIds(interfaces));
ClassLoader classLoader = dexMaker.generateAndLoad(parentClassLoader, dexCache, generatedName);
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);
}
setInvocationHandler(proxyClass, handler);
setMethodsStaticField(proxyClass, methodsToProxy);
generatedProxyClasses.put(baseClass, proxyClass);
generatedProxyClassesClassLoaders.put(proxyClass, classLoader);
return proxyClass;
}
private Class buildBridgeClass(Class baseClass) throws IOException {
DexMaker dexMaker = new DexMaker();
// the cache missed; generate the class
Class extends T> bridgeClass = null;
String generatedName = getNameForBridgeOf(baseClass);
TypeId extends T> generatedType = TypeId.get("L" + generatedName + ";");
TypeId superType = TypeId.get(baseClass);
generateConstructorsForBridge(dexMaker, generatedType, superType, baseClass);
Method[] abstractMethods = getAbstractMethodsToProxyRecursive();
BeanProperty[] beanMethods = getBeanMethodsToProxyRecursive();
generateCodeForAbstractMethods(dexMaker, abstractMethods, generatedType);
generateCodeForBeanMethods(dexMaker, beanMethods, generatedType);
dexMaker.declare(generatedType, generatedName + ".generated", PUBLIC, superType,
getInterfacesAsTypeIds(beanInterfaces));
ClassLoader classLoader = dexMaker.generateAndLoad(parentClassLoader, dexCache, generatedName);
try {
bridgeClass = loadClass(classLoader, generatedName);
} catch (IllegalAccessError e) {
// Thrown when the base class is not accessible.WW
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);
}
generatedProxyClassesClassLoaders.put(bridgeClass, classLoader);
return bridgeClass;
}
// 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(Class> proxyClass) {
try {
Field field = proxyClass.getDeclaredField(FIELD_NAME_HANDLER);
field.setAccessible(true);
return (InvocationHandler) field.get(null);
} 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(Class> proxyClass, InvocationHandler handler) {
try {
Field handlerField = proxyClass.getDeclaredField(FIELD_NAME_HANDLER);
handlerField.setAccessible(true);
handlerField.set(null, handler);
} catch (NoSuchFieldException e) {
throw new IllegalArgumentException("Not a valid proxy class", 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 generateCodeForAbstractMethods(DexMaker dexMaker, Method[] methodsToImplement,
TypeId generatedType) {
for (int m = 0; m < methodsToImplement.length; ++m) {
Method method = methodsToImplement[m];
String name = method.getName();
Class>[] argClasses = method.getParameterTypes();
Class> returnType = method.getReturnType();
TypeId>[] argTypes = new TypeId>[argClasses.length];
for (int i = 0; i < argTypes.length; ++i) {
argTypes[i] = TypeId.get(argClasses[i]);
}
TypeId> resultType = TypeId.get(returnType);
MethodId implementMethod = generatedType.getMethod(resultType, name, argTypes);
Code code = dexMaker.declare(implementMethod, PUBLIC);
TypeId iseType = TypeId.get(UnsupportedOperationException.class);
MethodId iseConstructor = iseType
.getConstructor();
Local localIse = code.newLocal(iseType);
code.newInstance(localIse, iseConstructor);
code.throwValue(localIse);
}
}
private static void generateCodeForBeanMethods(DexMaker dexMaker, BeanProperty[] methodsToImplement,
TypeId generatedType) {
for (int m = 0; m < methodsToImplement.length; ++m) {
BeanProperty method = methodsToImplement[m];
String name = method.getName();
Class type = method.getType();
Method setter = method.getSetter();
Method getter = method.getGetter();
TypeId> propertyType = TypeId.get(type);
FieldId propertyField = generatedType.getField(
propertyType, "$__" + name);
dexMaker.declare(propertyField, PUBLIC, null);
generateGetterMethod(dexMaker, getter, name, propertyField, generatedType);
generateSetterMethod(dexMaker, setter, name, propertyField, generatedType);
}
}
private static void generateGetterMethod(DexMaker dexMaker, Method method, String property, FieldId propertyField, TypeId generatedType) {
String name = method.getName();
Class>[] argClasses = method.getParameterTypes();
Class> returnType = method.getReturnType();
TypeId>[] argTypes = new TypeId>[argClasses.length];
for (int i = 0; i < argTypes.length; ++i) {
argTypes[i] = TypeId.get(argClasses[i]);
}
TypeId> resultType = TypeId.get(returnType);
MethodId implementMethod = generatedType.getMethod(resultType, name, argTypes);
Code code = dexMaker.declare(implementMethod, PUBLIC);
// get property
Local localThis = code.getThis(generatedType);
Local> localValue = code.newLocal(resultType);
code.iget(propertyField, localValue, localThis);
code.returnValue(localValue);
}
private static void generateSetterMethod(DexMaker dexMaker, Method method, String property, FieldId propertyField, TypeId generatedType) {
String name = method.getName();
Class>[] argClasses = method.getParameterTypes();
Class> returnType = method.getReturnType();
TypeId>[] argTypes = new TypeId>[argClasses.length];
for (int i = 0; i < argTypes.length; ++i) {
argTypes[i] = TypeId.get(argClasses[i]);
}
TypeId> resultType = TypeId.get(returnType);
MethodId implementMethod = generatedType.getMethod(resultType, name, argTypes);
Code code = dexMaker.declare(implementMethod, PUBLIC);
// set property
Local localThis = code.getThis(generatedType);
Local> parameter = code.getParameter(0, argTypes[0]);
code.iput(propertyField, localThis, parameter);
code.returnVoid();
}
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