12

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

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

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

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

编辑

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


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

4

1 回答 1

12

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

  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,您将包含在 agent 的 meta-inf 中.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(仅适用于运行时版本)库。

于 2019-09-01T19:30:35.880 回答