【问题标题】:Custom Java classloader not being used to load dependencies?自定义 Java 类加载器未用于加载依赖项?
【发布时间】:2012-10-30 04:52:14
【问题描述】:

我一直在尝试设置一个自定义类加载器,它拦截类以打印出哪些类正在加载到应用程序中。类加载器看起来像这样

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

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

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

编辑:

建议使用URLClassLoader 进行一些实验。我在loadClass()修改了委托:

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());

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

【问题讨论】:

  • 你为什么不直接调试它;)
  • 这就是我一直在尝试做的!但是关于类加载过程的文档很少,实际操作是不透明和神秘的......

标签: java class jvm metaprogramming classloader


【解决方案1】:

类加载的基础知识

有两个主要的地方可以扩展类加载器来改变类的加载方式:

  • findClass(String name) - 当你想重写这个方法时 找到一个具有通常父母优先委托的班级。
  • loadClass(String name, boolean resolve) - 当你想改变时覆盖这个方法 类加载委托的完成方式。

但是,类只能来自 java.lang.ClassLoader 提供的最终 defineClass(...) 方法。由于您想捕获所有已加载的类,因此我们需要重写 loadClass(String, boolean) 并在其中某处调用 defineClass(...)。

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

一个子类加载器示例

这是您尝试创建的 ClassLoader 的一个非常简单的实现。它假定您需要的所有类都可用于父类加载器,因此它只使用父类作为类字节的源。为了简洁起见,此实现使用 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(...);

这适用于最一般的情况,并允许您在加载类时收到通知。但是,如果这些类中的任何一个创建了自己的子类加载器,那么它们可以绕过此处添加的通知代码。

更高级的类加载

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

【讨论】:

  • 您是否有“有一个 JNI 绑定到 JVM 的本机端的源/引用 [i] 在该代码的内部,有一个检查 Java 中的 类。 *​ 包”?
  • @Pacerier 这个信息来自于试图对类加载器做“坏”的事情,比如重新设计 URL 类。但是,此信息已有数年历史,可能已过时。
  • 我认为为了(线程)安全,MyClassLoader 中的loadClass 实现的主体应该包装在synchronized (getClassLoadingLock(name)) {} 块中。至少这就是基类正在做的事情......
  • @Bogdan 感谢您指出这一点。我将为这个答案挖掘我的测试用例,并确保此更改按预期工作。
【解决方案2】:

如果你这样做

    System.out.println( p.getClass().getClassLoader() );

你会发现p 的类加载器不是你的MyClassLoader bcl。它实际上是由bcl 的父级系统类加载器加载的。

PythonInterpreter 加载它的依赖类时,它会使用它的实际类加载器,系统类加载器,而不是你的bcl,所以你的拦截没有到达。

要解决这个问题,你的类加载器不能委托给它的父类,它必须自己实际加载类。

为此,您可以继承 URLClassLoader(从系统类加载器中窃取 URL)。

【讨论】:

  • 我看到 null 当我做帽子打印声明时。您是否有任何链接我可以关注以查看如何从系统类加载器中获取 URL?今晚我的 googlefu 很弱
  • oracle jdk的系统类加载器是URLClassloader的子类,所以可以向下转型调用URLClassloader.getURLs()
  • 我在问题中添加了一个 sn-p,这就是我现在所拥有的;在我从系统类加载器中获取 URL 之后,我还没有设法让自定义类加载器工作。
【解决方案3】:

如果您想在加载类时打印它们,如何在 JVM 上打开 verbose:class 选项?

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

回答您的直接问题:

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

搜索 ClassLoader 时,搜索是从叶子 ClassLoader 到根的,当 Java 确定要加载一个新类时,它是从 root em> 的 ClassLoader 树返回到启动类解析的叶子。

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

这也解决了您最终可能会在不同的 ClassLoader 中加载多个系统 Classes 实例的问题 - 每个 ClassLoader 都具有相同的完全限定名称。 EDIT 类将在其 ClassLoader 中正确解析。然而有两个问题。

  1. 假设我们有两个 String 实例,ab。如果 ab 在不同的 ClassLoader 中实例化,则 @​​987654324@ 和 a.getClass() == b.getClass() 不成立。这将导致可怕的问题。
  2. 单例:它们不会是单例 - 每个 ClassLoader 可以有一个。

结束编辑

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

【讨论】:

  • This also solves the problem that potentially you could end up with multiple system Classes instances being loaded in different ClassLoaders - each with the same fully qualified name. 类加载器的重点不是由其完全限定名称定义的类加载器吗?在哪种情况下这不是问题?
  • 在正确编写的 JVM 中不可能有两个不相等的字符串实例。只有系统类加载器可以加载 java.* 包中的类。我读过的 JVM 实现在 C 方面强制执行这一点,通过防止该包中的类被系统类加载器以外的任何东西定义。
  • @C.Trimble:我完全同意只有系统类加载器才能加载 String - 我的语句是一个 counter 示例,它显示了如果 Java 类加载逻辑不存在会发生什么跟着。理论上,如果遵循官方类加载逻辑,则不需要您提到的硬编码,但您提到的硬编码是一种受欢迎的额外安全措施。 (但这在 JVM 1.0 天是可能的)
【解决方案4】:

如果你重写另一个 loadClass() 方法会怎样?

protected Class<?> loadClass(String name, boolean resolve)

【讨论】:

  • 没有骰子。它现在捕获了所有内容两次,我猜是因为其中一个重载委托给另一个,但它仍然只捕获顶级 PythonInterpreter 而不是它的依赖项。
【解决方案5】:

在实例化 PythonInterpreter 之前,您可以使用 PySystemState 对象指定自定义类加载器。

PySystemState state = new PySystemState();
state.setClassLoader(classLoader);
PythonInterpreter interp = new PythonInterpreter(table, state);

http://wiki.python.org/jython/LearningJython

【讨论】:

  • 我想我的问题更多地针对 Java 方面而不是 Jython 方面。查看src/org/python/util/PythonInterpreter,我在文件顶部看到了一个包含 19 个导入的列表。大概这些类正在整个类中使用,为什么它们不是由加载主类的同一个类加载器加载?不是真正的 Jython 特定
  • 并不是说我不想弄清楚 Jython 自己的类加载系统的复杂性,而是一步一步来......
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-04-20
  • 2016-08-17
  • 2019-12-29
  • 1970-01-01
  • 2017-04-23
  • 2023-02-03
相关资源
最近更新 更多