Рубрики
Без рубрики

Руководство по приборам Java

Руководство по пониманию Java Instrumentation и внедрению пользовательских агентов.

Автор оригинала: Adrian Precub.

Руководство по приборам Java

1. Введение

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

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

2. Настройка

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

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

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

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

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

com.baeldung.instrumentation
base
1.0.0
pom

    agent
    application

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

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

В общем, Java агент просто специально созданный файл банку. Он использует Инструментация API что JVM предоставляет для изменения существующего карт-кода, который загружается в JVM.

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

  • премейн – будет статично загружать агента с помощью -javaagent параметра при запуске JVM
  • агентмейн – будет динамически загружать агента в JVM с помощью Java Прикрепить API

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

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

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

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

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

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

  • статический – использует премейн для загрузки агента с помощью опции -javaagent
  • динамический – использует агентмейн для загрузки агента в СПМ с помощью Java Прикрепить API

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

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

Загрузка java-агента при запуске приложения называется статической нагрузкой. Статическая нагрузка изменяет карт-код во время запуска перед выполнением кода.

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

java -javaagent:agent.jar -jar application.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!

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

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

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

Процедура загрузки java-агента в уже запущенную JVM называется динамической нагрузкой. Агент прикрепляется с помощью Java Прикрепите API .

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

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

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

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

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

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

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-агента

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

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

  • добавитьТрансформер – добавляет трансформатор к приборной двигателю
  • getAllLoadedClasses – возвращает массив всех классов, загруженных в настоящее время JVM
  • ретрансформироватьКлассы – облегчает инструментарий уже загруженных классов путем добавления карт-кода
  • удалитьТрансформер – незарегистрированный поставляемый трансформатор
  • переопределитьКлассы – переопределить поставленный набор классов с использованием предоставленных файлов класса, что означает, что класс будет полностью заменен, а не изменен, как с ретрансформироватьклассы

5.1. Создание методов Premain и Agentmain

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

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

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

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

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

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

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. Определение нашего трансформатора

Трансформатор класса должен реализовать КлассФилеТрансформер и внедрить метод преобразования.

Мы будем использовать Явасистская добавить в 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-агента, нам понадобится файл манифеста с несколькими атрибутами.

Таким образом, мы можем найти полный список явных атрибутов в Инструментный пакет официальная документация.

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

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

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

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

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

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

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