使 Java class 从任何 ClassLoader 可见

Make a Java class visible from any ClassLoader

我正在使用 Java 代理 (Agent.class) 以包括调用代理 [=45] 的方式转换程序 (Program.class) 中的方法=].

public Program.getMultiplier()F:
    ALOAD 1
    ALOAD 2
    FDIV
+   INVOKESTATIC Agent.getCustomMultiplier(F)F
    FRETURN

我检查了 class 加载器及其代理和程序 class 的父级,它们的层次结构如下所示:

当程序执行添加的 INVOKESTATIC 指令时,它抛出 ClassNotFoundException -- 它找不到代理 class,因为它是由不同的 class 加载程序加载的。

作为临时解决方案,我尝试强制 AppClassLoader 通过反射成为 URLClassLoader 的父级,它在旧的 Java 版本中有效,但自 Java12.

是否有更可靠的方法来确保我的代理 class 从任何 class 加载程序可见?

让您的代理侦听新类加载器的创建,然后将它们的实例附加到新类加载器。

这样您就可以保留“Agent 监听 ClassLoader”接口,即使它现在超出了您希望 Agent 监听的一个平台 class 加载器。

您也许可以做一些适用于 URLClassLoader 的特定操作,但并非所有 classes 都由 URLClassLoader 的实例加载。任何 OSGi 项目都不会,大多数 Web 服务器也使用自己的 classloader 以支持热重载等

据我所知,没有办法随便更新一些 'global parent of all classloaders' 或注入一个;没有这样的父级,即使有,classloader 也可以完全忽略它的父级。

因此一般答案是:,你不能那样做。

但是,让我们戴上黑客帽吧!

您已经是特工了。作为代理,您要做的其中一件事是 'witness' classes,因为它们正在加载。只需在 agentmain 中获得的 Instrumentation 实例上调用 .addTransformer 并注册一个。

当您注意到 Program class 正在加载时,请执行以下操作:

  • 获取字节码并通过 ASM、BCEL、Bytecode Buddy 或任何其他 java 'class file reader/transformer' 框架进行处理。
  • 同时从您的代理代码中打开一个 class(我不会使用 Agent 本身,我会创建一个名为 ProgramAddonMethods 或诸如此类的 class作为容器 - 里面的一切都是供程序使用/供您的代理 'inject' 进入该程序。
  • ProgramAddonMethods中的每个个静态成员直接添加到Program。当你这样做时,修改所有访问的类型名(INVOKESTATIC 和 read/write 字段操作码),其中 etypename 是 ProgramAddonMethods 并使其成为目标 class 代替。
  • 像您已经做的那样注入 INVOKESTATIC,但是,重写它以使其成为自己的 class,因为您刚刚复制了那里的所有静态方法和字段。
  • 然后 return 从你的转换器修改 class 的字节码。

这 100% 保证你不可能 运行 进入任何模块或 class 路径边界问题,它可以与任何 class 加载器抽象一起工作,保证,但有一些警告:

  • 只是不要试图用任何东西来搞砸。使它成为所有静态方法和字段。如果必须,您可以使用 IdentityHashMap 制作假实例字段(例如 static IdentityHashMap<Foo, String> names; 实际上等同于将 private String name; 添加到 Foo class.. 除了它是当然要慢一点;大概是因为你已经陷入了这里可以接受的混乱之中。
  • 您的密码必须是 'dependency free'。它不能依赖任何其他东西,除了 java.* 之外没有任何库,甚至不能依赖助手 class。如果您注入的工作变得复杂,这个想法很快就会 运行 失去动力。如果你必须,为你自己的代理 jar 使用适当的 'thread-safely initialize it only once' 保护 制作 一个 class 加载器,并将该加载放在一个包中,这确实有好处允许依赖。

这些都是非常复杂的东西,但您似乎已经知道如何注入 INVOKESTATIC 调用,所以,我想您知道如何做到这一点。

这正是 lombok 对 'patch' eclipse 中的一些方法所做的,以确保保存操作、自动格式化和语法高亮等不会中断 - lombok 在适当的地方注入生成注释的知识,之所以以这种方式进行,是因为 eclipse 使用了一个名为 Equinox 的 classloader 平台,这使得任何其他解决方案都存在问题。您可以查看它以获取灵感或指南,尽管它没有特别详细的记录。您正在特别关注:

请注意,您可能也会对下一个方法感兴趣:lombok.patcher 的 'insert' 不会移动该方法 - 它直接将方法的主体注入其中(它 'inlines').这需要对堆栈进行一些严重的修改,并且只建议用于极其简单的单行式方法,并且对于这个问题可能是过度和不必要的火力。

免责声明:大部分都是我写的。

您可以使用 appendToBootstrapClassLoaderSearch 添加 classes 到 bootstrap class 加载程序。这使得指定 jar 文件的 classes 可用于定义 class 加载程序遵循标准委托模式的所有 classes。

但这需要将classes打包成一个jar文件。当您指定代理自己的 jar 文件时,您必须注意通过 bootstrap 加载程序加载的 classes 与通过应用程序加载程序加载的 classes 不同,即使它们源自来自同一个 jar 文件。此外,由 bootstrap 加载器加载的 classes 不得依赖于由其他 class 加载器加载的 classes。

如果您的 getCustomMultiplier 方法应该与 运行 代理交互,您必须将代理与包含此方法的 class 分开。