自定义 Java 类装入器不用于装入依赖项?示例子第一类加载器更高级的类加载

2022-09-02 21:19:29

我一直在尝试设置一个自定义类装入器,该类加载器拦截类以打印出哪些类正在加载到应用程序中。类装入器如下所示

public class MyClassLoader extends ClassLoader {
    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        System.out.println("Loading: " + name);
        return super.loadClass(name);
    }
}     

它只是吐出它加载的所有类的名称。但是,当我尝试运行一些代码时,

import org.python.util.PythonInterpreter;
public class Scripts {
    public String main(){

        PythonInterpreter p = new PythonInterpreter();
        p.exec("print 'Python ' + open('.gitignore').read()");

        return "Success! Nothing broke";
    }
}

通过

MyClassLoader bcl = new MyClassLoader();
Class c = bcl.loadClass("Scripts");

Method m = c.getMethod("main");
String result = (String) m.invoke(c.getConstructor().newInstance());

它打印出来

Loading: Scripts
Loading: java.lang.Object
Loading: java.lang.String
Loading: org.python.util.PythonInterpreter
Python build/
.idea/*
*.iml
RESULT: Success! Nothing broke

这似乎很奇怪。 不是一个简单的类,它依赖于包中的一大堆其他类。这些类显然正在加载,因为'd python代码能够做一些事情并读取我的文件。但是,由于某种原因,这些类不是由加载 的类加载器加载的。org.python.util.PythonInterpreterorg.python.utilexecPythonInterpreter

为什么?我的印象是,用于加载类的类装入器将用于装入 所需的所有其他类,但这显然不会在这里发生。这个假设是错误的吗?如果是,我如何设置它,以便所有可传递的依赖项都由我的类加载器加载?CCC

编辑:

一些使用的实验,有人建议这样做。我修改了委派:URLClassLoaderloadClass()

try{
    byte[] output = IOUtils.toByteArray(this.getResourceAsStream(name));
    return instrument(defineClass(name, output, 0, output.length));
}catch(Exception e){
    return instrument(super.loadClass(name));
}

以及使MyClassLoader子类URLClassLoader而不是普通的ClassLoader,通过以下方式获取URL:

super(((URLClassLoader)ClassLoader.getSystemClassLoader()).getURLs());

但这似乎不是正确的事情。特别是,对于我请求的所有类,甚至是像Jython lib这样的非系统类,都会向我抛出null。getResourceAsStream()


答案 1

类加载的基础知识

有两个主要位置可以扩展类装入器以更改类装入方式:

  • findClass(字符串名称) - 当您想要查找具有通常父级第一个委派的类时,将重写此方法。
  • loadClass(字符串名称,布尔解析) - 如果要更改类装入委派的完成方式,请重写此方法。

但是,类只能来自 java.lang.ClassLoader 提供的最终定义类(...) 方法。由于您希望捕获所有已加载的类,因此我们需要重写 loadClass( String, boolean ) 并使用调用来定义其中的某个位置的 class(...)。

注意:在 defineClass(...) 方法中,有一个 JNI 绑定到 JVM 的本机端。在该代码中,有一个对 java.* 包中的类的检查。它只允许系统类装入器装入这些类。这可以防止您弄乱Java本身的内部结构。

示例子第一类加载器

这是您尝试创建的类加载器的一个非常简单的实现。它假定所需的所有类都可用于父类装入器,因此它仅使用父类作为类字节的源。为了简洁起见,此实现使用Apache Commons IO,但它可以很容易地删除。

import java.io.IOException;
import java.io.InputStream;

import static org.apache.commons.io.IOUtils.toByteArray;
import static org.apache.commons.io.IOUtils.closeQuietly;
...
public class MyClassLoader
  extends ClassLoader {
  MyClassLoaderListener listener;

  MyClassLoader(ClassLoader parent, MyClassLoaderListener listener) {
    super(parent);
    this.listener = listener;
  }

  @Override
  protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException {
    // respect the java.* packages.
    if( name.startsWith("java.")) {
      return super.loadClass(name, resolve);
    }
    else {
      // see if we have already loaded the class.
      Class<?> c = findLoadedClass(name);
      if( c != null ) return c;

      // the class is not loaded yet.  Since the parent class loader has all of the
      // definitions that we need, we can use it as our source for classes.
      InputStream in = null;
      try {
        // get the input stream, throwing ClassNotFound if there is no resource.
        in = getParent().getResourceAsStream(name.replaceAll("\\.", "/")+".class");
        if( in == null ) throw new ClassNotFoundException("Could not find "+name);

        // read all of the bytes and define the class.
        byte[] cBytes = toByteArray(in);
        c = defineClass(name, cBytes, 0, cBytes.length);
        if( resolve ) resolveClass(c);
        if( listener != null ) listener.classLoaded(c);
        return c;
      } catch (IOException e) {
        throw new ClassNotFoundException("Could not load "+name, e);
      }
      finally {
        closeQuietly(in);
      }
    }
  }
}

这是一个简单的侦听器接口,用于监视类加载。

public interface MyClassLoaderListener {
  public void classLoaded( Class<?> c );
}

然后,您可以创建一个新的 MyClassLoader 实例,以当前类装入器作为父级,并在装入类时对其进行监视。

MyClassLoader classLoader = new MyClassLoader(this.getClass().getClassLoader(), new MyClassLoaderListener() {
  public void classLoaded(Class<?> c) {
    System.out.println(c.getName());
  }
});
classLoader.loadClass(...);

这将在最一般的情况下工作,并允许您在加载类时收到通知。但是,如果这些类中的任何一个创建了自己的子 first 类装入器,则它们可以绕过此处添加的通知代码。

更高级的类加载

要真正捕获正在加载的类,即使子类装入器覆盖 loadClass(String,布尔值),您也必须在正在加载的类和它们可能对 ClassLoader.defineClass(...) 进行的任何调用之间插入代码。为此,您必须开始使用ASM等工具进行字节代码重写。我在GitHub上有一个名为Chlorin的项目,它使用此方法重写java.net.URL构造函数调用。如果你对在加载时弄乱类感到好奇,我会检查一下这个项目。


答案 2

如果要在加载类时打印它们,那么在 JVM 上打开 verbose:class 选项怎么样?

java -verbose:class your.class.name.here

要回答您的直接问题:

为什么?我的印象是,用于加载类 C 的类装入器将用于装入 C 所需的所有其他类,但这显然不会在这里发生。这个假设是错误的吗?如果是,我如何设置它,以便C的所有传递依赖项都由我的类加载器加载?

搜索类加载器时,搜索是从叶类加载器到根执行的,当Java确定必须加载一个新类时,它从类加载器树的向下执行到启动类解析的叶。

为什么?请考虑您的自定义类是否要从 Java 标准库中加载某些内容。正确的答案是,这应该由系统类加载器加载,以便可以最大程度地共享类。特别是当您考虑到正在加载的类可能会加载更多的类时。

这也解决了这样一个问题,即最终可能会将多个系统类实例加载到不同的类加载器中 - 每个实例都具有相同的完全限定名。编辑类将在其类加载器中正确解析。但是,有两个问题。

  1. 假设我们有两个字符串实例,并且 . 并且如果 和 在不同的类加载器中实例化,则不成立。这将导致可怕的问题。aba.getClass().isInstance(b)a.getClass() == b.getClass()ab
  2. 单例:它们不是单例 - 每个类加载器可以有一个。

结束编辑

另一个观察结果:就像您设置了一个 ClassLoader 来专门从中加载类一样,解释器通常自己创建 ClassLoader 实例,他们将解释环境和脚本加载到这些实例中。这样,如果脚本发生更改,则可以删除 ClassLoader(以及随之删除脚本),并在新的 ClassLoader 中重新加载。EJB 和 Servlet 也使用这个技巧。