Java - перемещение ссылок во время выполнения

Итак, во-первых, я знаю, что вы можете переместить все ссылки на ваш скомпилированный jar с различными плагинами shadow в разных системах сборки. Я знаю, как это работает, и уже использую это. Однако я столкнулся с проблемой, когда я не могу сделать это во время компиляции.

Я упросту свою ситуацию, чтобы ее было легче понять (но я объясню полную картину внизу, если вам интересно).
Я пишу плагин для двух разных (но похожих) систем (одна фляга). Эти платформы отвечают за запуск базового программного обеспечения и загрузку/запуск всех плагинов (поэтому у меня нет контроля над приложением, включая параметры запуска).
Платформа A предлагает мне библиотеку (пусть назовем ее com.example.lib). И платформа тоже B. Но он решил переместить его в org.b.shadow.com.example.lib.
Теперь в основном коде (код, используемый на обеих платформах) моего плагина я использую библиотеку. Теперь, когда я могу определить, на какой платформе я работаю, в настоящее время я не знаю, как переписать все ссылки в моем коде на библиотеку во время выполнения, чтобы она работала на платформе B.

Судя по тому, что я обнаружил, мне кажется, что для этого нужно использовать кастом ClassLoader. Проблема здесь в том, что я не знаю, смогу ли я заставить среду выполнения использовать свой кастом ClassLoader. Или с чего начать на самом деле.
Одна важная вещь заключается в том, что эти перемещения могут влиять только на ссылки в классах из моих пакетов (например, me.brainstone.project).
Другая зависимость, которую я использую (и заштриховал), использует ASM и ASM Commons, так что если это возможно, то это будет удивительно!

Итак, в заключение. При желании я могу переместить ссылки (на другие классы) только в мои классы во время выполнения.

Редактирование:

Несмотря на всю мою (оригинальную) почту, я когда-либо говорил только об одной библиотеке, я хотел бы отметить, что я буду делать это для нескольких библиотек. И для того, чтобы делать вещи, которые требуют от меня значительных усилий (написание оберток для каждой библиотеки (класса или раздела) было бы значительным усилием), чтобы позволить мне использовать библиотеку, это не то, что я ищу. Вместо этого я хочу решение, которое требует минимальных дополнений для добавления новых библиотек в смесь.


Теперь немного более подробное объяснение моей настройки.
Во-первых, я хотел бы предисловие, что я знаю, что я могу просто создать две разные банки для разных платформ. И я уже делаю это. Но так как удивительно, что многие люди не могут понять это, и я устаю объяснять это снова и снова (это люди, которые не читают документы, чтобы спасти свою жизнь), я хотел бы просто предложить одна банка для обоих, даже если это означает, что мне нужно потратить значительное время на то, чтобы заставить его работать (я очень предпочитаю это, чем постоянно объяснять это).
Теперь моя фактическая настройка выглядит следующим образом: на платформе A библиотека предоставляется, а на платформе B - нет. Я знаю, что другие плагины часто используют библиотеку, затеняя ее (многие не перемещаются, вызывая всевозможные проблемы). Поэтому, чтобы предотвратить любые конфликты, я загружаю библиотеку, перемещаю классы внутри этого jar с помощью jar-relocator и затем внедряю ее в путь к классам, используя отражения. Недостатком в этом случае я в настоящее время не могу использовать библиотеку, если она перемещена. Вот почему я хотел бы изменить ссылки в моем коде во время выполнения. И это также объясняет, почему я не хочу менять ссылки на другие классы, потому что я не хочу случайно сломать эти другие плагины. Я также думаю, что если я могу каким-то образом использовать свой собственный ClassLoader, мне не нужно вводить банки в основной ClassLoader, потому что тогда я могу просто сказать, что ClassLoader использовать дополнительные банки без необходимости прибегать к отражениям.
Но, как я уже сказал, из того, что я понимаю, проблема та же, что и в упрощенной версии.

Ответы

Ответ 1

Сначала вы должны подумать о другом решении, так как любое другое решение лучше, чем это, поэтому возможные варианты:

  1. Просто создайте отдельные модули.
  2. Используйте некоторую генерацию кода во время компиляции, чтобы генерировать эти модули, чтобы вам не нужно было дублировать код, посмотрите, например, https://github.com/vigna/fastutil.

Но если вы действительно хотите сделать это очень грязным способом:
Используйте Java-агенты. Это требует использования jdk jvm и/или дополнительных параметров запуска. Вам, вероятно, следует использовать библиотеку byte-buddy-agent, если вы хотите сделать это во время выполнения без аргументов запуска, а также в java 8 есть грязная хитрость для запуска агентов во время выполнения даже без надлежащих файлов из jdk - просто вставляя их вручную, возможно также возможно на 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 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.
Обратите внимание, что вы можете включить код агента и манифест внутри вашего main (plugin?).Jar, просто убедитесь, что не перепутаны зависимости, классы для агента будут загружаться с использованием другого загрузчика классов, поэтому не делайте вызовов между приложением и агентом, это будет 2 отдельные вещи внутри одного .jar.

При этом используются библиотека org.ow2.asm.asm-all и библиотека net.bytebuddy.byte-buddy-agent (только для версии времени выполнения).