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
использовать дополнительные банки, не прибегая к отражениям.
Но, как я уже сказал, из того, что я понимаю, проблема та же, что и в упрощенной версии.
0 ответов
Сначала вы должны подумать о другом решении, так как любое другое решение лучше, чем это, поэтому возможные:
- Просто создайте отдельные модули.
- Используйте генерацию кода во время компиляции, чтобы сгенерировать эти модули, чтобы вам не нужно было дублировать свой код, например, посмотрите https://github.com/vigna/fastutil.
Но если вы действительно хотите сделать это очень грязно:
используйте Java-агентов. Это требует использования jdk jvm и / или дополнительных аргументов запуска. Вам, вероятно, следует использовать библиотеку byte-buddy-agent, если вы хотите сделать это во время выполнения без аргументов запуска, а также есть грязный трюк на java 8 для запуска агентов во время выполнения даже без надлежащих файлов из jdk - просто вводя их вручную, возможно также возможно на java 9+, но пока у меня не было времени, и мне нужно было найти способ сделать это. Вы можете увидеть мои инструкции здесь https://github.com/raphw/byte-buddy/issues/374
Но, если возможно, лучший способ - просто использовать аргумент командной строки, чтобы прикрепить агент.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.
Это использует библиотеку org.ow2.asm.asm-all и библиотеку net.bytebuddy.byte-buddy-agent (только для версии во время выполнения) .