Статьи

Агенты Java

Эта статья является частью нашего Академического курса под названием Advanced Java .

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

1. Введение

В этой последней части руководства мы поговорим об агентах Java, настоящей черной магии для обычных разработчиков Java. Агенты Java могут «вмешиваться» в выполнение приложений Java, работающих на JVM во время выполнения, путем непосредственного изменения байт-кода. Агенты Java чрезвычайно мощны и опасны: они могут делать в основном все, однако, если что-то пойдет не так, они могут легко привести к сбою JVM.

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

2. Основы Java-агента

По своей сути, агент Java — это обычный класс Java, который следует строгим соглашениям. Класс агента должен реализовывать public static void premain(String agentArgs, Instrumentation inst) который становится точкой входа агента (аналогично методу main для обычных приложений Java).

После инициализации виртуальной машины Java (JVM) каждый такой premain(String agentArgs, Instrumentation inst) метод каждого агента будет вызываться в том порядке, в котором агенты были указаны при запуске JVM. Когда этот шаг инициализации будет выполнен, будет вызван main метод реального приложения Java.

Однако, если класс не реализует public static void premain(String agentArgs, Instrumentation inst) , JVM попытается найти и вызвать другую перегруженную версию public static void premain(String agentArgs) . Пожалуйста, обратите внимание, что каждый метод premain должен возвращаться для продолжения фазы запуска.

Наконец, что не менее public static void agentmain(String agentArgs, Instrumentation inst) класс агента Java также может иметь public static void agentmain(String agentArgs, Instrumentation inst) или public static void agentmain(String agentArgs) методы, которые используются при запуске агента после запуска JVM.

На первый взгляд это выглядит просто, но есть еще одна вещь, которую реализация Java-агента должна предоставить в составе пакета: manifest. Файлы манифеста, обычно расположенные в папке META-INF и называемые MANIFEST.MF , содержат различные метаданные, связанные с распространением пакета.

Мы не говорили об манифестах в этом руководстве, потому что большую часть времени они не требуются, однако это не относится к агентам Java. Следующие атрибуты определены для агентов Java, которые упакованы как файлы архива Java (или просто файлы JAR):

Атрибут Манифеста Описание
Premain-класс Когда агент указывается во время запуска JVM, этот атрибут определяет класс агента Java: класс, содержащий метод premain . Если во время запуска JVM указан агент, этот атрибут обязателен. Если атрибут отсутствует, JVM будет прервана.
Агент-Class Если реализация поддерживает механизм запуска агентов Java через некоторое время после запуска JVM, тогда этот атрибут указывает класс агента: класс, содержащий метод agentmain . Этот атрибут является обязательным, и агент не будет запущен, если этот атрибут отсутствует.
Boot-Class-Path Список путей для поиска загрузчиком классов начальной загрузки. Пути представляют каталоги или библиотеки.
Can-переопределять-классы Значение true или false , не зависит от регистра и определяет возможность переопределения классов, необходимых этому агенту. Этот атрибут является необязательным, по умолчанию используется значение false .
Can-ретрансформации-классы Значение true или false , нечувствительно к регистру и определяет, нужна ли этому агенту возможность повторного преобразования классов. Этот атрибут является необязательным, по умолчанию используется значение false .
Can-Set-Native-Method-Prefix Значение true или false , нечувствительно к регистру и определяет, нужна ли этому агенту возможность устанавливать собственный префикс метода. Этот атрибут является необязательным, по умолчанию используется значение false .

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

3. Java-агент и инструментарий

Инструментальные возможности агентов Java поистине безграничны. Наиболее заметные из них включают, но не ограничиваются:

  • Возможность переопределять классы во время выполнения. Переопределение может изменить тела метода, пул констант и атрибуты. Переопределение не должно добавлять, удалять или переименовывать поля или методы, изменять сигнатуры методов или изменять наследование.
  • Возможность повторного преобразования классов во время выполнения. Ретрансформация может изменить тела метода, пул констант и атрибуты. Повторное преобразование не должно добавлять, удалять или переименовывать поля или методы, изменять сигнатуры методов или изменять наследование.
  • Возможность изменить обработку ошибок разрешения собственных методов, разрешив повторную попытку с префиксом, примененным к имени.

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

4. Написание вашего первого Java-агента

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

Чтобы заполнить этот пробел, творческое сообщество Java разработало несколько превосходных и на данный момент очень зрелых библиотек, таких как Javassist и ASM , и это лишь некоторые из них. Из этих двух Javassist гораздо проще в использовании, и именно поэтому он стал тем, который мы собираемся использовать в качестве решения для манипулирования байт-кодом. Пока что мы впервые не смогли найти подходящий API в стандартной библиотеке Java и не имели другого выбора, кроме как использовать тот, который предоставлен сообществом.

Пример, над которым мы собираемся работать, довольно прост, но он взят из реального варианта использования. Допустим, мы хотели бы захватить URL каждого HTTP-соединения, открытого приложениями Java. Есть много способов сделать это путем прямого изменения исходного кода Java, но давайте предположим, что исходный код недоступен из-за лицензионных политик или чего-то еще. Типичный пример класса, который открывает HTTP-соединение, может выглядеть так:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class SampleClass {
    public static void main( String[] args ) throws IOException {
        fetch("http://www.google.com");
        fetch("http://www.yahoo.com");
    }
 
    private static void fetch(final String address)
            throws MalformedURLException, IOException {
 
        final URL url = new URL(address);               
        final URLConnection connection = url.openConnection();
         
        try( final BufferedReader in = new BufferedReader(
                new InputStreamReader( connection.getInputStream() ) ) ) {
             
            String inputLine = null;
            final StringBuffer sb = new StringBuffer();
            while ( ( inputLine = in.readLine() ) != null) {
                sb.append(inputLine);
            }      
             
            System.out.println("Content size: " + sb.length());
        }
    }
}

Java-агенты очень хорошо подходят для решения подобных задач. Нам просто нужно определить преобразователь, который будет немного модифицировать конструкторы sun.net.www.protocol.http.HttpURLConnection путем внедрения кода для вывода на консоль. Звучит страшно, но с ClassFileTransformer и Javassist это очень просто. Давайте посмотрим на такую ​​реализацию трансформатора:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class SimpleClassTransformer implements ClassFileTransformer {
  @Override
  public byte[] transform(
      final ClassLoader loader,
      final String className,
      final Class<?> classBeingRedefined,
      final ProtectionDomain protectionDomain,
      final byte[] classfileBuffer ) throws IllegalClassFormatException {
         
    if (className.endsWith("sun/net/www/protocol/http/HttpURLConnection")) {
      try {
        final ClassPool classPool = ClassPool.getDefault();
        final CtClass clazz =
          classPool.get("sun.net.www.protocol.http.HttpURLConnection");
                 
        for (final CtConstructor constructor: clazz.getConstructors()) {
          constructor.insertAfter("System.out.println(this.getURL());");
        }
     
        byte[] byteCode = clazz.toBytecode();
        clazz.detach();
               
        return byteCode;
      } catch (final NotFoundException | CannotCompileException | IOException ex) {
        ex.printStackTrace();
      }
    }
         
    return null;
  }
}

ClassPool и все классы CtXxx ( CtClass , CtConstructor ) получены из библиотеки Javassist. Преобразование, которое мы сделали, довольно наивно, но оно здесь для демонстрационных целей. Во-первых, поскольку нас интересовали только HTTP-коммуникации, sun.net.www.protocol.http.HttpURLConnection является классом из стандартной библиотеки Java, который отвечает за это.

Обратите внимание, что вместо «.» разделитель, className имеет className ‘/’. Во-вторых, мы искали класс HttpURLConnection и изменили все его конструкторы, System.out.println(this.getURL()); заявление в конце. И, наконец, мы вернули новый байт-код преобразованной версии класса, так что он будет использоваться JVM вместо исходного.

При этом роль метода premain агента premain будет заключаться в добавлении экземпляра класса SimpleClassTransformer в контекст инструментария:

1
2
3
4
5
6
public class SimpleAgent {
    public static void premain(String agentArgs, Instrumentation inst) {
        final SimpleClassTransformer transformer = new SimpleClassTransformer();
        inst.addTransformer(transformer);
    }
}

Вот и все. Это выглядит довольно просто и несколько пугающе одновременно. Чтобы закончить с агентом Java, мы должны предоставить правильный файл MANIFEST.MF, чтобы JVM могла выбрать правильный класс. Вот соответствующий минимальный набор обязательных атрибутов (более подробную информацию см. В разделе « Основы Java-агента» ):

1
2
Manifest-Version: 1.0
Premain-Class: com.javacodegeeks.advanced.agent.SimpleAgent

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

5. Запуск агентов Java

При запуске из командной строки агент Java может быть передан экземпляру JVM с -javaagent аргумента -javaagent который имеет следующую семантику:

1
-javaagent:<path-to-jar>[=options]

Где <path-to-jar> — это путь для поиска JAR-архива агента Java, а параметры содержат дополнительные параметры, которые можно передать агенту Java, точнее через аргумент agentArgs . Например, командная строка для запуска нашего агента Java из раздела Написание вашего первого агента Java (с использованием его версии Java 7) будет выглядеть так (при условии, что файл JAR агента находится в текущей папке):

1
java -javaagent:advanced-java-part-15-java7.agents-0.0.1-SNAPSHOT.jar

При запуске класса SampleClass вместе с Java-агентом advanced-java-part-15-java7.agents-0.0.1-SNAPSHOT.jar приложение будет печатать на консоли все URL-адреса ( Google и Yahoo! ), которые были попытка доступа по протоколу HTTP (с последующим размером содержимого домашних страниц поиска Google и Yahoo! соответственно):

1
2
3
4
http://www.google.com
Content size: 20349
http://www.yahoo.com
Content size: 1387

Запуск того же класса SampleClass без SampleClass Java-агента SampleClass к выводу на консоль только размера контента, без URL-адресов (обратите внимание, что размер контента может отличаться):

1
2
Content size: 20349
Content size: 1387

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

6. Что дальше

Когда эта часть подходит к концу, расширенный учебник по Java также закончен. Надеюсь, вы нашли это полезным, практичным и интересным. Есть много тем, которые не были освещены в нем, но вы можете продолжить это глубокое погружение в удивительный мир языка Java, платформы, экосистемы и невероятного сообщества. Удачи!

7. Загрузите исходный код

Вы можете скачать исходный код этого урока здесь: advanced-java-part-15