blob: 13638945469b0791e6e78bd741786d04e7bda7da [file] [log] [blame]
/*
* 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 com.android.dx.stock;
import com.android.dx.Code;
import com.android.dx.Comparison;
import com.android.dx.DexMaker;
import com.android.dx.FieldId;
import com.android.dx.Label;
import com.android.dx.Local;
import com.android.dx.MethodId;
import com.android.dx.TypeId;
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.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 static java.lang.reflect.Modifier.PRIVATE;
import static java.lang.reflect.Modifier.PUBLIC;
import static java.lang.reflect.Modifier.STATIC;
/**
* Creates dynamic proxies of concrete classes.
* <p>
* This is similar to the {@code java.lang.reflect.Proxy} class, but works for classes instead of
* interfaces.
* <h3>Example</h3>
* 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.
* <pre>
* InvocationHandler handler = new InvocationHandler() {
* &#64;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());
* </pre>
* <h3>Usage</h3>
* 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.
* <p>
* 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.
* <p>
* 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[])}.
* <p>
* This process works only for classes with public and protected level of visibility.
* <p>
* You may proxy abstract classes. You may not proxy final classes.
* <p>
* 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.
* <p>
* The {@link #finalize()} method on {@code Object} will not be proxied.
* <p>
* 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:
* <pre>{@code
* getApplicationContext().getDir("dx", Context.MODE_PRIVATE);
* }</pre>
* <p>
* 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
* internally 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.
* <p>
* This class is <b>not thread safe</b>.
*/
public final class ProxyBuilder<T> {
// 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<?>, Class<?>> generatedProxyClasses
= Collections.synchronizedMap(new HashMap<Class<?>, Class<?>>());
private final Class<T> 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<Class<?>> interfaces = new HashSet<>();
private Method[] methods;
private boolean sharedClassLoader;
private ProxyBuilder(Class<T> clazz) {
baseClass = clazz;
}
public static <T> ProxyBuilder<T> forClass(Class<T> clazz) {
return new ProxyBuilder<T>(clazz);
}
/**
* Specifies the parent ClassLoader to use when creating the proxy.
*
* <p>If null, {@code ProxyBuilder.class.getClassLoader()} will be used.
*/
public ProxyBuilder<T> parentClassLoader(ClassLoader parent) {
parentClassLoader = parent;
return this;
}
public ProxyBuilder<T> 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<T> dexCache(File dexCacheParent) {
dexCache = new File(dexCacheParent, "v" + Integer.toString(VERSION));
dexCache.mkdir();
return this;
}
public ProxyBuilder<T> 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<T> constructorArgValues(Object... constructorArgValues) {
this.constructorArgValues = constructorArgValues;
return this;
}
public ProxyBuilder<T> constructorArgTypes(Class<?>... constructorArgTypes) {
this.constructorArgTypes = constructorArgTypes;
return this;
}
public ProxyBuilder<T> onlyMethods(Method[] methods) {
this.methods = methods;
return this;
}
public ProxyBuilder<T> withSharedClassLoader() {
this.sharedClassLoader = true;
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
// we only populate the map with matching types
@SuppressWarnings("unchecked")
Class<? extends T> proxyClass = (Class) generatedProxyClasses.get(baseClass);
if (proxyClass != null) {
boolean validClassLoader;
if (sharedClassLoader) {
ClassLoader parent = parentClassLoader != null ? parentClassLoader : baseClass
.getClassLoader();
validClassLoader = proxyClass.getClassLoader() == parent;
} else {
validClassLoader = proxyClass.getClassLoader().getParent() == parentClassLoader;
}
if (validClassLoader && 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<T> superType = TypeId.get(baseClass);
generateConstructorsAndFields(dexMaker, generatedType, superType, baseClass);
Method[] methodsToProxy;
if (methods == null) {
methodsToProxy = getMethodsToProxyRecursive();
} else {
methodsToProxy = methods;
}
// Sort the results array so that they are in a deterministic fashion.
Arrays.sort(methodsToProxy, new Comparator<Method>() {
@Override
public int compare(Method method1, Method method2) {
return method1.toString().compareTo(method2.toString());
}
});
generateCodeForAllMethods(dexMaker, generatedType, methodsToProxy, superType);
dexMaker.declare(generatedType, generatedName + ".generated", PUBLIC, superType, getInterfacesAsTypeIds());
if (sharedClassLoader) {
dexMaker.setSharedClassLoader(baseClass.getClassLoader());
}
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}.
* <p>
* 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)}.
* <p>
* 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 <T, G extends T> void generateCodeForAllMethods(DexMaker dexMaker,
TypeId<G> generatedType, Method[] methodsToProxy, TypeId<T> superclassType) {
TypeId<InvocationHandler> handlerType = TypeId.get(InvocationHandler.class);
TypeId<Method[]> methodArrayType = TypeId.get(Method[].class);
FieldId<G, InvocationHandler> handlerField =
generatedType.getField(handlerType, FIELD_NAME_HANDLER);
FieldId<G, Method[]> allMethods =
generatedType.getField(methodArrayType, FIELD_NAME_METHODS);
TypeId<Method> methodType = TypeId.get(Method.class);
TypeId<Object[]> objectArrayType = TypeId.get(Object[].class);
MethodId<InvocationHandler, Object> methodInvoke = handlerType.getMethod(TypeId.OBJECT,
"invoke", TypeId.OBJECT, methodType, objectArrayType);
for (int m = 0; m < methodsToProxy.length; ++m) {
/*
* If the 5th method on the superclass Example that can be overridden were to look like
* this:
*
* public int doSomething(Bar param0, int param1) {
* ...
* }
*
* Then the following code will generate a method on the proxy that looks something
* like this:
*
* public int doSomething(Bar param0, int param1) {
* int methodIndex = 4;
* Method[] allMethods = Example_Proxy.$__methodArray;
* Method thisMethod = allMethods[methodIndex];
* int argsLength = 2;
* Object[] args = new Object[argsLength];
* InvocationHandler localHandler = this.$__handler;
* // for-loop begins
* int p = 0;
* Bar parameter0 = param0;
* args[p] = parameter0;
* p = 1;
* int parameter1 = param1;
* Integer boxed1 = Integer.valueOf(parameter1);
* args[p] = boxed1;
* // for-loop ends
* Object result = localHandler.invoke(this, thisMethod, args);
* Integer castResult = (Integer) result;
* int unboxedResult = castResult.intValue();
* return unboxedResult;
* }
*
* Or, in more idiomatic Java:
*
* public int doSomething(Bar param0, int param1) {
* if ($__handler == null) {
* return super.doSomething(param0, param1);
* }
* return __handler.invoke(this, __methodArray[4],
* new Object[] { param0, Integer.valueOf(param1) });
* }
*/
Method method = methodsToProxy[m];
String name = method.getName();
Class<?>[] argClasses = method.getParameterTypes();
TypeId<?>[] argTypes = new TypeId<?>[argClasses.length];
for (int i = 0; i < argTypes.length; ++i) {
argTypes[i] = TypeId.get(argClasses[i]);
}
Class<?> returnType = method.getReturnType();
TypeId<?> resultType = TypeId.get(returnType);
MethodId<T, ?> superMethod = superclassType.getMethod(resultType, name, argTypes);
MethodId<?, ?> methodId = generatedType.getMethod(resultType, name, argTypes);
Code code = dexMaker.declare(methodId, PUBLIC);
Local<G> localThis = code.getThis(generatedType);
Local<InvocationHandler> localHandler = code.newLocal(handlerType);
Local<Object> invokeResult = code.newLocal(TypeId.OBJECT);
Local<Integer> intValue = code.newLocal(TypeId.INT);
Local<Object[]> args = code.newLocal(objectArrayType);
Local<Integer> argsLength = code.newLocal(TypeId.INT);
Local<Object> temp = code.newLocal(TypeId.OBJECT);
Local<?> resultHolder = code.newLocal(resultType);
Local<Method[]> methodArray = code.newLocal(methodArrayType);
Local<Method> thisMethod = code.newLocal(methodType);
Local<Integer> methodIndex = code.newLocal(TypeId.INT);
Class<?> aBoxedClass = PRIMITIVE_TO_BOXED.get(returnType);
Local<?> aBoxedResult = null;
if (aBoxedClass != null) {
aBoxedResult = code.newLocal(TypeId.get(aBoxedClass));
}
Local<?>[] superArgs2 = new Local<?>[argClasses.length];
Local<?> superResult2 = code.newLocal(resultType);
Local<InvocationHandler> nullHandler = code.newLocal(handlerType);
code.loadConstant(methodIndex, m);
code.sget(allMethods, methodArray);
code.aget(thisMethod, methodArray, methodIndex);
code.loadConstant(argsLength, argTypes.length);
code.newArray(args, argsLength);
code.iget(handlerField, localHandler, localThis);
// if (proxy == null)
code.loadConstant(nullHandler, null);
Label handlerNullCase = new Label();
code.compare(Comparison.EQ, handlerNullCase, nullHandler, localHandler);
// This code is what we execute when we have a valid proxy: delegate to invocation
// handler.
for (int p = 0; p < argTypes.length; ++p) {
code.loadConstant(intValue, p);
Local<?> parameter = code.getParameter(p, argTypes[p]);
Local<?> unboxedIfNecessary = boxIfRequired(code, parameter, temp);
code.aput(args, intValue, unboxedIfNecessary);
}
code.invokeInterface(methodInvoke, invokeResult, localHandler,
localThis, thisMethod, args);
generateCodeForReturnStatement(code, returnType, invokeResult, resultHolder,
aBoxedResult);
// This code is executed if proxy is null: call the original super method.
// This is required to handle the case of construction of an object which leaks the
// "this" pointer.
code.mark(handlerNullCase);
for (int i = 0; i < superArgs2.length; ++i) {
superArgs2[i] = code.getParameter(i, argTypes[i]);
}
if (void.class.equals(returnType)) {
code.invokeSuper(superMethod, null, localThis, superArgs2);
code.returnVoid();
} else {
invokeSuper(superMethod, code, localThis, superArgs2, superResult2);
code.returnValue(superResult2);
}
/*
* And to allow calling the original super method, the following is also generated:
*
* public String super$doSomething$java_lang_String(Bar param0, int param1) {
* int result = super.doSomething(param0, param1);
* return result;
* }
*/
// TODO: don't include a super_ method if the target is abstract!
MethodId<G, ?> callsSuperMethod = generatedType.getMethod(
resultType, superMethodName(method), argTypes);
Code superCode = dexMaker.declare(callsSuperMethod, PUBLIC);
Local<G> superThis = superCode.getThis(generatedType);
Local<?>[] superArgs = new Local<?>[argClasses.length];
for (int i = 0; i < superArgs.length; ++i) {
superArgs[i] = superCode.getParameter(i, argTypes[i]);
}
if (void.class.equals(returnType)) {
superCode.invokeSuper(superMethod, null, superThis, superArgs);
superCode.returnVoid();
} else {
Local<?> superResult = superCode.newLocal(resultType);
invokeSuper(superMethod, superCode, superThis, superArgs, superResult);
superCode.returnValue(superResult);
}
}
}
@SuppressWarnings({"unchecked", "rawtypes"})
private static void invokeSuper(MethodId superMethod, Code superCode,
Local superThis, Local[] superArgs, Local superResult) {
superCode.invokeSuper(superMethod, superResult, superThis, superArgs);
}
private static Local<?> boxIfRequired(Code code, Local<?> parameter, Local<Object> temp) {
MethodId<?, ?> unboxMethod = PRIMITIVE_TYPE_TO_UNBOX_METHOD.get(parameter.getType());
if (unboxMethod == null) {
return parameter;
}
code.invokeStatic(unboxMethod, temp, parameter);
return temp;
}
public static Object callSuper(Object proxy, Method method, Object... args) throws Throwable {
try {
return proxy.getClass()
.getMethod(superMethodName(method), method.getParameterTypes())
.invoke(proxy, args);
} catch (InvocationTargetException e) {
throw e.getCause();
}
}
/**
* The super method must include the return type, otherwise its ambiguous
* for methods with covariant return types.
*/
private static String superMethodName(Method method) {
String returnType = method.getReturnType().getName();
return "super$" + method.getName() + "$"
+ returnType.replace('.', '_').replace('[', '_').replace(';', '_');
}
private static void check(boolean condition, String message) {
if (!condition) {
throw new IllegalArgumentException(message);
}
}
private static <T, G extends T> void generateConstructorsAndFields(DexMaker dexMaker,
TypeId<G> generatedType, TypeId<T> superType, Class<T> superClass) {
TypeId<InvocationHandler> handlerType = TypeId.get(InvocationHandler.class);
TypeId<Method[]> methodArrayType = TypeId.get(Method[].class);
FieldId<G, InvocationHandler> handlerField = generatedType.getField(
handlerType, FIELD_NAME_HANDLER);
dexMaker.declare(handlerField, PRIVATE, null);
FieldId<G, Method[]> allMethods = generatedType.getField(
methodArrayType, FIELD_NAME_METHODS);
dexMaker.declare(allMethods, PRIVATE | STATIC, null);
for (Constructor<T> constructor : getConstructorsToOverwrite(superClass)) {
if (constructor.getModifiers() == Modifier.FINAL) {
continue;
}
TypeId<?>[] types = classArrayToTypeArray(constructor.getParameterTypes());
MethodId<?, ?> method = generatedType.getConstructor(types);
Code constructorCode = dexMaker.declare(method, PUBLIC);
Local<G> thisRef = constructorCode.getThis(generatedType);
Local<?>[] params = new Local[types.length];
for (int i = 0; i < params.length; ++i) {
params[i] = constructorCode.getParameter(i, types[i]);
}
MethodId<T, ?> superConstructor = superType.getConstructor(types);
constructorCode.invokeDirect(superConstructor, null, thisRef, params);
constructorCode.returnVoid();
}
}
// The type parameter on Constructor is the class in which the constructor is declared.
// The getDeclaredConstructors() method gets constructors declared only in the given class,
// hence this cast is safe.
@SuppressWarnings("unchecked")
private static <T> Constructor<T>[] getConstructorsToOverwrite(Class<T> clazz) {
return (Constructor<T>[]) clazz.getDeclaredConstructors();
}
private TypeId<?>[] getInterfacesAsTypeIds() {
TypeId<?>[] result = new TypeId<?>[interfaces.size()];
int i = 0;
for (Class<?> implemented : interfaces) {
result[i++] = TypeId.get(implemented);
}
return result;
}
/**
* Gets all {@link Method} objects we can proxy in the hierarchy of the
* supplied class.
*/
private Method[] getMethodsToProxyRecursive() {
Set<MethodSetEntry> methodsToProxy = new HashSet<>();
Set<MethodSetEntry> seenFinalMethods = new HashSet<>();
// Traverse the class hierarchy to ensure that all concrete methods (which could be marked
// as final) are visited before any abstract methods from interfaces.
for (Class<?> c = baseClass; c != null; c = c.getSuperclass()) {
getMethodsToProxy(methodsToProxy, seenFinalMethods, c);
}
// Now traverse the interface hierarchy, starting with the ones implemented by the class,
// followed by any extra interfaces.
for (Class<?> c = baseClass; c != null; c = c.getSuperclass()) {
for (Class<?> i : c.getInterfaces()) {
getMethodsToProxy(methodsToProxy, seenFinalMethods, i);
}
}
for (Class<?> c : interfaces) {
getMethodsToProxy(methodsToProxy, seenFinalMethods, c);
}
Method[] results = new Method[methodsToProxy.size()];
int i = 0;
for (MethodSetEntry entry : methodsToProxy) {
results[i++] = entry.originalMethod;
}
return results;
}
private void getMethodsToProxy(Set<MethodSetEntry> sink, Set<MethodSetEntry> seenFinalMethods,
Class<?> c) {
for (Method method : c.getDeclaredMethods()) {
if ((method.getModifiers() & Modifier.FINAL) != 0) {
// Skip final methods, we can't override them. We
// also need to remember them, in case the same
// method exists in a parent class.
MethodSetEntry entry = new MethodSetEntry(method);
seenFinalMethods.add(entry);
// We may have seen this method already, from an interface
// implemented by a child class. We need to remove it here.
sink.remove(entry);
continue;
}
if ((method.getModifiers() & STATIC) != 0) {
// Skip static methods, overriding them has no effect.
continue;
}
if (!Modifier.isPublic(method.getModifiers())
&& !Modifier.isProtected(method.getModifiers())
&& (!sharedClassLoader || Modifier.isPrivate(method.getModifiers()))) {
// Skip private methods, since they are invoked through direct
// invocation (as opposed to virtual). Therefore, it would not
// be possible to intercept any private method defined inside
// the proxy class except through reflection.
// Skip package-private methods as well (for non-shared class
// loaders). The proxy class does
// not actually inherit package-private methods from the parent
// class because it is not a member of the parent's package.
// This is even true if the two classes have the same package
// name, as they use different class loaders.
continue;
}
if (method.getName().equals("finalize") && method.getParameterTypes().length == 0) {
// Skip finalize method, it's likely important that it execute as normal.
continue;
}
MethodSetEntry entry = new MethodSetEntry(method);
if (seenFinalMethods.contains(entry)) {
// This method is final in a child class.
// We can't override it.
continue;
}
sink.add(entry);
}
// Only visit the interfaces of this class if it is itself an interface. That prevents
// visiting interfaces of a class before its super classes.
if (c.isInterface()) {
for (Class<?> i : c.getInterfaces()) {
getMethodsToProxy(sink, seenFinalMethods, i);
}
}
}
private static <T> String getMethodNameForProxyOf(Class<T> clazz) {
return clazz.getName().replace(".", "/") + "_Proxy";
}
private static TypeId<?>[] classArrayToTypeArray(Class<?>[] input) {
TypeId<?>[] result = new TypeId[input.length];
for (int i = 0; i < input.length; ++i) {
result[i] = TypeId.get(input[i]);
}
return result;
}
/**
* Calculates the correct return statement code for a method.
* <p>
* A void method will not return anything. A method that returns a primitive will need to
* unbox the boxed result. Otherwise we will cast the result.
*/
// This one is tricky to fix, I gave up.
@SuppressWarnings({ "rawtypes", "unchecked" })
private static void generateCodeForReturnStatement(Code code, Class methodReturnType,
Local localForResultOfInvoke, Local localOfMethodReturnType, Local aBoxedResult) {
if (PRIMITIVE_TO_UNBOX_METHOD.containsKey(methodReturnType)) {
code.cast(aBoxedResult, localForResultOfInvoke);
MethodId unboxingMethodFor = getUnboxMethodForPrimitive(methodReturnType);
code.invokeVirtual(unboxingMethodFor, localOfMethodReturnType, aBoxedResult);
code.returnValue(localOfMethodReturnType);
} else if (void.class.equals(methodReturnType)) {
code.returnVoid();
} else {
code.cast(localOfMethodReturnType, localForResultOfInvoke);
code.returnValue(localOfMethodReturnType);
}
}
private static <T> Set<T> asSet(T... array) {
return new CopyOnWriteArraySet<>(Arrays.asList(array));
}
private static MethodId<?, ?> getUnboxMethodForPrimitive(Class<?> methodReturnType) {
return PRIMITIVE_TO_UNBOX_METHOD.get(methodReturnType);
}
private static final Map<Class<?>, Class<?>> PRIMITIVE_TO_BOXED;
static {
PRIMITIVE_TO_BOXED = new HashMap<>();
PRIMITIVE_TO_BOXED.put(boolean.class, Boolean.class);
PRIMITIVE_TO_BOXED.put(int.class, Integer.class);
PRIMITIVE_TO_BOXED.put(byte.class, Byte.class);
PRIMITIVE_TO_BOXED.put(long.class, Long.class);
PRIMITIVE_TO_BOXED.put(short.class, Short.class);
PRIMITIVE_TO_BOXED.put(float.class, Float.class);
PRIMITIVE_TO_BOXED.put(double.class, Double.class);
PRIMITIVE_TO_BOXED.put(char.class, Character.class);
}
private static final Map<TypeId<?>, MethodId<?, ?>> PRIMITIVE_TYPE_TO_UNBOX_METHOD;
static {
PRIMITIVE_TYPE_TO_UNBOX_METHOD = new HashMap<>();
for (Map.Entry<Class<?>, Class<?>> entry : PRIMITIVE_TO_BOXED.entrySet()) {
TypeId<?> primitiveType = TypeId.get(entry.getKey());
TypeId<?> boxedType = TypeId.get(entry.getValue());
MethodId<?, ?> valueOfMethod = boxedType.getMethod(boxedType, "valueOf", primitiveType);
PRIMITIVE_TYPE_TO_UNBOX_METHOD.put(primitiveType, valueOfMethod);
}
}
/**
* Map from primitive type to method used to unbox a boxed version of the primitive.
* <p>
* This is required for methods whose return type is primitive, since the
* {@link InvocationHandler} will return us a boxed result, and we'll need to convert it back to
* primitive value.
*/
private static final Map<Class<?>, MethodId<?, ?>> PRIMITIVE_TO_UNBOX_METHOD;
static {
Map<Class<?>, MethodId<?, ?>> map = new HashMap<>();
map.put(boolean.class, TypeId.get(Boolean.class).getMethod(TypeId.BOOLEAN, "booleanValue"));
map.put(int.class, TypeId.get(Integer.class).getMethod(TypeId.INT, "intValue"));
map.put(byte.class, TypeId.get(Byte.class).getMethod(TypeId.BYTE, "byteValue"));
map.put(long.class, TypeId.get(Long.class).getMethod(TypeId.LONG, "longValue"));
map.put(short.class, TypeId.get(Short.class).getMethod(TypeId.SHORT, "shortValue"));
map.put(float.class, TypeId.get(Float.class).getMethod(TypeId.FLOAT, "floatValue"));
map.put(double.class, TypeId.get(Double.class).getMethod(TypeId.DOUBLE, "doubleValue"));
map.put(char.class, TypeId.get(Character.class).getMethod(TypeId.CHAR, "charValue"));
PRIMITIVE_TO_UNBOX_METHOD = map;
}
/**
* Wrapper class to let us disambiguate {@link Method} objects.
* <p>
* The purpose of this class is to override the {@link #equals(Object)} and {@link #hashCode()}
* methods so we can use a {@link Set} to remove duplicate methods that are overrides of one
* another. For these purposes, we consider two methods to be equal if they have the same
* name, return type, and parameter types.
*/
public static class MethodSetEntry {
public final String name;
public final Class<?>[] paramTypes;
public final Class<?> returnType;
public final Method originalMethod;
public MethodSetEntry(Method method) {
originalMethod = method;
name = method.getName();
paramTypes = method.getParameterTypes();
returnType = method.getReturnType();
}
@Override
public boolean equals(Object o) {
if (o instanceof MethodSetEntry) {
MethodSetEntry other = (MethodSetEntry) o;
return name.equals(other.name)
&& returnType.equals(other.returnType)
&& Arrays.equals(paramTypes, other.paramTypes);
}
return false;
}
@Override
public int hashCode() {
int result = 17;
result += 31 * result + name.hashCode();
result += 31 * result + returnType.hashCode();
result += 31 * result + Arrays.hashCode(paramTypes);
return result;
}
}
}