Package org.hotswap.agent.plugin.jvm

Source Code of org.hotswap.agent.plugin.jvm.AnonymousClassPatchPlugin

package org.hotswap.agent.plugin.jvm;

import org.hotswap.agent.annotation.Init;
import org.hotswap.agent.annotation.LoadEvent;
import org.hotswap.agent.annotation.OnClassLoadEvent;
import org.hotswap.agent.annotation.Plugin;
import org.hotswap.agent.javassist.*;
import org.hotswap.agent.logging.AgentLogger;
import org.hotswap.agent.util.HotswapTransformer;
import org.hotswap.agent.util.classloader.*;

import java.io.IOException;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
import java.util.HashMap;
import java.util.Map;
import java.util.WeakHashMap;

/**
* Class names MyClass$1, MyClass$2 are created in the order as anonymous class appears in the source code.
* After anonymous class insertion/deletion the indexes are shifted producing not compatible hot swap.
* <p/>
* This patch will create class state info before the change (from current ClassLoader via reflection) and
* after the change (from filesystem using javassist) find all compatible transitions.
* <p/>
* <p/>
* For example if you exchange order the anonymous class appears in the source code, Transition may
* produce something like:<ul>
* <li>MyClass$1 -> MyClass$2</li>
* <li>MyClass$2 -> MyClass$3</li>
* <li>MyClass$3 -> MyClass$1</li>
* </ul>
* Then the transformation will behave:<ul>
* <li>When the class MyClass$1 is hot swapped, the bytecode from MyClass$2 is returned (and renamed to MyClass$1)</li>
* <li>When the class MyClass$2 is hot swapped, the bytecode from MyClass$3 is returned (and renamed to MyClass$2)</li>
* <li>When the class MyClass$3 is hot swapped, the bytecode from MyClass$1 is returned (and renamed to MyClass$3)</li>
* <li>When the class MyClass is hot swapped, all occurences of MyClass$1 are exchanged for MyClass$3</li>
* <li>                         , all occurences of MyClass$2 are exchanged for MyClass$1</li>
* <li>                         , all occurences of MyClass$3 are exchanged for MyClass$2</li>
* </ul>
* <p/>
* Swap may produce even to not compatible change. Consider existing MyClass$1 and MyClass$2, then MyClass$1
* is removed. Then hotswap is called only on MyClass$1, which contains different class to MyClass$2. Then
* MyClass$1 is on hotswap replaced with empty implementation and new class MyClass$1000x is created to
* contain code from the new MyClass$1 (class compatible with old MyClass$2). Not that because this is not
* a true hotswap, old existing instances of MyClass$1 are updated to an empty class, not the new one.
* When calling a method on this class, AbstractErrorMethod is thrown (this should be replaced to some
* more clear error in the future).
*
* @author Jiri Bubnik
*/
@Plugin(name = "AnonymousClassPatch",
        description = "Swap anonymous inner class names to avoid not compatible changes.",
        testedVersions = {"DCEVM"})
public class AnonymousClassPatchPlugin {
    private static AgentLogger LOGGER = AgentLogger.getLogger(AnonymousClassPatchPlugin.class);

    @Init
    static HotswapTransformer hotswapTransformer;

    // Map ClassLoader -> (className -> infos about inner/local anonymous classes)
    // This caches information for one hotswap on main class and all anonymous classes
    private static Map<ClassLoader, Map<String, AnonymousClassInfos>> anonymousClassInfosMap =
            new WeakHashMap<ClassLoader, Map<String, AnonymousClassInfos>>();

    /**
     * Replace an anonymous class with an compatible change (from another class according to state info).
     * If no compatible class exists, replace with compatible empty implementation.
     */
    @OnClassLoadEvent(classNameRegexp = ".*\\$\\d+", events = LoadEvent.REDEFINE)
    public static CtClass patchAnonymousClass(ClassLoader classLoader, ClassPool classPool, String className, Class original)
            throws IOException, NotFoundException, CannotCompileException {

        String javaClass = className.replaceAll("/", ".");
        String mainClass = javaClass.replaceAll("\\$\\d+$", "");

        // skip synthetic classes
        if (classPool.find(className) == null)
            return null;

        AnonymousClassInfos info = getStateInfo(classLoader, classPool, mainClass);

        String compatibleName = info.getCompatibleTransition(javaClass);

        if (compatibleName != null) {
            LOGGER.debug("Anonymous class '{}' - replacing with class file {}.", javaClass, compatibleName);
            CtClass ctClass = classPool.get(compatibleName);
            ctClass.replaceClassName(compatibleName, javaClass);
            return ctClass;
        } else {
            LOGGER.debug("Anonymous class '{}' - not compatible change is replaced with empty implementation.", javaClass, compatibleName);

            // replace current class with empty implementation (to avid not compatible exception)
            CtClass ctClass = classPool.makeClass(javaClass);
            // replace superclass
            ctClass.setSuperclass(classPool.get(original.getSuperclass().getName()));
            // replace interfaces
            Class[] originalInterfaces = original.getInterfaces();
            CtClass[] interfaces = new CtClass[originalInterfaces.length];
            for (int i = 0; i < originalInterfaces.length; i++)
                interfaces[i] = classPool.get(originalInterfaces[i].getName());
            ctClass.setInterfaces(interfaces);

            return ctClass;

            // TODO provide implementation that will throw an exception
            //   throw new IllegalAccessError("HOTSWAP AGENT - obsolete anonymous class. This class has been
            // replaced with a new version. Automatic update of old instances containing references to obsolete
            // class is not supported yet.");
        }
    }

    private static boolean isHotswapAgentSyntheticClass(String compatibleName) {
        String anonymousClassIndexString = compatibleName.replaceAll("^.*\\$(\\d+)$", "$1");
        try {
            long anonymousClassIndex = Long.valueOf(anonymousClassIndexString);
            return anonymousClassIndex >= AnonymousClassInfos.UNIQUE_CLASS_START_INDEX;
        } catch (NumberFormatException e) {
            throw new IllegalArgumentException(compatibleName + " is not in a format of className$i");
        }
    }

    // new anonymous class, not covered by hotswap (patchAnonymousClass) - register custom transformer and
    // on event swap and unregister.
    private static void registerReplaceOnLoad(final String newName, final CtClass anonymous) {
        hotswapTransformer.registerTransformer(null, newName, new ClassFileTransformer() {
            @Override
            public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
                LOGGER.trace("Anonymous class '{}' - replaced.", newName);
                hotswapTransformer.removeTransformer(newName, this);
                try {
                    return anonymous.toBytecode();
                } catch (Exception e) {
                    LOGGER.error("Unable to create bytecode of class {}.", e, anonymous.getName());
                    return null;
                }
            }
        });
    }

    /**
     * If class contains anonymous classes, rename class references to compatible transition classes.
     * <p/>
     * If the transitioned class is not loaded by hotswap replace, catch class define event to do
     * the replacement.
     * <p/>
     * Define new synthetic classes for not compatible changes.
     */
    @OnClassLoadEvent(classNameRegexp = ".*", events = LoadEvent.REDEFINE)
    public static byte[] patchMainClass(String className, ClassPool classPool,
                                        ClassLoader classLoader, ProtectionDomain protectionDomain) throws IOException, CannotCompileException, NotFoundException {
        String javaClassName = className.replaceAll("/", ".");

        // check if has anonymous classes
        if (!ClassLoaderHelper.isClassLoaded(classLoader, javaClassName + "$1"))
            return null;


        AnonymousClassInfos stateInfo = getStateInfo(classLoader, classPool, javaClassName);
        Map<AnonymousClassInfo, AnonymousClassInfo> transitions = stateInfo.getCompatibleTransitions();

        ClassMap replaceClassNameMap = new ClassMap();
        for (Map.Entry<AnonymousClassInfo, AnonymousClassInfo> entry : transitions.entrySet()) {
            String compatibleName = entry.getKey().getClassName();
            String newName = entry.getValue().getClassName();

            if (!newName.equals(compatibleName)) {
                replaceClassNameMap.put(newName, compatibleName);
                LOGGER.trace("Class '{}' replacing '{}' for '{}'", javaClassName, newName, compatibleName);
            }

            // new class (not known by current classloader)
            if (isHotswapAgentSyntheticClass(compatibleName)) {
                LOGGER.debug("Anonymous class '{}' not comatible and is replaced with synthetic class '{}'", newName, compatibleName);
                // define contens of new class as new unique "myClass$hotswapAgentXx" class
                CtClass anonymous = classPool.get(newName);
                anonymous.replaceClassName(newName, compatibleName);
                anonymous.toClass(classLoader, protectionDomain);
            } else if (!ClassLoaderHelper.isClassLoaded(classLoader, newName)) {
                CtClass anonymous = classPool.get(compatibleName);
                anonymous.replaceClassName(compatibleName, newName);

                // is a new class of standard type myClass$x -> replace on load
                LOGGER.debug("Anonymous class '{}' - will be replaced from class file {}.", newName, compatibleName);
                registerReplaceOnLoad(newName, anonymous);
            }
        }

        // rename all class names according to the map
        CtClass ctClass = classPool.get(javaClassName);
        ctClass.replaceClassName(replaceClassNameMap);

        LOGGER.reload("Class '{}' has been enhanced with anonymous classes for hotswap.", className);
        return ctClass.toBytecode();
    }

    /**
     * Calculate anonymous class new/previous state info from current classloader/filesystem.
     * It checks, if state info is current via modification date on the main class file.
     * <p/>
     * <p/>Note: Synchronized may be too restrictive, in case of performance issues consider synchronization
     * only on a classloader and class.
     */
    private static synchronized AnonymousClassInfos getStateInfo(ClassLoader classLoader, ClassPool classPool, String className) {
        Map<String, AnonymousClassInfos> classInfosMap = getClassInfosMapForClassLoader(classLoader);

        AnonymousClassInfos infos = classInfosMap.get(className);

        if (infos == null || !infos.isCurrent(classPool)) {
            if (infos == null)
                LOGGER.trace("Creating new infos for className {}", className);
            else
                LOGGER.trace("Creating new infos, current is obsolete for className {}", className);

            infos = new AnonymousClassInfos(classPool, className);
            infos.mapPreviousState(new AnonymousClassInfos(classLoader, className));
            classInfosMap.put(className, infos);
        } else {
            LOGGER.trace("Returning existing infos for className {}", className);
        }
        return infos;
    }

    /**
     * Return classInfos for a classloader. Hold known classloaders in weak hash map.
     */
    private static Map<String, AnonymousClassInfos> getClassInfosMapForClassLoader(ClassLoader classLoader) {
        Map<String, AnonymousClassInfos> classInfosMap = anonymousClassInfosMap.get(classLoader);
        if (classInfosMap == null) {
            synchronized (classLoader) {
                if (!anonymousClassInfosMap.containsKey(classLoader)) {
                    classInfosMap = new HashMap<String, AnonymousClassInfos>();
                    anonymousClassInfosMap.put(classLoader, classInfosMap);
                }
            }
        }
        return classInfosMap;
    }
}
TOP

Related Classes of org.hotswap.agent.plugin.jvm.AnonymousClassPatchPlugin

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.