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, CtClass ctClass, 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 // TODO: it could be done via classPool but it doesn't work // 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; } }