在运行时替换部分方法的内容

22

我希望能在运行时替换一些方法的内容。

我知道可以使用 javassist 实现这个功能,但是它不起作用,因为我想增强的类已经被系统 classLoader 加载了。

我该怎么做才能在运行时替换一个方法的内容?我应该尝试卸载这个类吗?我该如何操作呢?我知道这是可能的,但我无法找到如何实现。

如果可能,我想避免使用外部库,我想自己编写代码。

更多信息: - 我想增强的类包含在框架中(在一个 jar 文件中) - 我的代码实际上是该框架的插件 - 我所使用的框架有自己的 classLoader,但是这个 classLoader 不会加载它自己的类(它会将它们委托给系统类加载器) - 我所使用的框架是 Play

谢谢你的帮助!


Play! 是开源的。你可以直接在源代码中更改方法,并构建自己的 JAR 文件来使用。 - Jeffrey
你能否编写一个想要修改的类的子类? - Code-Apprentice
@Jeffrey 我可以这样做,但我想为社区编写一个“可用”的插件,所以我想避免修改源代码。 - Fabien Henon
@Code-Guru 不行,因为它是一个静态类。所以它的方法是通过类直接调用的。 - Fabien Henon
2个回答

8
你可以使用Javaassist,以及其他任何字节码工程库来实现它。这种魔法在于Java Attach API,它允许程序附加到正在运行的JVM(并修改已加载的类)。
它可以在com.sun.tools.attach包中找到,并且正如其名称所示,它是特定于Oracle JVM的。尽管如此,JDK工具例如jstackjmap使用它来支持它们的“附加到正在运行的JVM”功能,因此可以肯定它会一直存在。
关于Attach API的文档相当描述性,而这篇Oracle博客文章演示了在运行时附加代理。总的来说,它归结为:
  • 使用“常规”-javaagent方式创建一个重新转换程序,其中包括premain
  • premain重命名为agentmain
  • 创建一个临时JAR文件,其中包含代理类,并具有指向您的代理(包含agentmain)类的清单的Agent-Class,以及将Can-Retransform-Classes设置为true
  • 获取目标JVM的PID(可能是同一进程),并将临时JAR附加到它上面

值得庆幸的是,API可以在您不需要太多工作的情况下完成此操作,但如果您在运行时进行JAR生成,则可能需要一些技巧来打包代理所需的所有类。

我希望包括一个演示代理程序,在运行时附加分析器,但最终由于过长而无法发布。尽管如此,我已经将其放在Github repo中。

这种方法的一个警告是它使你的程序依赖于随JDK一起提供的tools.jar,而该库在JRE中不存在。您可以通过将tools.jar与应用程序一起分发(或提取到应用程序中),来解决此问题,但您仍需要提供Attach API所需的attach本地库。我已经在上面链接的存储库中包含了所有平台的库,但您也可以自己获取它们。
根据您的用例,这可能是理想的,也可能不是。但它肯定有效!
如果您想在运行时完全“热交换”一个类并使用自己的类,那么问题中并不清楚,但您无需使用任何字节码操作库。相反,您可以单独编译您的类(确保相同的包,类名等),并在目标类上调用transform时返回新类的字节。

4
普通的类加载器不支持在类被定义后取消定义或修改。因此,插件无法修改框架的行为,除非该框架提供了这样的自定义钩子。
您可以创建一个自定义类加载器,它会从其父类加载器中隐藏某些类,并重新定义它们,添加任何您可能需要的仪器。但是框架会在插件之前加载,并使用自己的类加载器解析类。因此,它将继续使用未经仪器化的类的版本。
我能想到的唯一合理的方法是先出现:如果您的代码首先启动,它可以引入一个类加载器用于加载框架。但这意味着您必须有一种方式将您的代码作为包装器放入框架链中。不确定在您的情况下是否可行。

回复评论中的更新:
为了创建一个隐藏某些类的类加载器,您需要覆盖其loadClass方法。如果您的许可证允许使用GPL代码,则可以查看OpenJDK如何实现此功能的默认实现。您只需要为那些不想隐藏的类延迟到父类加载器。

在隐藏父版本后,您仍然需要修改该类。也许BCEL类加载器可以帮助您完成这项工作。或者您可以从包含已修改版本的jar文件中加载类。或者类似的方式。


我不能“先到那里”。但是你提到了“隐藏”一些类。你的意思是我可以创建另一个classLoader,让它继承框架的classLoader,当我想增强的类被要求时,我自己加载它?(如果要求另一个类,我就让超类来加载它?)这样行得通吗?如果可以,我该如何加载这个类?在其他地方有这个类的副本并加载这个副本吗?找到这个类,增强它并返回新创建的类? - Fabien Henon
我尝试在Play框架定义的ClassLoader中添加日志,但当我想要增强的类被使用时,它似乎没有被调用(即loadClass方法)。这是因为它是从系统类加载器中加载的吗?那我该怎么办呢?还是因为它是一个只有静态方法的类? - Fabien Henon
@FabienHenon,您可以使用Class.getClassLoader()来查询有关加载它的类加载器的每个类。您可以使用它来查看加载相关类的是谁。我猜测这将是系统类加载器。正如我所说,您必须在框架之前决定使用哪个类加载器。 - MvG
那么如果我无法在框架之前替换类加载器,就不可能替换方法的内容了吗?我不能只设置一个新的加载器吗?或者释放系统加载器? - Fabien Henon
@FabienHenon,任何新的加载器只会影响你通过它明确加载的类或它们所引用的类。因此,你必须同时替换整个框架。就我所知,这不是你想要的。你心目中想要的修改方式在像Python这样的语言中被称为“猴子补丁”。关于Java的这个问题有一个相关的问题,但我怀疑解决方案也不适用于你的情况。 - MvG
好的,我会尝试找到另一个解决方案。谢谢你的帮助! - Fabien Henon

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接