Руководство по Java Instrumentation

Руководство по Java Instrumentation

1. Вступление

В этом руководстве мы поговорим оJava Instrumentation API.. Он предоставляет возможность добавлять байтовый код в существующие скомпилированные классы Java.

Мы также поговорим о Java-агентах и ​​о том, как мы используем их для инструментария нашего кода.

2. Настроить

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

Наше приложение будет состоять из двух модулей:

  1. Приложение для банкомата, которое позволяет нам снимать деньги

  2. И Java-агент, который позволит нам измерять производительность нашего банкомата, измеряя затраченное время и деньги

Агент Java изменит байт-код банкомата, что позволит нам измерить время снятия средств без необходимости изменять приложение банкомата.

Наш проект будет иметь следующую структуру:

com.example.instrumentation
base
1.0.0
pom

    agent
    application

Прежде чем углубляться в детали инструментария, давайте посмотрим, что такое Java-агент.

3. Что такое Java-агент

В общем, java-агент - это просто специально созданный jar-файл. It utilizes the Instrumentation API that the JVM provides to alter existing byte-code that is loaded in a JVM.с

Чтобы агент работал, нам нужно определить два метода:

  • premain - статически загружает агент с помощью параметра -javaagent при запуске JVM

  • agentmain - динамически загружает агент в JVM, используяJava Attach API

Следует иметь в виду, что реализация JVM, такая как Oracle, OpenJDK и другие, может предоставить механизм для динамического запуска агентов, но это не является обязательным требованием.

Во-первых, давайте посмотрим, как мы будем использовать существующий агент Java.

После этого мы рассмотрим, как создать его с нуля, чтобы добавить необходимые функции в наш байт-код.

4. Загрузка Java-агента

Чтобы иметь возможность использовать агент Java, мы должны сначала загрузить его.

У нас есть два типа нагрузки:

  • static - используетpremain для загрузки агента с помощью параметра -javaagent

  • динамический - используетagentmain для загрузки агента в JVM с помощьюJava Attach API

Далее мы рассмотрим каждый тип нагрузки и объясним, как это работает.

4.1. Статическая нагрузка

Загрузка агента Java при запуске приложения называется статической загрузкой. Static load modifies the byte-code at startup time before any code is executed.с

Имейте в виду, что статическая загрузка использует методpremain, который запускается до запуска любого кода приложения, чтобы запустить его, мы можем выполнить:

java -javaagent:agent.jar -jar application.jar

Важно отметить, что мы всегда должны ставить спараметр -javaagent перед спараметром -jar .

Ниже приведены журналы для нашей команды:

22:24:39.296 [main] INFO - [Agent] In premain method
22:24:39.300 [main] INFO - [Agent] Transforming class MyAtm
22:24:39.407 [main] INFO - [Application] Starting ATM application
22:24:41.409 [main] INFO - [Application] Successful Withdrawal of [7] units!
22:24:41.410 [main] INFO - [Application] Withdrawal operation completed in:2 seconds!
22:24:53.411 [main] INFO - [Application] Successful Withdrawal of [8] units!
22:24:53.411 [main] INFO - [Application] Withdrawal operation completed in:2 seconds!

Мы можем видеть, когда запускался методpremain и когда был преобразован классMyAtm . Мы также видим два журнала транзакций снятия наличных в банкоматах, в которых указано время, необходимое для завершения каждой операции.

Помните, что в нашем исходном приложении у нас не было этого времени завершения транзакции, оно было добавлено нашим Java-агентом.

4.2. Динамическая нагрузка

The procedure of loading a Java agent into an already running JVM is called dynamic load. Агент прикрепляется с помощьюJava Attach API.

Более сложный сценарий - это когда у нас уже работает наше приложение ATM, и мы хотим динамически добавлять общее время транзакций без простоев нашего приложения.

Для этого напишем небольшой фрагмент кода и назовем этот классAgentLoader. . Для простоты поместим этот класс в файл jar приложения. Таким образом, файл jar нашего приложения может как запустить наше приложение, так и присоединить нашего агента к приложению ATM:

VirtualMachine jvm = VirtualMachine.attach(jvmPid);
jvm.loadAgent(agentFile.getAbsolutePath());
jvm.detach();

Теперь, когда у нас естьAgentLoader, мы запускаем наше приложение, следя за тем, чтобы в течение десятисекундной паузы между транзакциями мы динамически подключили наш Java-агент с помощьюAgentLoader.

Давайте также добавим клей, который позволит нам либо запустить приложение, либо загрузить агент.

Мы назовем этот классLauncher, и он будет нашим основным классом файла jar:

public class Launcher {
    public static void main(String[] args) throws Exception {
        if(args[0].equals("StartMyAtmApplication")) {
            new MyAtmApplication().run(args);
        } else if(args[0].equals("LoadAgent")) {
            new AgentLoader().run(args);
        }
    }
}

Запуск приложения

java -jar application.jar StartMyAtmApplication
22:44:21.154 [main] INFO - [Application] Starting ATM application
22:44:23.157 [main] INFO - [Application] Successful Withdrawal of [7] units!

Присоединение Java-агента

После первой операции мы присоединяем Java-агент к нашей JVM:

java -jar application.jar LoadAgent
22:44:27.022 [main] INFO - Attaching to target JVM with PID: 6575
22:44:27.306 [main] INFO - Attached to target JVM and loaded Java agent successfully

Проверить журналы приложений

Теперь, когда мы подключили нашего агента к JVM, мы увидим, что у нас есть общее время завершения для второй операции снятия средств через банкомат.

Это означает, что мы добавили нашу функциональность на лету, когда наше приложение работало:

22:44:27.229 [Attach Listener] INFO - [Agent] In agentmain method
22:44:27.230 [Attach Listener] INFO - [Agent] Transforming class MyAtm
22:44:33.157 [main] INFO - [Application] Successful Withdrawal of [8] units!
22:44:33.157 [main] INFO - [Application] Withdrawal operation completed in:2 seconds!

5. Создание Java-агента

Узнав, как использовать агента, давайте посмотрим, как мы можем его создать. Мы посмотрим наhow to use Javassist, чтобы изменить байт-код, и объединим это с некоторыми методами API инструментария.

Поскольку java-агент используетJava Instrumentation API, прежде чем углубляться в создание нашего агента, давайте рассмотрим некоторые из наиболее часто используемых методов в этом API и краткое описание того, что они делают:

  • addTransformer - добавляет трансформатор в инструментальный движок

  • getAllLoadedClasses - возвращает массив всех классов, загруженных в данный момент JVM

  • retransformClasses - упрощает оснащение уже загруженных классов путем добавления байт-кода

  • removeTransformer - отменяет регистрацию поставленного трансформатора

  • redefineClasses - переопределить предоставленный набор классов, используя предоставленные файлы классов, что означает, что класс будет полностью заменен, а не изменен, как сretransformClasses

5.1. Создайте методыPremain иAgentmain

Мы знаем, что каждому Java-агенту нужен хотя бы один из методовpremain илиagentmain. Последний используется для динамической загрузки, тогда как первый используется для статической загрузки Java-агента в JVM.

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

public static void premain(
  String agentArgs, Instrumentation inst) {

    LOGGER.info("[Agent] In premain method");
    String className = "com.example.instrumentation.application.MyAtm";
    transformClass(className,inst);
}
public static void agentmain(
  String agentArgs, Instrumentation inst) {

    LOGGER.info("[Agent] In agentmain method");
    String className = "com.example.instrumentation.application.MyAtm";
    transformClass(className,inst);
}

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

Ниже приведен код методаtransformClass, который мы определили, чтобы помочь нам преобразовать классMyAtm.

В этом методе мы находим класс, который хотим преобразовать, и используем методtransform . Кроме того, мы добавляем трансформатор в инструментальный движок:

private static void transformClass(
  String className, Instrumentation instrumentation) {
    Class targetCls = null;
    ClassLoader targetClassLoader = null;
    // see if we can get the class using forName
    try {
        targetCls = Class.forName(className);
        targetClassLoader = targetCls.getClassLoader();
        transform(targetCls, targetClassLoader, instrumentation);
        return;
    } catch (Exception ex) {
        LOGGER.error("Class [{}] not found with Class.forName");
    }
    // otherwise iterate all loaded classes and find what we want
    for(Class clazz: instrumentation.getAllLoadedClasses()) {
        if(clazz.getName().equals(className)) {
            targetCls = clazz;
            targetClassLoader = targetCls.getClassLoader();
            transform(targetCls, targetClassLoader, instrumentation);
            return;
        }
    }
    throw new RuntimeException(
      "Failed to find class [" + className + "]");
}

private static void transform(
  Class clazz,
  ClassLoader classLoader,
  Instrumentation instrumentation) {
    AtmTransformer dt = new AtmTransformer(
      clazz.getName(), classLoader);
    instrumentation.addTransformer(dt, true);
    try {
        instrumentation.retransformClasses(clazz);
    } catch (Exception ex) {
        throw new RuntimeException(
          "Transform failed for: [" + clazz.getName() + "]", ex);
    }
}

Разобравшись с этим, давайте определим трансформатор для классаMyAtm.

5.2. Определение нашегоTransformer

Преобразователь класса должен реализоватьClassFileTransformer и реализовать метод преобразования.

Мы будем использоватьJavassist, чтобы добавить байт-код в классMyAtm и добавить журнал с общим временем транзакции вывода ATW:

public class AtmTransformer implements ClassFileTransformer {
    @Override
    public byte[] transform(
      ClassLoader loader,
      String className,
      Class classBeingRedefined,
      ProtectionDomain protectionDomain,
      byte[] classfileBuffer) {
        byte[] byteCode = classfileBuffer;
        String finalTargetClassName = this.targetClassName
          .replaceAll("\\.", "/");
        if (!className.equals(finalTargetClassName)) {
            return byteCode;
        }

        if (className.equals(finalTargetClassName)
              && loader.equals(targetClassLoader)) {

            LOGGER.info("[Agent] Transforming class MyAtm");
            try {
                ClassPool cp = ClassPool.getDefault();
                CtClass cc = cp.get(targetClassName);
                CtMethod m = cc.getDeclaredMethod(
                  WITHDRAW_MONEY_METHOD);
                m.addLocalVariable(
                  "startTime", CtClass.longType);
                m.insertBefore(
                  "startTime = System.currentTimeMillis();");

                StringBuilder endBlock = new StringBuilder();

                m.addLocalVariable("endTime", CtClass.longType);
                m.addLocalVariable("opTime", CtClass.longType);
                endBlock.append(
                  "endTime = System.currentTimeMillis();");
                endBlock.append(
                  "opTime = (endTime-startTime)/1000;");

                endBlock.append(
                  "LOGGER.info(\"[Application] Withdrawal operation completed in:" +
                                "\" + opTime + \" seconds!\");");

                m.insertAfter(endBlock.toString());

                byteCode = cc.toBytecode();
                cc.detach();
            } catch (NotFoundException | CannotCompileException | IOException e) {
                LOGGER.error("Exception", e);
            }
        }
        return byteCode;
    }
}

5.3. Создание файла манифеста агента

Наконец, чтобы получить работающий Java-агент, нам понадобится файл манифеста с парой атрибутов.

Следовательно, мы можем найти полный список атрибутов manifest в официальной документацииInstrumentation Package.

В окончательном файле jar агента Java мы добавим следующие строки в файл манифеста:

Agent-Class: com.example.instrumentation.agent.MyInstrumentationAgent
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Premain-Class: com.example.instrumentation.agent.MyInstrumentationAgent

Наш инструментарий Java завершен. Чтобы запустить его, обратитесь к разделуLoading a Java Agent этой статьи.

6. Заключение

В этой статье мы поговорили об API инструментария Java. Мы рассмотрели, как загрузить агент Java в JVM как статически, так и динамически.

Мы также рассмотрели, как мы будем создавать собственный агент Java с нуля.

Как всегда, полную реализацию примера можно найти вover on Github.