【问题标题】:Java - Relocate references at runtimeJava - 在运行时重定位引用
【发布时间】:2020-01-02 02:35:00
【问题描述】:

所以首先我知道你可以在不同的构建系统上使用各种影子插件重新定位你编译的 jar 的所有引用。我知道它是如何工作的,并且已经在使用它。但是我遇到了一个问题,我不能在编译时这样做。

我将简化我的情况,以便更容易理解(但我会在底部解释全貌,以防你好奇)。
我正在为两个不同(但相似)的系统(一个 jar forall)编写一个插件。这些平台负责启动底层软件和加载/启动所有插件(所以我无法控制应用程序,包括启动参数)。
平台A 为我提供了一个库(我们称之为com.example.lib)。平台B 也是如此。 但是它决定将其重新定位到org.b.shadow.com.example.lib
现在在我的插件的核心代码(两个平台上使用的代码)中,我使用了这个库。现在虽然我可以检测到我在哪个平台上,但我目前不知道如何在运行时重写我的代码中对库的所有引用,以便它在平台B 上工作。

根据我的发现,我似乎需要使用自定义 ClassLoader 来实现这一点。这里的问题是我不知道我可以让运行时使用我的自定义ClassLoader。或者真正从哪里开始。
一件重要的事情是,这些重定位可能只会影响我的包中的类中的引用(例如me.brainstone.project)。
我使用的另一个依赖项(并且有阴影)使用 ASM 和 ASM Commons,所以如果可以用它们来做,那就太棒了!

总结一下。我想在运行时仅在我的类中选择性地重新定位引用(对其他类)。

编辑

在我的整个(原始)帖子中,我只讨论过一个库,但我想指出,我将为多个库执行此操作。并且在那里做需要我付出大量努力的事情(为每个库(类或部分)编写包装器将被视为一项重大努力)允许我使用库并不是我想要的。相反,我想要一个解决方案,该解决方案需要最少的插件才能将新库添加到组合中。


现在这里对我的设置进行更详细的说明。
拳头我想先说一下我知道我可以为不同的平台创建两个不同的罐子。我已经在这样做了。但是由于令人惊讶的是,许多人似乎无法弄清楚这一点,而且我已经厌倦了一遍又一遍地解释它(那些人不会阅读文档来挽救他们的生命)我只想提供一个两个都用一个罐子,即使这意味着我需要花费大量时间来让它工作(我更喜欢这个而不是不断地解释它)。
现在我的实际设置如下所示:在平台A 上提供了库,但在平台B 上却没有。我知道其他插件经常通过遮蔽它来使用该库(许多没有重新定位导致各种问题)。因此,为了防止任何冲突,我下载了库,使用 jar-relocator 重新定位该 jar 中的类,然后使用反射将其注入到类路径中。在这种情况下,如果重新定位,我目前无法使用该库。这就是为什么我想在运行时更改代码中的引用。它也解释了为什么我不想更改其他类的引用,因为我不想意外破坏那些其他插件。我还认为,如果我能以某种方式使用我自己的ClassLoader,我就不需要将 jars 注入主要的ClassLoader,因为这样我就可以告诉ClassLoader 使用额外的 jars 而不必诉诸反射.
但正如我所说,据我了解,问题与简化版本相同。

【问题讨论】:

  • 别这样。创建接口和 2 个单独的实现,并在运行时选择匹配的一个。听起来像是另一个过于复杂的 Minecraft 插件,无缘无故。
  • @GotoFinal 大量重复的代码,并从外部库包装我需要使用的所有内容,这正是我想要的。更不用说建造它了......
  • 如果你会做类似的事情,你将失去调试此类代码的能力,更糟糕的是......是的,复制一些代码比创建类似的东西更好。并且通过良好的抽象,您将不需要复制任何东西或只是非常小的东西。构建将简单明了。但是我以后可以在这里写一个答案……我什么时候下班回来
  • 由于项目的性质,我已经仅限于非常基本的调试。我已经学会了如何使用它,所以我真的不介意不能做我以前做不到的事情。另一件事是我有多个库可以使用。猜测大约 10 到 15。在那些我使用的不仅仅是一个类/接口。因此,对于我将使用的每一个界面都必须这样做,这听起来像是一件可怕的苦差事。老实说,我宁愿有一个解决方案,它一次需要大量工作,但它的扩展性非常好。
  • 最后我正在使用 gradle。因此,在展示如何使用 maven 构建它的同时(如果你要这样做的话)很好。这个地方适合所有人。但这对我没有帮助。

标签: java reflection java-8 runtimemodification


【解决方案1】:

首先您应该考虑不同的解决方案,因为其他所有解决方案都比这个更好,所以可能的解决方案:

  1. 只需创建单独的模块。
  2. 在编译时使用一些代码生成来生成这些模块,这样您就不需要复制代码,例如查看https://github.com/vigna/fastutil

但如果你真的想以非常肮脏的方式来做:
使用 java 代理。这需要使用 jdk jvm 或/和其他启动参数。如果您想在没有启动参数的情况下在运行时执行此操作,您可能应该使用 byte-buddy-agent 库,并且即使没有来自 jdk 的正确文件,java 8 也有一个肮脏的技巧可以在运行时运行代理 - 只需手动注入它们,可能在 java 9+ 上也可以,但到目前为止我没有时间,需要找到一种方法来做到这一点。你可以在这里查看我的说明https://github.com/raphw/byte-buddy/issues/374#issuecomment-343786107
但如果可能的话,最好的方法是使用命令行参数将代理 .jar 作为单独的东西附加。
首先要做的是编写一个类文件转换器,它将完成您需要的所有逻辑:

public class DynamicLibraryReferenceTransformer implements ClassFileTransformer {
    private final String packageToProcess;
    private final String originalPackage;
    private final String resolvedPackage;

    DynamicLibraryReferenceTransformer(String packageToProcess, String originalPackage, String resolvedPackage) {
        this.packageToProcess = packageToProcess;
        this.originalPackage = originalPackage;
        this.resolvedPackage = resolvedPackage;
    }

    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain,
                            byte[] classfileBuffer) {
        if (! className.startsWith(this.packageToProcess)) {
            return null; // return null if you don't want to perform any changes
        }
        Remapper remapper = new Remapper() {
            @Override
            public String map(String typeName) {
                return typeName.replace(originalPackage, resolvedPackage);
            }
        };
        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
        ClassRemapper classRemapper = new ClassRemapper(cw, remapper);
        ClassReader classReader = new ClassReader(classfileBuffer);
        classReader.accept(classRemapper, 0);
        return cw.toByteArray();
    }
}

然后你只需要在运行时将它应用为 java 代理:

static { 
    Instrumentation instrumentation= ByteBuddyAgent.install();
    // note that this uses internal names, with / instead of dots, as I'm using simple .replace it's good idea to keep that last / to prevent conflicts between libraries using similar packages. (like com/assist vs com/assistance)
    instrumentation.addTransformer(new DynamicLibraryReferenceTransformer("my/pckg/", "original/pckg/", "relocated/lib/"), true);
    // you can retransform existing classes if needed but I don't suggest doing it. Only needed if some classes you might need to transform are already loaded
    // (classes are loaded on first use, with some smaller exceptions, like return types of methods of loaded class are also loaded if I remember correctly, where fields are not)
    // you can also just retransform only known classes
    instrumentation.retransformClasses(install.getAllLoadedClasses());
}

此代码应尽快运行,就像在主类中的静态代码块中一样。

更好的选择是在启动时使用命令行在 JVM 中包含代理:

首先,您需要创建新项目,因为这将是单独的 .jar,并使用 Premain-Class: mypckg.AgentMainClass 创建清单,您将包含在代理 .jar 的元信息中。
使用和上面一样的转换器,然后你只需要像这样编写非常简单的代理:

public class AgentMainClass {
    public static void premain(String agentArgs, Instrumentation instrumentation) {
        instrumentation.addTransformer(new DynamicLibraryReferenceTransformer("my/pckg/", "original/pckg/", "relocated/lib/"), true);
    }
}

现在只需将其包含在您的 java 命令中即可运行应用程序(或可能是服务器)-javaagent:MyAgent.jar
请注意,您可以在主(插件?) .jar 中包含代理和清单的代码,只要确保不要混淆依赖项,代理的类将使用不同的类加载器加载,所以不要在应用程序和代理之间进行调用,这将是单个 .jar 中的 2 个独立的东西。

这使用 org.ow2.asm.asm-all 库和 net.bytebuddy.byte-buddy-agent(仅适用于运行时版本)库。

【讨论】:

  • 我们是否在谈论 Java 9 模块(在您关于如何以更好的方式解决它的建议中)?我不能用那个。我正在使用 Java 8。
  • @BrainStone 它也适用于 java 8 或 9+,它实际上在 java 8 中会更好地工作,更少的问题和需要担心的事情。
  • 我还有两个问题:1) instrumentation.retransformClasses(install.getAllLoadedClasses()); 应该只用于重新转换已经加载的类,对吧? 2) 我是保留Instrumentation 实例还是为每个变压器重新创建它? (当我知道要使用和重新定位哪些库时,我有不同的时间)
  • byte-buddy-agent 将保证检测实例的安全,因此您无需担心这一点。是的,retransformClasses 应该只用于在最好的情况下将更改应用于加载的类,您根本不应该使用它,但如果您由于某种原因无法确保类加载顺序,它可能会很有用。
  • 啊,我忘记在这里包含一个重要的限制:重新转换的类(所以已经加载的)不能更改字段或方法签名,所以你真的需要尽可能多地确保这些类将不会被加载。 (在加载类之前,一切都可以改变)
猜你喜欢
  • 1970-01-01
  • 2014-02-02
  • 1970-01-01
  • 1970-01-01
  • 2014-05-29
  • 2021-03-16
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多