Статьи

Генерация потока событий с помощью JDI

Скажем, у вас есть большой кусок кода в работе, который не всегда работает так, как ожидалось, но так часто, что каждый желает продолжать его использовать (включая ваших клиентов). У вас есть много в вашем списке дел, и вы достаточно заняты только тем, что справляетесь с серьезными сбоями, настолько, что у вас нет времени исследовать случайный сбой анализа и загрузки или таинственную трассировку стека, должен быть безвредным. Кроме того: ваше приложение делает так много; по статистике, все время не получается все правильно, не так ли?

Для проблемы, которую нелегко повторить, но которая считается серьезной, работа в отладчике может привести к деморализации. Что если бы вы могли создать эквивалент «робо-отладчика», процесса, который будет непрерывно запускать отладчик для вас и с бесконечным терпением ждать этого редкого случая? И тогда есть здравый смысл собирать информацию со стека и — задыхаться — даже рассказать вам об этом? Если это не звучит революционно, то хорошо для вас. Почему люди когда-либо сидят перед монитором, все равно обходя отладчик вручную? Мы «сделали вручную» операцию, которая должна быть автоматизирована.

Конечно, регистрация может сделать то же самое для вас. Мой интерес к этой идее возник, когда я поддерживал приложение, для которого у меня был исходный код, но мне не разрешили его изменять. Однако мне было разрешено перекомпилировать приложение с включенным параметром отладки, и мне было разрешено подключиться с помощью отладчика. После того, как я написал монитор на основе JDI для этого приложения, я понял, что у него есть одно дополнительное преимущество — вам не нужно добавлять много операторов ведения журнала для проблемы, которую, возможно, потребуется отладить только один раз. Также обратите внимание, что подобный код может быть встроен в другое приложение (например, это может быть расширение VisualVM) и использоваться для генерации событий по требованию, что является еще одной причиной для пропуска встроенных операторов ведения журнала.

Вот общий подход:

  1. Убедитесь, что ваше целевое приложение скомпилировано с ключом -g.
  2. Запустите целевое приложение как обычно, но прослушивайте порт для подключения отладчика.
  3. Запустите ваш робо-отладчик и подключитесь к целевой JVM.
  4. Прочитайте список спецификаций точек останова, каждая из которых содержит следующую информацию:
    • Имя класса и номер строки источника.
    • Список переменных в стеке, которые вы хотите проверить.
    • Необязательное сообщение в виде отформатированной строки с заполнителями для указанных переменных, извлеченных из стека.
    • Необязательный список пар ключ-значение, значения снова извлекаются из стека.
  5. В каждой определенной точке останова остановите выполнение (кратко!) И сгенерируйте событие, реализованное в виде сообщения журнала (в файл, JDBC и т. Д.), Сообщения JMS и т. Д.
  6. Извлеките поток событий из вашего приложения, чтобы решить все проблемы, которые беспокоили вас с тех пор, как вы начали работать.

Этот пост будет охватывать все, кроме последнего пункта, который является сложной частью. Я также не буду писать логи или код JMS, так как это не имеет отношения к обсуждению. Мой пример будет генерировать вывод на стандартный вывод.

Для начала выберите целевое приложение. Я буду использовать приложение, которое я написал, под названием «JarView» (просто простое приложение Swing для поиска в каталоге файлов .jar, чтобы найти отсутствующий файл класса).

Запустите целевое приложение.

В JPDA (Java Platform Debugger Architecture) есть два основных транспорта: на основе сокетов и на основе разделяемой памяти. Я запусту свое приложение, используя основанные на сокетах JPDA и (transport = dt_socket), проинструктирую его ждать, пока к нему подключится отладчик (server = y), и
не приостанавливать его в ожидании соединения (suspend = n):

     c:\JarView> java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n -cp jarview.jar JarView

Вы увидите сообщение о запуске, подобное следующему:

     Listening for transport dt_socket at address: 50069

Прикрепить к цели

Написать программу

  1. Используйте класс Bootstrap JDI, чтобы получить экземпляр VirtualMachineManager.
  2. Перебирайте список AttachingConnectors VirtualMachineManager, пока не найдете соединитель, поддерживающий транспортный dt_socket.
  3. Получите порт Connector.Argument AttachingConnector и установите его в качестве порта, который прослушивает целевое приложение.
  4. Присоединитесь к AttachingConnector и получите экземпляр VirtualMachine.

Ниже приведен пример кода (минимум, без специальной обработки исключений), который будет выполнять описанные выше шаги. Вам нужно будет скомпилировать и запустить файл lib / tools.jar из JDK на пути к классам (кстати, его нет в JRE).

import java.util.List;
import java.util.Map;
import com.sun.jdi.Bootstrap;
import com.sun.jdi.VirtualMachine;
import com.sun.jdi.VirtualMachineManager;
import com.sun.jdi.connect.AttachingConnector;
import com.sun.jdi.connect.Connector;

public class JDIDemo
{
 public static void main(String[] args) throws Exception
 {
   VirtualMachineManager vmMgr = Bootstrap.virtualMachineManager();
   AttachingConnector socketConnector = null;
   List attachingConnectors = vmMgr.attachingConnectors();
   for (AttachingConnector ac: attachingConnectors)
   {
     if (ac.transport().name().equals("dt_socket"))
     {
       socketConnector = ac;
       break;
     }
   }
   if (socketConnector != null)
   {
     Map paramsMap = socketConnector.defaultArguments();
     Connector.IntegerArgument portArg = (Connector.IntegerArgument)paramsMap.get("port");
     portArg.setValue(Integer.parseInt(args[0]));
     VirtualMachine vm = socketConnector.attach(paramsMap);
     System.out.println("Attached to process '" + vm.name() + "'");
   }
 }
}

Намного проще получить Connector.Argument из существующей структуры данных (как указано выше), чем создать ее с нуля. Также обратите внимание, что в этом API очень мало (если есть) конструкторов; практически каждая ссылка, которую вы получаете, в конечном итоге извлекается путем прохождения класса Bootstrap и прокладывания вашего пути в API. В моем примере было 3 AttachingConnectors, представляющих транспорты dt_socket, dt_shmem и local. Когда я запускаю приведенный выше пример, я вижу следующий вывод:

    Attached to process 'Java HotSpot(TM) 64-Bit Server VM'

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

Пауза в точке останова и генерация события.

Чтобы завершить этот пост в разумных пределах, я просто выберу строку в моей цели, которую я хорошо знаю, и приведу пример кода, который выберет переменную из стека и выведет ее на стандартный вывод. , Подробности регистрации или отправки сообщения JMS на самом деле не имеют отношения к этой теме.

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

849:    if (fullName.lastIndexOf("/") > -1)
850:    {
851:      directoryName = fullName.substring(0, fullName.lastIndexOf("/"));
852:      fileName = fullName.substring(directoryName.length()+1, fullName.length());
853:    }
854:    else
855:    {
856:      fileName = fullName;
857:    }
858:    if (fileName.indexOf(searchForTextField.getText()) > -1)
859:    {
860:      Vector nextRow = new Vector();
861:      nextRow.add(archive.getAbsolutePath());
862:      nextRow.add(fileName);
863:      rowData.add(nextRow);
864:    }

Я хотел бы напечатать короткое сообщение в строке 863, которое выводит значение fileName.

Как вы определяете точку останова в JDI? Вы должны знать, что вы просите. Обычно вы ищете класс, может быть, метод и номер строки. Мое целевое приложение — это приложение Swing с множеством анонимных внутренних классов, поэтому вместо того, чтобы выяснить, какой из них мне нужен, я просто собираюсь искать по номеру строки. Возможно, вы хотите вызвать конструктор, чтобы создать точку останова для номера строки, но конструктора нет; вам нужно будет просмотреть множество метаданных и «найти» описание этой строки кода, а затем запросить точку останова, используя это описание, и метод фабрики в EventRequestManager. Чтобы сделать длинную историю несколько короче:

  1. Получить список всех классов (как ReferenceTypes).
  2. Для каждого класса получите все линейные локации (Location).
  3. В строке, соответствующей строке 863, вырвитесь из цикла поиска.
  4. Получите экземпляр EventRequestManager из VirtualMachine.
  5. Создайте BreakpointRequest в EventRequestManager, используя объект Location для строки 863.
  6. Получить экземпляр EventQueue из VirtualMachine.
  7. Создайте цикл while (true) для EventQueue, вызвав его метод remove ().
  8. Для каждого EventSet, удаленного из очереди, обработайте каждое событие.
  9. Для каждого события проверьте, является ли оно точкой останова, и, если номер строки соответствует точке останова, в которой мы заинтересованы, обработайте событие дальше.
  10. Для соответствующего события получите верхний элемент StackFrame, получите все видимые переменные в элементе StackFrame, найдите ту, чье имя соответствует искомой переменной, и, если это так, покопайтесь в API для правильной цепочки вызовов методов. извлечь его значение.

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

import java.util.List;
import java.util.Map;
import com.sun.jdi.AbsentInformationException;
import com.sun.jdi.Bootstrap;
import com.sun.jdi.LocalVariable;
import com.sun.jdi.Location;
import com.sun.jdi.ReferenceType;
import com.sun.jdi.StackFrame;
import com.sun.jdi.StringReference;
import com.sun.jdi.ThreadReference;
import com.sun.jdi.Value;
import com.sun.jdi.VirtualMachine;
import com.sun.jdi.VirtualMachineManager;
import com.sun.jdi.connect.AttachingConnector;
import com.sun.jdi.connect.Connector;
import com.sun.jdi.event.BreakpointEvent;
import com.sun.jdi.event.Event;
import com.sun.jdi.event.EventIterator;
import com.sun.jdi.event.EventQueue;
import com.sun.jdi.event.EventSet;
import com.sun.jdi.request.BreakpointRequest;
import com.sun.jdi.request.EventRequest;
import com.sun.jdi.request.EventRequestManager;

public class JDIDemo
{
 public static void main(String[] args) throws Exception
 {
   if (args.length != 3)
   {
     System.out.println("Usage:  java JDIDemo debugPortNumber sourceLineNumber variableName");
     System.exit(-1);
   }
   int debugPort = Integer.parseInt(args[0]);
   int lineNumber = Integer.parseInt(args[1]);
   String varName = args[2];

   VirtualMachineManager vmMgr = Bootstrap.virtualMachineManager();
   AttachingConnector socketConnector = null;
   List attachingConnectors = vmMgr.attachingConnectors();
   for (AttachingConnector ac: attachingConnectors)
   {
     if (ac.transport().name().equals("dt_socket"))
     {
       socketConnector = ac;
       break;
     }
   }

   if (socketConnector != null)
   {
     Map paramsMap = socketConnector.defaultArguments();
     Connector.IntegerArgument portArg = (Connector.IntegerArgument)paramsMap.get("port");
     portArg.setValue(debugPort);
     VirtualMachine vm = socketConnector.attach(paramsMap);
     System.out.println("Attached to process '" + vm.name() + "'");

     List refTypes = vm.allClasses();
     Location breakpointLocation = null;
     for (ReferenceType refType: refTypes)
     {
       if (breakpointLocation != null)
       {
         break;
       }
       List locs = refType.allLineLocations();
       for (Location loc: locs)
       {
         if (loc.lineNumber() == lineNumber)
         {
           breakpointLocation = loc;
           break;
         }
       }
     }

     if (breakpointLocation != null)
     {
       EventRequestManager evtReqMgr = vm.eventRequestManager();
       BreakpointRequest bReq = evtReqMgr.createBreakpointRequest(breakpointLocation);
       bReq.setSuspendPolicy(BreakpointRequest.SUSPEND_ALL);
       bReq.enable();
       EventQueue evtQueue = vm.eventQueue();
       while(true)
       {
         EventSet evtSet = evtQueue.remove();
         EventIterator evtIter = evtSet.eventIterator();
         while (evtIter.hasNext())
         {
           try
           {
             Event evt = evtIter.next();
             EventRequest evtReq = evt.request();
             if (evtReq instanceof BreakpointRequest)
             {
               BreakpointRequest bpReq = (BreakpointRequest)evtReq;
               if (bpReq.location().lineNumber() == lineNumber)
               {
                 System.out.println("Breakpoint at line " + lineNumber + ": ");
                 BreakpointEvent brEvt = (BreakpointEvent)evt;
                 ThreadReference threadRef = brEvt.thread();
                 StackFrame stackFrame = threadRef.frame(0);
                 List visVars = stackFrame.visibleVariables();
                 for (LocalVariable visibleVar: visVars)
                 {
                   if (visibleVar.name().equals(varName))
                   {
                     Value val = stackFrame.getValue(visibleVar);
                     if (val instanceof StringReference)
                     {
                       String varNameValue = ((StringReference)val).value();
                       System.out.println(varName + " = '" + varNameValue + "'");
                     }
                   }
                 }
               }
             }
           }
           catch (AbsentInformationException aie)
           {
             System.out.println("AbsentInformationException: did you compile your target application with -g option?");
           }
           catch (Exception exc)
           {
             System.out.println(exc.getClass().getName() + ": " + exc.getMessage());
           }
           finally
           {
             evtSet.resume();
           }
         }
       }
     }

   }
 }
}

Когда я запускаю это приложение с командной строкой, как:

 java -cp c:\jdk1.6.0_20\lib\tools.jar;. JDIDemo 56485 863 fileName

Я получаю следующий вывод:

    Attached to process 'Java HotSpot(TM) 64-Bit Server VM'
    Breakpoint at line 863:
    fileName = 'BreakpointEvent.class'
    Breakpoint at line 863:
    fileName = 'EventSetImpl$BreakpointEventImpl.class'

Указатели

Возможно, вы заметили строку выше со ссылкой на исключение AbsentInformationException. Вы получите это, если ваше целевое приложение не было скомпилировано с параметром debug (-g). Если вы не можете скомпилировать код с помощью переключателя отладки, вы сможете установить точку останова, но когда вы туда попадете, в стеке не будет никакой информации.

Некоторые операции JDI стоят дороже, чем другие. В последний раз, когда я писал приложение JDI, я заметил, что точки останова «method-entry» и «method-exit» были намного дороже, чем простые точки останова на линии. Теперь, когда у меня есть рабочий пример, я рассмотрю эти проблемы в следующей статье, чтобы увидеть, как обстоят дела в текущем обновлении Java 6.

 

От http://wayne-adams.blogspot.com/2011/10/generating-minable-event-stream-with.html