Статьи

Byteman – швейцарский армейский нож для манипулирования байтовым кодом

Я работаю с несколькими сообществами в JBoss, и есть так много интересных вещей, о которых можно поговорить, так что я не могу каждый раз оборачиваться. Это главная причина, почему я очень благодарен за возможность время от времени приглашать сюда блоггеров-гостей. Сегодня это Йохен Мадер, который является частью стада ботаников в Codecentric. В настоящее время он тратит свое профессиональное время на программирование промежуточного программного обеспечения на базе Vert.x, написание статей для различных публикаций и выступление на конференциях. Его свободное время принадлежит его семье, mtb и настольным играм. Вы можете подписаться на него в Твиттере  @codepitbull .

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

Что это такое
Byteman — это набор инструментов для манипуляции с байтовым кодом и его инъекции. Это позволяет нам перехватывать и заменять произвольные части кода Java, чтобы заставить его вести себя иначе или нарушать его (нарочно):

  •  получить все темы застрял в определенном месте, и пусть они продолжаются в то же время (привет гонка условие)
  •  выбросить исключения в неожиданных местах
  •  отслеживание вашего кода во время выполнения
  •  изменить возвращаемые значения

и многое другое.

Пример
Давайте разберемся в некотором коде, чтобы проиллюстрировать, что Байтман может сделать для вас.
Здесь у нас есть замечательный синглтон и (к сожалению) хороший пример кода, который вы можете найти во многих местах.

public class BrokenSingleton {

    private static volatile BrokenSingleton instance;

    private BrokenSingleton() {
    }

    public static BrokenSingleton get() {
        if (instance == null) {
            instance = new BrokenSingleton();
        }
        return instance;
    }
}

Давайте представим, что мы — бедняги, которым поручено отладить некоторый устаревший код, показывающий странное поведение на производстве. Через некоторое время мы обнаруживаем этот драгоценный камень, и наша интуиция указывает на то, что здесь что-то не так.
Сначала мы можем попробовать что-то вроде этого:

public class BrokenSingletonMain {

    public static void main(String[] args) throws Exception {
        Thread thread1 = new Thread(new SingletonAccessRunnable());
        Thread thread2 = new Thread(new SingletonAccessRunnable());
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
    }

    public static class SingletonAccessRunnable implements Runnable {
        @Override
        public void run() {
            System.out.println(BrokenSingleton.get());
        }
    }
}

Выполнение этого дает очень маленький шанс увидеть реальную проблему. Но, скорее всего, мы не увидим ничего необычного. Синглтон инициализируется один раз, и приложение работает как положено. Часто люди начинают грубо форсировать, увеличивая количество потоков, надеясь, что проблема проявится. Но я предпочитаю более структурированный подход.

Введите Байтман.

DSL
Byteman предоставляет удобный DSL для изменения и отслеживания поведения приложения. Мы начнем с отслеживания звонков в моем маленьком примере. Взгляните на этот кусок кода.

RULE trace entering
CLASS de.codepitbull.byteman.BrokenSingleton
METHOD get
AT ENTRY
IF true
DO traceln("entered get-Method")
ENDRULE

RULE trace read stacks
CLASS de.codepitbull.byteman.BrokenSingleton
METHOD get
AFTER READ BrokenSingleton.instance
IF true
DO traceln("READ:\n" + formatStack())
ENDRULE

Основным строительным блоком Byteman-скриптов является ПРАВИЛО.

Он состоит из нескольких компонентов (пример беззастенчиво оторванного от  Byteman-Docs :

 # rule skeleton
 RULE <rule name>
 CLASS <class name>
 METHOD <method name>
 BIND <bindings>
 IF <condition>
 DO <actions>
 ENDRULE

Каждое ПРАВИЛО должно иметь уникальное имя __rule__. Комбинация CLASS и METHOD определяет, где мы хотим применить наши модификации. BIND позволяет нам связывать переменные с именами, которые мы можем использовать внутри IF и DO. Используя IF, мы можем добавить условия, при которых правило срабатывает. В DO происходит настоящая магия.

ENDRULE, это заканчивает правило.

Понимая это, мое первое правило легко переводится на:

Когда кто-то вызывает _de.codepitbull.byteman.BrokenSingleton.get () _ Я хочу напечатать строку «enter get-Method» прямо перед вызовом тела метода (это то, что __AT ENTRY__ переводит к).

Мое второе правило можно перевести на:

После прочтения (__AFTER READ__) экземпляра-члена BrokenSingleton я хочу увидеть текущий стек вызовов.

Возьмите код и поместите его в файл с именем _check.btm_. Byteman предоставляет хороший инструмент для проверки ваших скриптов. Используйте __ <bytemanhome> /bin/bmcheck.sh -cp folder / Содержит / Compiled / classes / to / test check.btm__, чтобы увидеть, компилируется ли ваш скрипт. Делайте это КАЖДЫЙ раз, когда вы меняете его, очень легко ошибиться в деталях и потратить много времени на его выяснение.

Теперь, когда скрипт сохранен и протестирован, пришло время использовать его с нашим приложением. Сценарии

агента
применяются для выполнения кода через агента. Откройте конфигурацию запуска для __BrokenSingletonMain-class__ и добавьте

__-javaagent:<BYTEMAN_HOME>/lib/byteman.jar=script:check.btm__

к вашим JVM-параметрам. Это зарегистрирует агента и скажет ему запустить _check.btm_.

И пока мы на него здесь несколько вариантов больше:
Если вы когда — нибудь понадобится , чтобы манипулировать некоторые ядра использования Java вещи

__-javaagent:<BYTEMAN_HOME>/lib/byteman.jar=script:appmain.btm,boot:<BYTEMAN_HOME>/lib/byteman.jar__

Это добавит Byteman в загрузочный путь к классам и позволит нам манипулировать такими классами, как _Thread_, _String_ … Я имею в виду, если вы когда-нибудь хотели такие неприятные вещи …

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

__<bytemanhome>/bin/bminstall.sh <pid>__

установить агент. После бега

__<bytemanhome>/bin/bmsubmit.sh check.btm__

Вернемся к нашей проблеме под рукой.

Запуск нашего приложения с измененной конфигурацией запуска должен привести к выводу, подобному этому

entered get-Method
entered get-Method
READ:
Stack trace for thread Thread-0
de.codepitbull.byteman.BrokenSingleton.get(BrokenSingleton.java:14)
de.codepitbull.byteman.BrokenSingletonMain$SingletonAccessRunnable.run(BrokenSingletonMain.java:20)
java.lang.Thread.run(Thread.java:745)

READ:
Stack trace for thread Thread-1
de.codepitbull.byteman.BrokenSingleton.get(BrokenSingleton.java:14)
de.codepitbull.byteman.BrokenSingletonMain$SingletonAccessRunnable.run(BrokenSingletonMain.java:20)
java.lang.Thread.run(Thread.java:745)

Поздравляю, вы только что манипулировали байтовым кодом. Результат пока не очень полезен, но это то, что мы собираемся изменить.

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

С помощью Byteman это легко достигается.

RULE define rendezvous
CLASS de.codepitbull.byteman.BrokenSingleton
METHOD get
AT ENTRY
IF NOT isRendezvous("rendezvous", 2)
DO createRendezvous("rendezvous", 2, true);
traceln("rendezvous created");
ENDRULE

Это правило определяет так называемое рандеву. Это позволяет нам указать место, куда должны приходить несколько потоков, пока им не разрешат продолжить (также известный как барьер).

И вот перевод для правила:

при вызове _BrokenSingleton.get () _ создайте новое рандеву, которое позволит прогрессировать, когда прибывают 2 потока. Сделайте рандеву многоразовым и создайте его только в том случае, если он не существует (если IF NOT не важен, иначе мы бы создали барьер при каждом вызове _BrokenSingleton.get () _).

После определения этого барьера нам все еще нужно явно его использовать.

RULE catch threads
CLASS de.codepitbull.byteman.BrokenSingleton
METHOD get
AFTER READ BrokenSingleton.instance
IF isRendezvous("rendezvous", 2)
DO rendezvous("rendezvous");
ENDRULE

Перевод: После прочтения _instance_-member внутри _BrokenSingleton.get () _ дождитесь встречи, пока не прибудет второй поток, и продолжите вместе.

Теперь мы останавливаем оба потока из _BrokenSingletonMain_ в одном и том же шнурке после проверки экземпляра на ноль. Вот как сделать условия гонки воспроизводимыми. Оба потока будут продолжать думать, что _instance_ равен нулю, в результате чего конструктор запускается дважды.

Я оставляю вам решение этой проблемы …

Юнит-тесты
Что-то, что я обнаружил во время написания этого поста в блоге, это возможность запускать Byteman-скрипты как часть моих юнит-тестов. Их  JUNit- и TestNG-интеграция  легко интегрируются.

Добавьте следующую зависимость в ваш _pom.xml_

<dependency>
    <groupId>org.jboss.byteman</groupId>   
    <artifactId>byteman-submit</artifactId>
    <scope>test</scope>
    <version>${byteman.version}</version>
</dependency>

Теперь Byteman-скрипты могут выполняться внутри ваших юнит-тестов следующим образом:

@RunWith(BMUnitRunner.class)
public class BrokenSingletonTest
{
  @Test
  @BMScript("check.btm")
  public void testForRaceCondition() {
    ...
  }
}

Добавление таких тестов в ваши костюмы значительно увеличивает полезность Byteman. Нет лучшего способа помешать другим повторить ваши ошибки, если сделать эти сценарии частью процесса сборки.

Заключительные слова
В блоге очень мало места, и я тоже не хочу переписывать их документацию. Написание этого поста было забавным занятием, так как я давно не использовал Byteman. Я не знаю, как мне удалось пропустить интеграцию модульных тестов. Это заставит меня использовать это намного больше в будущем.
И теперь я предлагаю просмотреть их  документацию  и начать вводить, есть что поиграть.