Как я могу перезагрузить измененный класс во время выполнения в Java?

Я пишу бакалаврскую диссертацию о библиотеках для манипулирования кодом времени выполнения на 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 и наоборот не удастся.

Stephen C 02.09.2024 17:52

Это означает, что перезагрузка класса после внедрения байт-кода будет непрактичной... если только вы не сможете обратиться к обеим версиям типа через интерфейс, который они оба реализуют.

Stephen C 02.09.2024 17:57

В вашем коде имя Test в этом фрагменте кода всегда будет относиться к одному и тому же типу, независимо от того, изменен ли файл класса для Test и загружен в новый загрузчик классов; то есть независимо от того, что делает inject().

Stephen C 02.09.2024 18:00
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
0
3
55
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Если все, что вам нужно, это преобразовать тело метода, то вы можете сделать это с помощью инструментирования. Вот пример, который:

  • Преобразует метод 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 уже будут применены эти изменения.

Rob Spoor 03.09.2024 10:44

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

Slaw 03.09.2024 11:08

Обратите внимание на код моего метода main и выходные данные. Класс загружается, создается экземпляр и вызывается метод greet. Это печатает «Hello, World!». Затем я применяю повторное преобразование, а затем еще раз вызываю greet. Теперь это печатает «Прощай, мир!» (из-за ретрансформации). Другими словами, изменения применяются по требованию, а не при первом использовании класса.

Slaw 03.09.2024 11:17

Вы правы, я, должно быть, что-то упустил, когда впервые прочитал ваш ответ.

Rob Spoor 03.09.2024 11:49

Ах. Это новая функция Java 22. Надо об этом прочитать...

Stephen C 03.09.2024 13:54

@StephenC API java.lang.classfile.* определенно является новым в Java 22. Но разве инструментарий всегда (или, по крайней мере, долгое время) не мог изменять класс во время выполнения? У меня сложилось впечатление, что именно так работают профилировщики, даже те, которые могут динамически подключаться (вместо, например, -javaagent) к работающей JVM.

Slaw 03.09.2024 23:07

Другие вопросы по теме