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

ru.saidgadjiev.proxymaker.ProxyMaker Maven / Gradle / Ivy

The newest version!
package ru.saidgadjiev.proxymaker;

import ru.saidgadjiev.logger.Log;
import ru.saidgadjiev.logger.LoggerFactory;
import ru.saidgadjiev.proxymaker.bytecode.ByteCodeUtils;
import ru.saidgadjiev.proxymaker.bytecode.CodeAttribute;
import ru.saidgadjiev.proxymaker.bytecode.FieldInfo;
import ru.saidgadjiev.proxymaker.bytecode.MethodInfo;
import ru.saidgadjiev.proxymaker.bytecode.constantpool.ConstantPool;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;

import static ru.saidgadjiev.proxymaker.ProxyFactoryHelper.*;

/**
 * Class for create dynamic proxy classes.
 */
public class ProxyMaker {

    /**
     * Logger.
     */
    private static final Log LOGGER = LoggerFactory.getLogger(ProxyMaker.class);

    /**
     * Object class {@link Class}.
     */
    private static final Class OBJECT_CLASS = Object.class;

    /**
     * Proxy method invocation handler field name in dynamic proxy class.
     */
    private static final String HANDLER_FIELD_NAME = "handler";

    /**
     * Proxy invocation handler type for bytecode.
     */
    private static final String HANDLER_TYPE = 'L' + MethodHandler.class.getName().replace('.', '/') + ';';

    /**
     * Invocation handler setter method name {@link Proxy#setHandler(MethodHandler)}.
     */
    private static final String HANDLER_SETTER_METHOD_NAME = "setHandler";

    /**
     * Invocation handler setter method type.
     */
    private static final String HANDLER_SETTER_METHOD_TYPE = "(" + HANDLER_TYPE + ")V";

    /**
     * Handler getter method name {@link Proxy#getHandler()}.
     */
    private static final String HANDLER_GETTER_METHOD_NAME = "getHandler";

    /**
     * Handler getter method type.
     */
    private static final String HANDLER_GETTER_METHOD_TYPE = "()" + HANDLER_TYPE;

    /**
     * Proxied methods list holder field name.
     */
    private static final String HOLDER_FIELD_NAME = "methods";

    /**
     * Proxied methods list holder field type. It is {@link List}.
     */
    private static final String HOLDER_FIELD_TYPE = "L" + List.class.getName().replace('.', '/') + ";";

    /**
     * Proxy invocation handler method name.
     *
     * @see MethodHandler#invoke
     */
    private static final String PROXY_INTERFACE_INVOKE_METHOD_NAME = "invoke";

    /**
     * Proxy invocation handler method description.
     */
    private static final String PROXY_INTERFACE_INVOKE_METHOD_DESC =
            "(Ljava/lang/reflect/Method;[Ljava/lang/Object;)Ljava/lang/Object;";

    /**
     * Invocation handler method definition count {@link MethodHandler#invoke(Method, Object[])}.
     */
    private static final int PROXY_INTERFACE_INVOKE_METHOD_DEFINITION_COUTN = 3;

    /**
     * Super class.
     */
    private Class superClass;

    /**
     * Interfaces.
     */
    private Class[] interfaces;

    /**
     * Fully qualified super class name.
     */
    private String superClassName;

    /**
     * Fully qualified dynamic proxy class name.
     */
    private String className;

    /**
     * Proxied methods list.
     */
    private List methods = new ArrayList<>();

    /**
     * Cached dynamic proxy classes.
     */
    private static final Map>> PROXY_CACHE = new WeakHashMap<>();

    /**
     * Unique integer index generator. Used for make dynamic proxy class name unique.
     */
    private final UIDGenerator uidGenerator = new UIDGenerator() {

        private AtomicInteger uid = new AtomicInteger(0);

        @Override
        public int nextUID() {
            return uid.getAndIncrement();
        }
    };

    /**
     * Ignore override methods.
     */
    private static final Map> OVERRIDE_IGNORE_METHODS = new HashMap>() {{
        put("getClass", Object.class);
        put("notify", Object.class);
        put("notifyAll", Object.class);
        put("wait", Object.class);
        put("finalize", Object.class);
        put("clone", Object.class);
    }};

    /**
     * Provide dynamic proxy super class. If not provided it will be {@code OBJECT_CLASS}
     *
     * @param superClass target super class
     * @return current instance
     */
    public ProxyMaker superClass(Class superClass) {
        this.superClass = superClass;

        return this;
    }

    /**
     * Resolve super class and dynamic proxy class names.
     * If super class is null super class will be {@code OBJECT_CLASS}.
     * Class name make with concatenate {@code superClassName} and {@code uidGenerator} nextUID.
     */
    private void resolveSuperClassAndClassName() {
        if (interfaces == null) {
            interfaces = new Class[0];
        }
        if (superClass == null) {
            superClass = OBJECT_CLASS;
            superClassName = OBJECT_CLASS.getName();
        } else {
            superClassName = superClass.getName();
        }

        className = superClassName + uidGenerator.nextUID();
        if (className.startsWith("java.")) {
            className = "ru.proxy.maker.tmp." + className;
        }
    }

    /**
     * Resolve dynamic proxy override methods.
     * Ignore methods @{code OVERRIDE_IGNORE_METHODS}
     */
    private void resolveMethods() {
        this.methods = getMethods(superClass, interfaces);
    }

    /**
     * Resolve override methods.
     *
     * @param superClass target super class
     * @param interfaces target interfaces
     * @return resolved methods
     */
    private List getMethods(Class superClass,
                                    Class[] interfaces) {

        Map methods = new LinkedHashMap<>();
        Set> visitedClasses = new HashSet<>();

        for (Class targetClass : interfaces) {
            getMethods(methods, targetClass, visitedClasses);
        }

        getMethods(methods, superClass, visitedClasses);

        return new ArrayList<>(methods.values());
    }

    /**
     * Recursively resolve methods by target class.
     *
     * @param methods        map for save resolved methods
     * @param targetClass    target class {@link Class}
     * @param visitedClasses visited classes. This both speeds up scanning by avoiding duplicate interfaces and
     *                       is needed to ensure that superinterfaces are always scanned before subinterfaces.
     */
    private void getMethods(Map methods,
                            Class targetClass,
                            Set> visitedClasses) {
        if (!visitedClasses.add(targetClass)) {
            return;
        }

        Class[] interfaces = targetClass.getInterfaces();

        for (Class interfaceClass : interfaces) {
            getMethods(methods, interfaceClass, visitedClasses);
        }
        Class parentClass = targetClass.getSuperclass();

        if (parentClass != null) {
            getMethods(methods, parentClass, visitedClasses);
        }
        for (Method method : targetClass.getDeclaredMethods()) {
            if (!Modifier.isPrivate(method.getModifiers())
                    && !(OVERRIDE_IGNORE_METHODS.containsKey(method.getName())
                    && OVERRIDE_IGNORE_METHODS.get(method.getName()).equals(method.getDeclaringClass()))) {
                String key = method.getName() + ":" + ByteCodeUtils.makeDescriptor(method);

                methods.put(key, method);
            }
        }
    }

    /**
     * Add default constructor to class file {@link ClassFile}.
     *
     * @param classFile target class file
     */
    private void addDefaultConstructor(ClassFile classFile) {
        MethodInfo methodInfo = new MethodInfo(classFile.getConstantPool(), MethodInfo.NAME_INIT, "()V");

        methodInfo.setAccessFlags(AccessFlag.PUBLIC);
        CodeAttribute codeAttribute = new CodeAttribute(classFile.getConstantPool(), 0, 1);

        codeAttribute.addAload(0);
        codeAttribute.addInvokeSpecial(superClass.getName(), MethodInfo.NAME_INIT, "()V");
        codeAttribute.addOpcode(CodeAttribute.Opcode.RETURN);

        methodInfo.setCodeAttribute(codeAttribute);
        classFile.addMethodInfo(methodInfo);
    }

    /**
     * Create new dynamic proxy class instance.
     *
     * @param handler method handler for the proxy class
     * @return new dynamic proxy class instance
     * @throws InvocationTargetException throws in {@link Constructor#newInstance(Object...)}
     * @throws IllegalAccessException    throws in {@link Constructor#newInstance(Object...)}
     * @throws NoSuchMethodException     throws in {@link Class#getConstructor(Class[])}}
     * @throws InstantiationException    throws in {@link Constructor#newInstance(Object...)}
     */
    public Object make(MethodHandler handler) throws
            InvocationTargetException,
            IllegalAccessException,
            NoSuchMethodException,
            InstantiationException {
        synchronized (PROXY_CACHE) {
            ClassLoader classLoader = getClassLoader();
            Map> cacheForTheLoader = PROXY_CACHE.computeIfAbsent(classLoader, k -> new HashMap<>());
            String key = getKey();

            if (cacheForTheLoader.containsKey(key)) {
                Class cachedClass = cacheForTheLoader.get(key);

                Object proxy = createInstance(cachedClass, handler);

                LOGGER.debug("Retrieve class " + cachedClass + " from cache " + key);

                return proxy;
            } else {
                LOGGER.debug("Cache miss for " + key);
            }

            Class proxyClass = createClass(classLoader);

            cacheForTheLoader.put(key, proxyClass);

            LOGGER.debug("Put to cache " + key + " class " + proxyClass);

            return createInstance(proxyClass, handler);
        }
    }

    /**
     * Create new dynamic proxy class instance.
     *
     * @param classLoader proxy class loader
     * @return new dynamic proxy class
     */
    private Class createClass(ClassLoader classLoader) {
        resolveSuperClassAndClassName();
        resolveMethods();
        ClassFile classFile = new ClassFile(className, superClassName);

        setInterfaces(classFile, interfaces, Proxy.class);
        classFile.setAccessFlags(AccessFlag.PUBLIC);
        addDefaultConstructor(classFile);
        addDefaultClassFields(classFile);
        addClassInitializer(classFile);
        addHandlerSetter(classFile);
        addHandlerGetter(classFile);
        overrideMethods(classFile);

        return ProxyFactoryHelper.toClass(classFile, classLoader);
    }

    /**
     * Get classloader.
     *
     * @return new class classloader
     */
    private ClassLoader getClassLoader() {
        ClassLoader loader = null;

        if (superClass != null && !superClass.getName().equals("java.lang.Object")) {
            loader = superClass.getClassLoader();
        }
        if (loader == null) {
            loader = getClass().getClassLoader();

            if (loader == null) {
                loader = Thread.currentThread().getContextClassLoader();

                if (loader == null) {
                    loader = ClassLoader.getSystemClassLoader();
                }
            }
        }

        return loader;
    }

    /**
     * Create new instance of dynamic proxy class {@code proxyClass} by default constructor.
     *
     * @param proxyClass target class
     * @param handler    dynamic proxy invocation handler
     * @return new dynamic proxy class instance
     * @throws NoSuchMethodException     throws in {@link Class#getConstructor(Class[])}
     * @throws IllegalAccessException    throws in {@link Constructor#newInstance(Object...)
     * @throws InvocationTargetException throws in {@link Constructor#newInstance(Object...)
     * @throws InstantiationException    throws in {@link Constructor#newInstance(Object...)
     */
    private Object createInstance(Class proxyClass, MethodHandler handler) throws
            NoSuchMethodException,
            IllegalAccessException,
            InvocationTargetException,
            InstantiationException {
        Constructor constructor = proxyClass.getConstructor();
        Object proxyInstance = constructor.newInstance();

        ((Proxy) proxyInstance).setHandler(handler);

        return proxyInstance;
    }

    /**
     * Return key for cached proxy class.
     *
     * @return this dynamic proxy class cached ky
     */
    private String getKey() {
        StringBuilder builder = new StringBuilder();

        builder.append(superClass.getName());

        if (interfaces != null && interfaces.length > 0) {
            builder.append(":");
            for (Class interfaceClass : interfaces) {
                builder.append(interfaceClass.getName());
            }
        }

        return builder.toString();
    }

    /**
     * Add default dynamic proxy class fields.
     * Add field for holder proxied methods, field for invocation handler {@link MethodHandler}.
     *
     * @param classFile target class file
     */
    private void addDefaultClassFields(ClassFile classFile) {
        FieldInfo handlerField = new FieldInfo(classFile.getConstantPool(), HANDLER_FIELD_NAME, HANDLER_TYPE);

        handlerField.setAccessFlags(AccessFlag.PRIVATE);
        classFile.addFieldInfo(handlerField);

        FieldInfo holderField = new FieldInfo(classFile.getConstantPool(), HOLDER_FIELD_NAME, HOLDER_FIELD_TYPE);

        holderField.setAccessFlags(AccessFlag.PRIVATE | AccessFlag.STATIC);
        classFile.addFieldInfo(holderField);
    }

    /**
     * Add invocation handler setter method {@link Proxy#setHandler(MethodHandler)}.
     *
     * @param classFile target class file
     */
    private void addHandlerSetter(ClassFile classFile) {
        MethodInfo methodInfo = new MethodInfo(
                classFile.getConstantPool(),
                HANDLER_SETTER_METHOD_NAME,
                HANDLER_SETTER_METHOD_TYPE
        );

        methodInfo.setAccessFlags(AccessFlag.PUBLIC);

        CodeAttribute codeAttribute = new CodeAttribute(classFile.getConstantPool(), 2, 2);

        codeAttribute.addAload(0);
        codeAttribute.addAload(1);
        codeAttribute.addPutField(className, HANDLER_FIELD_NAME, HANDLER_TYPE);
        codeAttribute.addOpcode(CodeAttribute.Opcode.RETURN);
        methodInfo.setCodeAttribute(codeAttribute);
        classFile.addMethodInfo(methodInfo);
    }

    /**
     * Add invocation handler getter method {@link Proxy#getHandler()}.
     *
     * @param classFile target class file
     */
    private void addHandlerGetter(ClassFile classFile) {
        MethodInfo methodInfo = new MethodInfo(
                classFile.getConstantPool(),
                HANDLER_GETTER_METHOD_NAME, HANDLER_GETTER_METHOD_TYPE
        );

        methodInfo.setAccessFlags(AccessFlag.PUBLIC);
        CodeAttribute code = new CodeAttribute(classFile.getConstantPool(), 1, 1);

        code.addAload(0);
        code.addGetField(className, HANDLER_FIELD_NAME, HANDLER_TYPE);

        code.addOpcode(CodeAttribute.Opcode.ARETURN);
        methodInfo.setCodeAttribute(code);

        classFile.addMethodInfo(methodInfo);
    }

    /**
     * Add static block to dynamic proxy class for resolve proxy method refs.
     *
     * @param classFile target class file.
     */
    private void addClassInitializer(ClassFile classFile) {
        MethodInfo methodInfo = new MethodInfo(classFile.getConstantPool(), MethodInfo.NAME_CLINIT, "()V");

        methodInfo.setAccessFlags(AccessFlag.STATIC);
        CodeAttribute codeAttribute = new CodeAttribute(classFile.getConstantPool(), 0, 0);

        codeAttribute.addNew(ArrayList.class.getName());
        codeAttribute.addOpcode(CodeAttribute.Opcode.DUP);
        codeAttribute.addInvokeSpecial(ArrayList.class.getName(), MethodInfo.NAME_INIT, "()V");

        codeAttribute.addPutStatic(className, HOLDER_FIELD_NAME, HOLDER_FIELD_TYPE);
        codeAttribute.addGetstatic(className, HOLDER_FIELD_NAME, HOLDER_FIELD_TYPE);

        for (Iterator methodIterator = methods.iterator(); methodIterator.hasNext();) {
            addCallFindMethod(codeAttribute, methodIterator.next());

            if (methodIterator.hasNext()) {
                codeAttribute.addGetstatic(className, HOLDER_FIELD_NAME, HOLDER_FIELD_TYPE);
            }
        }
        codeAttribute.addOpcode(CodeAttribute.Opcode.RETURN);
        methodInfo.setCodeAttribute(codeAttribute);
        classFile.addMethodInfo(methodInfo);
    }

    /**
     * Add static call {@link ProxyRuntimeHelper#findMethod(String, String, String[])} for find method in super class.
     *
     * @param code   target code attribute
     * @param method target method
     */
    private void addCallFindMethod(CodeAttribute code, Method method) {
        String findClass = ProxyRuntimeHelper.class.getName();
        String findDesc
                = "(Ljava/lang/String;Ljava/lang/String;[Ljava/lang/String;)Ljava/lang/reflect/Method;";

        code.addLdc(superClassName);
        code.addLdc(method.getName());
        code.addIconst(method.getParameterTypes().length);
        code.addAnewarray(String.class.getName());

        if (method.getParameterTypes().length > 0) {
            int iconstNumber = 0;

            code.addOpcode(CodeAttribute.Opcode.DUP);
            for (Iterator> parametrTypeIterator = Arrays.asList(method.getParameterTypes()).iterator();
                 parametrTypeIterator.hasNext();) {
                code.addIconst(iconstNumber++);
                code.addLdc(parametrTypeIterator.next().getName());
                code.addOpcode(CodeAttribute.Opcode.AASTORE);

                if (parametrTypeIterator.hasNext()) {
                    code.addOpcode(CodeAttribute.Opcode.DUP);
                }
            }
        }
        code.addInvokeStatic(findClass, "findMethod", findDesc);
        code.addInvokeInterface(List.class.getName(), "add", "(Ljava/lang/Object;)Z", 2);
        code.addOpcode(CodeAttribute.Opcode.POP);
    }

    /**
     * Override methods in dynamic proxy class.
     *
     * @param classFile target class file
     */
    private void overrideMethods(ClassFile classFile) {
        int methodIndex = 0;

        for (Method method : methods) {
            MethodInfo methodInfo = new MethodInfo(
                    classFile.getConstantPool(),
                    method.getName(),
                    ByteCodeUtils.makeDescriptor(method)
            );

            methodInfo.setAccessFlags(Modifier.FINAL | Modifier.PUBLIC
                    | (method.getModifiers() & ~(Modifier.PRIVATE
                    | Modifier.PROTECTED
                    | Modifier.ABSTRACT
                    | Modifier.NATIVE
                    | Modifier.SYNCHRONIZED)));

            methodInfo.setCodeAttribute(toMethodCode(classFile.getConstantPool(), method, methodIndex));
            classFile.addMethodInfo(methodInfo);
            ++methodIndex;
        }
    }

    /**
     * Create proxied method code in dynamic proxy class.
     *
     * @param constantPool target constant pool
     * @param method       target method
     * @param methodIndex  target method index from methods list field in dynamic proxy class
     * @return proxied method code which proxy method call to invocation handler.
     */
    private CodeAttribute toMethodCode(ConstantPool constantPool, Method method, int methodIndex) {
        int argsSize = method.getParameterCount();
        CodeAttribute codeAttribute = new CodeAttribute(constantPool, 0, argsSize + 2);

        codeAttribute.addAload(0);
        codeAttribute.addGetField(className, HANDLER_FIELD_NAME, HANDLER_TYPE);
        codeAttribute.addGetstatic(className, HOLDER_FIELD_NAME, HOLDER_FIELD_TYPE);
        codeAttribute.addIconst(methodIndex);
        codeAttribute.addInvokeInterface(List.class.getName(), "get", "(I)Ljava/lang/Object;", 2);
        codeAttribute.addCheckCast("java/lang/reflect/Method");
        makeParameterList(codeAttribute, method.getParameterTypes());
        codeAttribute.addInvokeInterface(
                MethodHandler.class.getName(),
                PROXY_INTERFACE_INVOKE_METHOD_NAME,
                PROXY_INTERFACE_INVOKE_METHOD_DESC,
                PROXY_INTERFACE_INVOKE_METHOD_DEFINITION_COUTN
        );

        Class retType = method.getReturnType();
        addUnWrapper(codeAttribute, retType);
        addReturn(codeAttribute, retType);

        return codeAttribute;
    }

    /**
     * Make invoke handler args array.
     *
     * @param code   target method code
     * @param params target arg types
     */
    private static void makeParameterList(CodeAttribute code, Class[] params) {
        int regno = 1;
        int argsSize = params.length;

        code.addIconst(argsSize);
        code.addAnewarray("java/lang/Object");
        for (int i = 0; i < argsSize; i++) {
            code.addOpcode(CodeAttribute.Opcode.DUP);
            code.addIconst(i);
            Class type = params[i];
            if (type.isPrimitive()) {
                regno = makeWrapper(code, type, regno);
            } else {
                code.addAload(regno);
                regno++;
            }

            code.addOpcode(CodeAttribute.Opcode.AASTORE);
        }
    }

    /**
     * Make wrapper for primitive types. Example: 2->new Integer(2)...
     *
     * @param code  target method code
     * @param type  primitive type class {@link Class}
     * @param regno arg index in args array
     * @return regno + {@link ProxyFactoryHelper#DATA_SIZE}
     */
    private static int makeWrapper(CodeAttribute code, Class type, int regno) {
        int index = typeIndex(type);
        String wrapper = WRAPPER_TYPES[index];

        code.addNew(wrapper);
        code.addOpcode(CodeAttribute.Opcode.DUP);
        addLoad(code, regno, type);
        code.addInvokeSpecial(wrapper, "",
                WRAPPER_DESC[index]);

        return regno + DATA_SIZE[index];
    }

    /**
     * Push n to local variable.
     *
     * @param code target method code
     * @param n    target variable
     * @param type primitive type class {@link Class}
     */
    private static void addLoad(CodeAttribute code, int n, Class type) {
        if (type.isPrimitive()) {
            if (type == Long.TYPE) {
                code.addLload(n);
            } else if (type == Float.TYPE) {
                code.addFload(n);
            } else if (type == Double.TYPE) {
                code.addDload(n);
            } else {
                code.addIload(n);
            }
        } else {
            code.addAload(n);
        }
    }

    /**
     * Add unwrap methods for return value. For primitive type int it will be {@link Integer#intValue()}.
     * Another add checkcast{@link CodeAttribute#addCheckCast(String)} instruction.
     *
     * @param code target method code
     * @param type method arg type
     */
    private static void addUnWrapper(CodeAttribute code, Class type) {
        if (type.isPrimitive()) {
            if (type == Void.TYPE) {
                code.addOpcode(CodeAttribute.Opcode.POP);
            } else {
                int index = typeIndex(type);
                String wrapper = WRAPPER_TYPES[index];
                code.addCheckCast(wrapper);
                code.addInvokeVirtual(wrapper,
                        UNWARP_METHODS[index],
                        UNWRAP_DESC[index]);
            }
        } else {
            code.addCheckCast(type.getName());
        }
    }

    /**
     * Add return instruction to method.
     *
     * @param code target method code
     * @param type arg type {@link Class}
     */
    private static void addReturn(CodeAttribute code, Class type) {
        if (type.isPrimitive()) {
            if (type == Long.TYPE) {
                code.addOpcode(CodeAttribute.Opcode.LRETURN);
            } else if (type == Float.TYPE) {
                code.addOpcode(CodeAttribute.Opcode.FRETURN);
            } else if (type == Double.TYPE) {
                code.addOpcode(CodeAttribute.Opcode.DRETURN);
            } else if (type == Void.TYPE) {
                code.addOpcode(CodeAttribute.Opcode.RETURN);
            } else {
                code.addOpcode(CodeAttribute.Opcode.IRETURN);
            }
        } else {
            code.addOpcode(CodeAttribute.Opcode.ARETURN);
        }
    }

    /**
     * Set interfaces.
     *
     * @param cf         target class file
     * @param interfaces target interfaces
     * @param proxyClass target proxy interface
     */
    private static void setInterfaces(ClassFile cf, Class[] interfaces, Class proxyClass) {
        Collection interfaceNames = new ArrayList<>();

        for (Class interfaceClass : interfaces) {
            interfaceNames.add(interfaceClass.getName());
        }
        interfaceNames.add(proxyClass.getName());

        cf.setInterfaces(interfaceNames);
    }

    /**
     * Interface represent uid generator which would generate unique uids.
     */
    private interface UIDGenerator {

        /**
         * This method will be called for generate next unique uid.
         *
         * @return next uid
         */
        int nextUID();
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy