All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.android.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 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.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import static java.lang.reflect.Modifier.ABSTRACT;
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.
 * 

* 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 * 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. *

* 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 List> interfaces = new ArrayList<>(); private Method[] methods; private boolean sharedClassLoader; private boolean markTrusted; 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) { List> list = this.interfaces; for (Class i : interfaces) { if (!i.isInterface()) { throw new IllegalArgumentException("Not an interface: " + i.getName()); } if (!list.contains(i)) { list.add(i); } } return this; } public ProxyBuilder constructorArgValues(Object... constructorArgValues) { this.constructorArgValues = constructorArgValues; return this; } public ProxyBuilder constructorArgTypes(Class... constructorArgTypes) { this.constructorArgTypes = constructorArgTypes; return this; } public ProxyBuilder onlyMethods(Method[] methods) { this.methods = methods; return this; } public ProxyBuilder withSharedClassLoader() { this.sharedClassLoader = true; return this; } public ProxyBuilder markTrusted() { this.markTrusted = 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 proxyClass = buildProxyClass(); Constructor 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 buildProxyClass() throws IOException { ClassLoader requestedClassloader; if (sharedClassLoader) { requestedClassloader = baseClass.getClassLoader(); } else { requestedClassloader = parentClassLoader; } // try the cache to see if we've generated this one before // we only populate the map with matching types ProxiedClass cacheKey = new ProxiedClass<>(baseClass, interfaces, requestedClassloader, sharedClassLoader); @SuppressWarnings("unchecked") Class proxyClass = (Class) generatedProxyClasses.get(cacheKey); if (proxyClass != null) { return proxyClass; // cache hit! } // the cache missed; generate the class DexMaker dexMaker = new DexMaker(); String generatedName = getMethodNameForProxyOf(baseClass, interfaces); TypeId generatedType = TypeId.get("L" + generatedName + ";"); TypeId 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. // // We use the same parameters to sort as used in {@link MethodId#hashCode}. This is needed // as e.g. making a method "public" instead of "protected" should not change the id's of the // methods. If the id's would change the classes loaded from the cache would be incorrect. Arrays.sort(methodsToProxy, new Comparator() { @Override public int compare(Method method1, Method method2) { String m1Signature = method1.getDeclaringClass() + method1.getName() + Arrays.toString(method1.getParameterTypes()) + method1.getReturnType(); String m2Signature = method2.getDeclaringClass() + method2.getName() + Arrays.toString(method2.getParameterTypes()) + method2.getReturnType(); return m1Signature.compareTo(m2Signature); } }); generateCodeForAllMethods(dexMaker, generatedType, methodsToProxy, superType); dexMaker.declare(generatedType, generatedName + ".generated", PUBLIC, superType, getInterfacesAsTypeIds()); if (sharedClassLoader) { dexMaker.setSharedClassLoader(requestedClassloader); } if (markTrusted) { // The proxied class might have blacklisted methods. Blacklisting methods (and fields) // is a new feature of Android P: // // https://android-developers.googleblog.com/2018/02/ // improving-stability-by-reducing-usage.html // // The newly generated class might not be allowed to call methods of the proxied class // if it is not trusted. As it is not clear which classes have blacklisted methods, mark // all generated classes as trusted. dexMaker.markAsTrusted(); } ClassLoader classLoader; if (sharedClassLoader) { classLoader = dexMaker.generateAndLoad(null, dexCache); } else { 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(cacheKey, proxyClass); return proxyClass; } // The type cast is safe: the generated type will extend the base class type. @SuppressWarnings("unchecked") private Class loadClass(ClassLoader classLoader, String generatedName) throws ClassNotFoundException { return (Class) 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; } } /** * Add * *

     *     abstractMethodErrorMessage = method + " cannot be called";
     *     abstractMethodError = new AbstractMethodError(abstractMethodErrorMessage);
     *     throw abstractMethodError;
     * 
* * to the {@code code}. * * @param code The code to add to * @param method The method that is abstract * @param abstractMethodErrorMessage The {@link Local} to store the error message * @param abstractMethodError The {@link Local} to store the error object */ private static void throwAbstractMethodError(Code code, Method method, Local abstractMethodErrorMessage, Local abstractMethodError) { TypeId abstractMethodErrorClass = TypeId.get(AbstractMethodError.class); MethodId abstractMethodErrorConstructor = abstractMethodErrorClass.getConstructor(TypeId.STRING); code.loadConstant(abstractMethodErrorMessage, "'" + method + "' cannot be called"); code.newInstance(abstractMethodError, abstractMethodErrorConstructor, abstractMethodErrorMessage); code.throwValue(abstractMethodError); } 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 objectArrayType = TypeId.get(Object[].class); MethodId 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 dex byte code will generate a method on the proxy that looks * something like this (in idiomatic Java): * * // if doSomething is not abstract * 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) }); * } * * // if doSomething is abstract * public int doSomething(Bar param0, int param1) { * if ($__handler == null) { * throw new AbstractMethodError("'doSomething' cannot be called"); * } * 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 methodId = generatedType.getMethod(resultType, name, argTypes); TypeId abstractMethodErrorClass = TypeId.get(AbstractMethodError.class); Code code = dexMaker.declare(methodId, PUBLIC); Local localThis = code.getThis(generatedType); Local localHandler = code.newLocal(handlerType); Local invokeResult = code.newLocal(TypeId.OBJECT); Local intValue = code.newLocal(TypeId.INT); Local args = code.newLocal(objectArrayType); Local argsLength = code.newLocal(TypeId.INT); Local temp = code.newLocal(TypeId.OBJECT); Local resultHolder = code.newLocal(resultType); Local methodArray = code.newLocal(methodArrayType); Local thisMethod = code.newLocal(methodType); Local 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 nullHandler = code.newLocal(handlerType); Local[] superArgs2 = null; Local superResult2 = null; MethodId superMethod = null; Local abstractMethodErrorMessage = null; Local abstractMethodError = null; if ((method.getModifiers() & ABSTRACT) == 0) { superArgs2 = new Local[argClasses.length]; superResult2 = code.newLocal(resultType); superMethod = superclassType.getMethod(resultType, name, argTypes); } else { abstractMethodErrorMessage = code.newLocal(TypeId.STRING); abstractMethodError = code.newLocal(abstractMethodErrorClass); } 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); if ((method.getModifiers() & ABSTRACT) == 0) { 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); } } else { throwAbstractMethodError(code, method, abstractMethodErrorMessage, abstractMethodError); } /* * 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; * } */ MethodId callsSuperMethod = generatedType.getMethod( resultType, superMethodName(method), argTypes); Code superCode = dexMaker.declare(callsSuperMethod, PUBLIC); if ((method.getModifiers() & ABSTRACT) == 0) { Local 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); } } else { Local superAbstractMethodErrorMessage = superCode.newLocal(TypeId.STRING); Local superAbstractMethodError = superCode.newLocal (abstractMethodErrorClass); throwAbstractMethodError(superCode, method, superAbstractMethodErrorMessage, superAbstractMethodError); } } } @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 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 void generateConstructorsAndFields(DexMaker dexMaker, TypeId generatedType, TypeId superType, Class superClass) { TypeId handlerType = TypeId.get(InvocationHandler.class); TypeId methodArrayType = TypeId.get(Method[].class); FieldId handlerField = generatedType.getField( handlerType, FIELD_NAME_HANDLER); dexMaker.declare(handlerField, PRIVATE, null); FieldId allMethods = generatedType.getField( methodArrayType, FIELD_NAME_METHODS); dexMaker.declare(allMethods, PRIVATE | STATIC, null); for (Constructor 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 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 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 Constructor[] getConstructorsToOverwrite(Class clazz) { return (Constructor[]) 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 methodsToProxy = new HashSet<>(); Set 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 sink, Set 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 String getMethodNameForProxyOf(Class clazz, List> interfaces) { String interfacesHash = Integer.toHexString(interfaces.hashCode()); return clazz.getName().replace(".", "/") + "_" + interfacesHash + "_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. *

* 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 MethodId getUnboxMethodForPrimitive(Class methodReturnType) { return PRIMITIVE_TO_UNBOX_METHOD.get(methodReturnType); } private static final Map, 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, MethodId> PRIMITIVE_TYPE_TO_UNBOX_METHOD; static { PRIMITIVE_TYPE_TO_UNBOX_METHOD = new HashMap<>(); for (Map.Entry, 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. *

* 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, MethodId> PRIMITIVE_TO_UNBOX_METHOD; static { Map, 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. *

* 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; } } /** * A class that was already proxied. */ private static class ProxiedClass { final Class clazz; final List> interfaces; /** * Class loader requested when the proxy class was generated. This might not be the * class loader of {@code clazz} as not all class loaders can be shared. * * @see DexMaker#generateClassLoader(File, File, ClassLoader) */ final ClassLoader requestedClassloader; final boolean sharedClassLoader; @Override public boolean equals(Object other) { if (this == other) { return true; } if (other == null || getClass() != other.getClass()) { return false; } ProxiedClass that = (ProxiedClass) other; return clazz == that.clazz && interfaces.equals(that.interfaces) && requestedClassloader == that.requestedClassloader && sharedClassLoader == that.sharedClassLoader; } @Override public int hashCode() { return clazz.hashCode() + interfaces.hashCode() + requestedClassloader.hashCode() + (sharedClassLoader ? 1 : 0); } private ProxiedClass(Class clazz, List> interfaces, ClassLoader requestedClassloader, boolean sharedClassLoader) { this.clazz = clazz; this.interfaces = new ArrayList<>(interfaces); this.requestedClassloader = requestedClassloader; this.sharedClassLoader = sharedClassLoader; } } }