Я пишу бакалаврскую диссертацию о библиотеках для манипулирования кодом времени выполнения на Java. Для практической части мне нужно работать над профилировщиком, который может внедрять код в загруженные классы, помеченные аннотацией. Я работаю с новым API Java ClassFile, и все работает по плану до тех пор, пока я не хочу, чтобы мои изменения вступили в силу.
Я преобразовал тела некоторых методов в классе (включив таймеры и т. д., чтобы измерить, сколько времени занимает функция и т. д.), и теперь у меня есть массив байтов, содержащий измененный класс. Проблема теперь в том, что я не могу понять, как загрузить этот новый класс и сделать так, чтобы его изменения влияли на все последующие экземпляры измененных классов.
Test before = new Test();
before.doNothing();
Profiler profiler = Profiler.getInstance();
profiler.inject();
Test after = new Test();
after.doNothing();
Это тестовый код. В классе Test есть функция, которая буквально ничего не делает, байт-код — это просто возврат. Профилировщик вводит байт-код оператора печати, который печатает 20. Но никакие изменения на него не влияют. Я попытался загрузить его с помощью специального загрузчика классов, и мне удалось создать новый экземпляр этого пользовательского загруженного класса, и он напечатал 20, но это не переопределило фактический класс Test, а создало дополнительный, и doNothing из Фактический тестовый класс остался прежним и ничего не делал.
Таким образом, я знаю, что сама модификация работает, просто она не применяется должным образом. Мне нужен способ каким-то образом обновить класс глобально с помощью нового класса, который у меня есть в массиве байтов (я также могу записать его во временный файл, если это поможет), и это должно происходить просто во время выполнения, без фактической перезаписи или во время компиляции. вещи.
Это означает, что перезагрузка класса после внедрения байт-кода будет непрактичной... если только вы не сможете обратиться к обеим версиям типа через интерфейс, который они оба реализуют.
В вашем коде имя Test в этом фрагменте кода всегда будет относиться к одному и тому же типу, независимо от того, изменен ли файл класса для Test и загружен в новый загрузчик классов; то есть независимо от того, что делает inject().




Если все, что вам нужно, это преобразовать тело метода, то вы можете сделать это с помощью инструментирования. Вот пример, который:
Преобразует метод Greeter#greet() для печати "Goodbye, World!" вместо "Hello, World!".
Использует функцию предварительного просмотра API файлов классов Java 22 для преобразования тела метода (так же, как и вы).
Регистрирует агент с помощью Launcher-Agent-Class, который характерен для исполняемых файлов JAR. См. ранее связанную документацию для альтернатив.
Разработан ли агент таким образом, чтобы можно было преобразовать класс Greeter в произвольный момент времени (а не сразу при вызове метода agentmain).
Обратите внимание, что повторное преобразование влияет на экземпляры класса, созданного до повторного преобразования. Также обратите внимание, что реализация ExampleAgent подходит только для примера (например, она не проверяет, вызывали ли вы уже #retransformGreeterClass()).
Приветствие.java
package com.example;
public class Greeter {
public void greet() {
System.out.println("Hello, World!");
}
}
Main.java
package com.example;
public final class Main {
public static void main(String[] args) {
var greeter = new Greeter();
greeter.greet();
ExampleAgent.retransformGreeterClass();
greeter.greet();
}
}
ПримерAgent.java
package com.example;
import static java.lang.classfile.ClassTransform.transformingMethodBodies;
import java.lang.classfile.ClassFile;
import java.lang.classfile.CodeBuilder;
import java.lang.classfile.CodeElement;
import java.lang.classfile.Instruction;
import java.lang.classfile.MethodModel;
import java.lang.classfile.Opcode;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;
import java.security.ProtectionDomain;
public class ExampleAgent {
private static Instrumentation inst;
public static void agentmain(String agentArgs, Instrumentation inst) {
ExampleAgent.inst = inst;
}
public static void retransformGreeterClass() {
inst.addTransformer(new GreeterClassFileTransformer(), true);
try {
inst.retransformClasses(Greeter.class);
} catch (UnmodifiableClassException ex) {
throw new RuntimeException(ex);
}
}
private static class GreeterClassFileTransformer implements ClassFileTransformer {
@Override
public byte[] transform(
ClassLoader loader,
String className,
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer) {
if ("com/example/Greeter".equals(className)) {
var cf = ClassFile.of();
return cf.transform(
cf.parse(classfileBuffer),
transformingMethodBodies(this::isGreetMethod, this::acceptGreetMethodElement));
}
return null;
}
private boolean isGreetMethod(MethodModel model) {
return model.methodName().equalsString("greet");
}
private void acceptGreetMethodElement(CodeBuilder builder, CodeElement element) {
if (element instanceof Instruction i && i.opcode() == Opcode.LDC) {
builder.ldc("Goodbye, World!");
} else {
builder.with(element);
}
}
}
}
МАНИФЕСТ.МФ
Main-Class: com.example.Main
Launcher-Agent-Class: com.example.ExampleAgent
Can-Retransform-Classes: true
<PROJECT-DIR>
|
\---src
+---com
| \---example
| ExampleAgent.java
| Greeter.java
| Main.java
|
\---META-INF
MANIFEST.MF
Рабочий каталог — <PROJECT-DIR>.
Компиляция:
javac --enable-preview --release 22 --source-path src -d out/classes src/com/example/*.java
Упаковка:
jar cfm out/example.jar src/META-INF/MANIFEST.MF -C out/classes .
Выполнение:
java --enable-preview -jar out/example.jar
Hello, World!
Goodbye, World
Как сказано в комментариях к вопросу, это не изменит существующий класс. Вместо этого он изменит класс при первом использовании. К экземпляру «до» Test уже будут применены эти изменения.
Возможно, я что-то упускаю из вопроса, но мой ответ ясно показывает, что существующий класс был изменен во время выполнения (без необходимости использования другого загрузчика классов). Это то, для чего предназначены приборы. И это должно работать для ОП, по крайней мере, если все, что они хотят сделать, это изменить тело метода, и именно это, как я понял, является целью ОП.
Обратите внимание на код моего метода main и выходные данные. Класс загружается, создается экземпляр и вызывается метод greet. Это печатает «Hello, World!». Затем я применяю повторное преобразование, а затем еще раз вызываю greet. Теперь это печатает «Прощай, мир!» (из-за ретрансформации). Другими словами, изменения применяются по требованию, а не при первом использовании класса.
Вы правы, я, должно быть, что-то упустил, когда впервые прочитал ваш ответ.
Ах. Это новая функция Java 22. Надо об этом прочитать...
@StephenC API java.lang.classfile.* определенно является новым в Java 22. Но разве инструментарий всегда (или, по крайней мере, долгое время) не мог изменять класс во время выполнения? У меня сложилось впечатление, что именно так работают профилировщики, даже те, которые могут динамически подключаться (вместо, например, -javaagent) к работающей JVM.
Короткий ответ: вы не можете. Когда вы загружаете уже загруженный класс, вам нужно использовать другой загрузчик классов. И вы фактически получите другой тип, который будет несовместим с исходной версией. В том смысле, что приведение старого
Testк новомуTestи наоборот не удастся.