How to determine the loading order of classes when they are introduced into SystemClassLoader?

For my toolkit of tools, I want to provide the packaging class ClassLoader, which is used to run the main method after certain classes are instrumental. My ClassLoader should load instrumental versions of certain classes. But for Jetty and JUnit, this approach is severely limited because they create their own class hierarchy hierarchy.

I do not want to pass VM arguments, so I cannot change SystemClassLoader. But I can force-feed it with my classes, using reflection, to make ClassLoader.defineClass(String, byte[], int, int) public.

 ClassLoader scl = ClassLoader.getSystemClassLoader(); Method defineClass = ClassLoader.class.getDeclaredMethod( "defineClass", String.class, byte[].class, int.class, int.class); defineClass.setAccessible(true); for (String binaryName : classNamesToLoad) { byte[] bytecode = this.declaredClasses.get(binaryName); defineClass.invoke(scl, binaryName, bytecode, 0, bytecode.length); } defineClass.setAccessible(false); 

This is just great - but one problem remains: if some of my classes inherit or contain other classes, they need to be loaded in the correct order, because SystemClassLoader loads all the classes that the current one depends on, and the non-instrumental version will load.

Here is an example with some (poorly named) classes and their loading order:

 A AA extends BA B BA extends BC BC 

need to load in order

 B BC BA A AA 

if I want to download only the instrumental version.

Is there a simple way out - for example, the "setSystemClassLoader" method, which I have not noticed yet?

A workaround with which I would not need to manipulate SystemClassLoader?

Or do I really need to do a complete analysis of the transitive dependency, starting with the classes I want to load in order to determine the correct order (and in this case: is there a โ€œprior artโ€ that I can work with)?

Thanks!

+4
source share
2 answers

There seems to be no relation to transitive dependency analysis.

I solved it this way and I really hope that someone can benefit from this implementation:

 import java.io.IOException; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import org.objectweb.asm.ClassReader; import org.objectweb.asm.tree.ClassNode; public class DependencyDetector { private static class Node implements Comparable<Node> { private final String binaryName; private final Node[] imports; private final int score; private Node(String binaryName, Node...imports) { this.binaryName = binaryName; this.imports = imports; this.score = calculateScore(imports); } public int compareTo(Node o) { return score - o.score; } private int calculateScore(Node...imports) { int newScore = 0; for (Node n : imports) { if (n.score >= newScore) { newScore = n.score + 1; } } return newScore; } } private Map<String, Node> nodes = new HashMap<String, Node>(); public DependencyDetector add(ClassNode node) { Node n = nodes.get(node.name); if (n == null) { n = createNode(node); } return this; } private Node createNode(ClassNode node) { String binaryName = node.name; String[] importNames = extractImportedBinaryNames(node); Node[] imports = new Node[importNames.length]; for (int i = 0; i < imports.length; i++) { String importName = importNames[i]; Node imp = nodes.get(importName); if (imp == null) { ClassNode cn = new ClassNode(); String path = importName.replace('.', '/') + ".class"; try { new ClassReader( ClassLoader.getSystemResourceAsStream(path) ).accept(cn, ClassReader.SKIP_CODE); } catch (IOException e) { throw new RuntimeException( "could not read class " + importName); } imp = createNode(cn); nodes.put(importName, imp); } imports[i] = imp; } Node result = new Node(binaryName, imports); nodes.put(binaryName, result); return result; } private String[] extractImportedBinaryNames(ClassNode node) { String binaryName = node.name; ArrayList<String> nodesToAdd = new ArrayList<String>(); int endOfOuter = binaryName.lastIndexOf('$'); if (endOfOuter >= 0) { nodesToAdd.add(binaryName.substring(0, endOfOuter)); } if (node.superName != null) { nodesToAdd.add(node.superName); } if (node.interfaces != null) { for (String interf : (List<String>) node.interfaces) { if (interf != null) { nodesToAdd.add(interf); } } } return nodesToAdd.toArray(new String[nodesToAdd.size()]); } public String[] getClassesToLoad(String...binaryNames) { String[] classNames = binaryNames != null && binaryNames.length > 0 ? binaryNames.clone() : nodes.keySet().toArray(new String[nodes.size()]); ArrayDeque<Node> dependencyQueue = new ArrayDeque<Node>(); for (String className : classNames) { Node node = nodes.get(className.replace('.', '/')); dependencyQueue.add(node); if (node == null) { throw new RuntimeException( "Class " + className + " was not registered"); } } HashMap<String, Node> dependencyMap = new HashMap<String, Node>(); while (!dependencyQueue.isEmpty()) { Node node = dependencyQueue.removeFirst(); dependencyMap.put(node.binaryName, node); for (Node i : node.imports) { dependencyQueue.addLast(i); } } ArrayList<Node> usedNodes = new ArrayList<Node>(dependencyMap.values()); Collections.sort(usedNodes); String[] result = new String[usedNodes.size()]; int i = 0; for (Node n : usedNodes) { result[i++] = n.binaryName.replace('/', '.'); } return result; } public boolean contains(String binaryName) { return nodes.containsKey(binaryName.replace('.', '/')); } } 

It is used as follows: on DependencyDetector you call add(ClassNode) to add the ClassNode and all its dependencies (all classes that it extends, implements, or contains). When you finish creating the dependency tree, you call getClassesToLoad() to get all the dependencies as String[] containing the binary names in the required order. You can also simply request a subset of all added classes and their dependencies by specifying binary names as the getClassesToLoad(...) parameter.

Now that I have the tool classes, I also add a ClassNode to the DependencyDetector and can get everything I need to pass it to a method like this:

 /** * load the specified classes (or all instrumented classes) * and all their dependencies with the specified ClassLoader. * @param loader * @param binaryNames binary names of all classes you want to load * - none loads all instrumented classes */ public void loadIntoClassLoader(ClassLoader loader, String...binaryNames) { final String[] classNamesToLoad = dependencies.getClassesToLoad(binaryNames); Method defineClass = null; Method findLoadedClass = null; try { // crack ClassLoader wide open and force-feed it with our classes defineClass = ClassLoader.class.getDeclaredMethod( "defineClass", String.class, byte[].class, int.class, int.class); defineClass.setAccessible(true); findLoadedClass = ClassLoader.class.getDeclaredMethod( "findLoadedClass", String.class); findLoadedClass.setAccessible(true); for (String binaryName : classNamesToLoad) { if (!binaryName.startsWith("java.")) { if (findLoadedClass.invoke(loader, binaryName) == null) { byte[] bytecode = getBytecode(binaryName); defineClass.invoke(loader, binaryName, bytecode, 0, bytecode.length); } else if (declaredClasses.containsKey(binaryName)) { throw new RuntimeException( "Class " + binaryName + " was already loaded, " + "it must not be redeclared"); } } } } catch (Exception e) { e.printStackTrace(); throw new RuntimeException( "could not load classes into ClassLoader", e); } finally { rehideMethod(findLoadedClass); rehideMethod(defineClass); } } private void rehideMethod(Method m) { if (m != null) { try { m.setAccessible(false); } catch (Exception e) { } } } 

which relies on

 private final DependencyDetector dependencies = new DependencyDetector(); private final Map<String, byte[]> declaredClasses = new HashMap<String, byte[]>(); private byte[] getBytecode(String binaryName) { byte[] bytecode = declaredClasses.get(binaryName); if (bytecode == null) { // asBytes loads the class as byte[] bytecode = asBytes(binaryName.replace('.', '/') + ".class"); } return bytecode; } 

To a large extent, this works great in any situation that I have encountered so far.

+1
source

Use an instance to validate an object - this is class membership.

 if (aAnimal instanceof Fish){ Fish fish = (Fish)aAnimal; fish.swim(); } else if (aAnimal instanceof Spider){ Spider spider = (Spider)aAnimal; spider.crawl(); } 
0
source

All Articles