package org.hotswap.agent.plugin.jvm; import org.hotswap.agent.javassist.ClassPool; import org.hotswap.agent.javassist.CtClass; import org.hotswap.agent.javassist.NotFoundException; import org.hotswap.agent.logging.AgentLogger; import java.io.File; import java.lang.reflect.Method; import java.util.*; /** * Info about anonymous classes. * <p/> * This class will on construction search for all anonymous classes of the main class and calculate * superclass, interfaces, all methods signature and all fields signature. Depending on used constructor * this is done via reflection from ClassLoader (current loaded state) or from ClassPool via javaassist. * Note that ClassPool uses LoadClassPath on the ClassLoader and hence are resources resolved via the * ClassLoader. Javaasist resolves the resource and returns bytcode as is in the resource file. * <p/> * Use mapPreviousState() to create compatible transition mapping between old state and new state. This mapping * is then used by plugin to swap class bytecoded to retain hotswap changes compatible * * @author Jiri Bubnik */ public class AnonymousClassInfos { private static AgentLogger LOGGER = AgentLogger.getLogger(AnonymousClassInfos.class); // start indexing hotswap created synthetic anonymous classes from this index to avoid collision with existing public static final int UNIQUE_CLASS_START_INDEX = 10000; // how many milliseconds is delta is considered as same time in modification check private static final long ALLOWED_MODIFICATION_DELTA = 100; // counter to create uniqueue class name static int uniqueClass = UNIQUE_CLASS_START_INDEX; // previous state AnonymousClassInfos previous; // calculated transitions Map<AnonymousClassInfo, AnonymousClassInfo> compatibleTransitions; // mo long lastModifiedTimestamp = 0; // the main class String className; // zero based index list of anonymous classes List<AnonymousClassInfo> anonymousClassInfoList = new ArrayList<AnonymousClassInfo>(); /** * Create info of the current state from the classloader via reflection. * * @param classLoader classloader to use * @param className main class */ public AnonymousClassInfos(ClassLoader classLoader, String className) { this.className = className; try { // reflective call to check already loaded class (not to load a new one) Method m = ClassLoader.class.getDeclaredMethod("findLoadedClass", new Class[]{String.class}); m.setAccessible(true); int i = 1; while (true) { Class anonymous = (Class) m.invoke(classLoader, className + "$" + i); if (anonymous == null) break; anonymousClassInfoList.add(i - 1, new AnonymousClassInfo(anonymous)); i++; } } catch (Exception e) { throw new Error("Unexpected error in checking loaded classes", e); } } /** * Create info of the new state from the classPool via javassist. * * @param classPool classPool to resolve class files * @param className main class */ public AnonymousClassInfos(ClassPool classPool, String className) { this.className = className; lastModifiedTimestamp = lastModified(classPool, className); // search for declared classes in new state to skip obsolete anonymous inner classes on filesystem List<CtClass> declaredClasses; try { CtClass ctClass = classPool.get(className); declaredClasses = Arrays.asList(ctClass.getNestedClasses()); } catch (NotFoundException e) { throw new IllegalArgumentException("Class " + className + " not found."); } int i = 1; while (true) { try { CtClass anonymous = classPool.get(className + "$" + i); if (!declaredClasses.contains(anonymous)) break; // skip obsolete classes anonymousClassInfoList.add(i - 1, new AnonymousClassInfo(anonymous)); i++; } catch (NotFoundException e) { // up to first not found class break; } catch (Exception e) { throw new Error("Unable to create AnonymousClassInfo definition for class " + className + "$i", e); } } LOGGER.trace("Anonymous class '{}' scan finished with {} classes found", className, i - 1); } /** * Search for a mapping between previous and nwe anonymous classes. * * @return map previous -> new. If no mapping to previous exists, synthetic class name is created. */ private void calculateCompatibleTransitions() { compatibleTransitions = new HashMap<AnonymousClassInfo, AnonymousClassInfo>(); // create a copy to remove resolved items List<AnonymousClassInfo> previousInfos = new ArrayList<AnonymousClassInfo>(previous.anonymousClassInfoList); List<AnonymousClassInfo> currentInfos = new ArrayList<AnonymousClassInfo>(anonymousClassInfoList); // previous classes are discarded and cannot be used if (previousInfos.size() > currentInfos.size()) { if (currentInfos.size() == 0) previousInfos.clear(); else previousInfos = previousInfos.subList(0, currentInfos.size()); } // try to match - exact, than signatures (enclosing method may change), than class signature searchForMappings(compatibleTransitions, previousInfos, currentInfos, new AnonymousClassInfoMatcher() { @Override public boolean match(AnonymousClassInfo previous, AnonymousClassInfo current) { return previous.matchExact(current); } }); searchForMappings(compatibleTransitions, previousInfos, currentInfos, new AnonymousClassInfoMatcher() { @Override public boolean match(AnonymousClassInfo previous, AnonymousClassInfo current) { return previous.matchSignatures(current); } }); searchForMappings(compatibleTransitions, previousInfos, currentInfos, new AnonymousClassInfoMatcher() { @Override public boolean match(AnonymousClassInfo previous, AnonymousClassInfo current) { return previous.matchClassSignature(current); } }); // how many anonymous classes will be defined int newDefinitionCount = anonymousClassInfoList.size(); // last myClass$index int lastAnonymousClassIndex = previous.anonymousClassInfoList.size(); // not matched for (AnonymousClassInfo currentNotMatched : currentInfos) { if (lastAnonymousClassIndex < newDefinitionCount) { // free anonymous class available - this will be registered with onDefine event, +1 because one based index name compatibleTransitions.put(new AnonymousClassInfo(className + "$" + (lastAnonymousClassIndex + 1)), currentNotMatched); lastAnonymousClassIndex++; } else { compatibleTransitions.put(new AnonymousClassInfo(className + "$" + uniqueClass++), currentNotMatched); } } if (LOGGER.isLevelEnabled(AgentLogger.Level.TRACE)) { for (Map.Entry<AnonymousClassInfo, AnonymousClassInfo> mapping : compatibleTransitions.entrySet()) { LOGGER.trace("Transition {} => {}", mapping.getKey().getClassName(), mapping.getValue().getClassName()); } } } /** * Iterate through both lists and find matching anonymous classes using matcher. * Found matches are removed from previous and current lists and added to transitions. */ private void searchForMappings(Map<AnonymousClassInfo, AnonymousClassInfo> transitions, List<AnonymousClassInfo> previousInfos, List<AnonymousClassInfo> currentInfos, AnonymousClassInfoMatcher matcher) { for (ListIterator<AnonymousClassInfo> previousIt = previousInfos.listIterator(); previousIt.hasNext(); ) { AnonymousClassInfo previous = previousIt.next(); for (ListIterator<AnonymousClassInfo> currentIt = currentInfos.listIterator(); currentIt.hasNext(); ) { AnonymousClassInfo current = currentIt.next(); // found and resolved if (matcher.match(previous, current)) { transitions.put(previous, current); previousIt.remove(); currentIt.remove(); break; } } } } /** * Returns stored info of an anonymous class * * @param className class name of the anonymous class (Should be in the form of MyClass$3) * @return class info x null */ public AnonymousClassInfo getAnonymousClassInfo(String className) { for (AnonymousClassInfo info : anonymousClassInfoList) { if (className.equals(info.getClassName())) { return info; } } return null; } /** * Set previous class info state and calculate compatible transitions. * Usually new state is created from classpool, while old state from classloader. * * @param previousAnonymousClassInfos previous state */ public void mapPreviousState(AnonymousClassInfos previousAnonymousClassInfos) { this.previous = previousAnonymousClassInfos; // hold only one state back previousAnonymousClassInfos.previous = null; // calculate the mapping calculateCompatibleTransitions(); } /** * Return true, if last modification timestamp is same as current timestamp of className. * * @param classPool classPool to check className file * @return true if is current */ public boolean isCurrent(ClassPool classPool) { return lastModifiedTimestamp >= lastModified(classPool, className) - ALLOWED_MODIFICATION_DELTA; } // get timestamp on the main class file private long lastModified(ClassPool classPool, String className) { String file = classPool.find(className).getFile(); return new File(file).lastModified(); } // matcher helper private interface AnonymousClassInfoMatcher { public boolean match(AnonymousClassInfo previous, AnonymousClassInfo current); } /** * Returns calculated compatible transitions. * * @return calculated compatible transitions. */ public Map<AnonymousClassInfo, AnonymousClassInfo> getCompatibleTransitions() { return compatibleTransitions; } /** * Find compatible transition class name. * * @param className name of existing class (old) * @return name of compatible new class that should replace old class. Can be null if no compatible class found. */ public String getCompatibleTransition(String className) { for (Map.Entry<AnonymousClassInfo, AnonymousClassInfo> transition : compatibleTransitions.entrySet()) { if (transition.getKey().getClassName().equals(className)) return transition.getValue().getClassName(); } return null; } }