实现选择性的ClassLoader

时间:2011-07-19 16:09:51

标签: java classloader instrumentation

我想在加载时检测类路径上某些类的字节码。由于这些是第三方库,我确切知道它们何时被加载。问题是我需要有选择地进行检测,即仅检测一些类。现在,如果我没有使用我的类加载器加载一个类但是使用它的父类,则将此父类设置为类类加载器,并且所有简洁类都由该父类加载,从而有效地使我的类加载器无法使用。所以我需要实现父级最后一个类加载器(参见How to put custom ClassLoader to use?)。

所以我需要自己加载课程。如果这些类是系统类(以“java”或“sun”开头),我将委托给父类。否则,我读取字节码并调用defineClass(name, byteBuffer, 0, byteBuffer.length);。但现在抛出java.lang.ClassNotFoundException: java.lang.Object

这是代码,任何评论都高度赞赏:

public class InstrumentingClassLoader extends ClassLoader {
private final BytecodeInstrumentation instrumentation = new BytecodeInstrumentation();

@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
    Class<?> result = defineClass(name);
    if (result != null) {
        return result;
    }
    result = findLoadedClass(name);
    if(result != null){
        return result;
    }
    result = super.findClass(name);
    return result;
}

private Class<?> defineClass(String name) throws ClassFormatError {
    byte[] byteBuffer = null;
    if (instrumentation.willInstrument(name)) {
        byteBuffer = instrumentByteCode(name);
    }
    else {
        byteBuffer = getRegularByteCode(name);
    }
    if (byteBuffer == null) {
        return null;
    }
    Class<?> result = defineClass(name, byteBuffer, 0, byteBuffer.length);
    return result;
}

private byte[] getRegularByteCode(String name) {
    if (name.startsWith("java") || name.startsWith("sun")) {
        return null;
    }
    try {
        InputStream is = ClassLoader.getSystemResourceAsStream(name.replace('.', '/') + ".class");
        ByteArrayOutputStream buffer = new ByteArrayOutputStream();
        int nRead;
        byte[] data = new byte[16384];
        while ((nRead = is.read(data, 0, data.length)) != -1) {
            buffer.write(data, 0, nRead);
        }
        buffer.flush();
        return buffer.toByteArray();
    } catch (IOException exc) {
        return null;
    }
}

private byte[] instrumentByteCode(String fullyQualifiedTargetClass) {
    try {
        String className = fullyQualifiedTargetClass.replace('.', '/');
        return instrumentation.transformBytes(className, new ClassReader(fullyQualifiedTargetClass));
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}
}

可以执行代码,例如用:

    InstrumentingClassLoader instrumentingClassLoader = new InstrumentingClassLoader();
    Class<?> changedClass = instrumentingClassLoader.loadClass(ClassLoaderTestSubject.class.getName());

ClassLoaderTestSubject应调用其他类,其中被调用的类是检测的目标,但ClassLoaderTestSubject本身不是......

2 个答案:

答案 0 :(得分:1)

我建议您使用常规的类加载器策略,即父级优先。但是,将要修改的所有类放入单独的jar文件中,不要将其添加到应用程序的类路径中。使用扩展URL类加载器的类加载器实例化这些类,并且知道在其他位置搜索jar。在这种情况下,所有JDK类都将自动知道,您的代码将更简单。您不必“思考”是否要对该类进行检测:如果它没有被父类加载器加载,则必须对您的类进行检测。

答案 1 :(得分:1)

愚蠢的错误。父类加载器不是继承层次结构中的父类。它是构造函数的父级。所以正确的代码如下所示:

public InstrumentingClassLoader() {
    super(InstrumentingClassLoader.class.getClassLoader());
    this.classLoader = InstrumentingClassLoader.class.getClassLoader();
}

@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
    [... as above ...]
    result = classLoader.loadClass(name);
    return result;
}