Скажем, у вас есть большой кусок кода в работе, который не всегда работает так, как ожидалось, но так часто, что каждый желает продолжать его использовать (включая ваших клиентов). У вас есть много в вашем списке дел, и вы достаточно заняты только тем, что справляетесь с серьезными сбоями, настолько, что у вас нет времени исследовать случайный сбой анализа и загрузки или таинственную трассировку стека, должен быть безвредным. Кроме того: ваше приложение делает так много; по статистике, все время не получается все правильно, не так ли?
Для проблемы, которую нелегко повторить, но которая считается серьезной, работа в отладчике может привести к деморализации. Что если бы вы могли создать эквивалент «робо-отладчика», процесса, который будет непрерывно запускать отладчик для вас и с бесконечным терпением ждать этого редкого случая? И тогда есть здравый смысл собирать информацию со стека и — задыхаться — даже рассказать вам об этом? Если это не звучит революционно, то хорошо для вас. Почему люди когда-либо сидят перед монитором, все равно обходя отладчик вручную? Мы «сделали вручную» операцию, которая должна быть автоматизирована.
Конечно, регистрация может сделать то же самое для вас. Мой интерес к этой идее возник, когда я поддерживал приложение, для которого у меня был исходный код, но мне не разрешили его изменять. Однако мне было разрешено перекомпилировать приложение с включенным параметром отладки, и мне было разрешено подключиться с помощью отладчика. После того, как я написал монитор на основе JDI для этого приложения, я понял, что у него есть одно дополнительное преимущество — вам не нужно добавлять много операторов ведения журнала для проблемы, которую, возможно, потребуется отладить только один раз. Также обратите внимание, что подобный код может быть встроен в другое приложение (например, это может быть расширение VisualVM) и использоваться для генерации событий по требованию, что является еще одной причиной для пропуска встроенных операторов ведения журнала.
Вот общий подход:
- Убедитесь, что ваше целевое приложение скомпилировано с ключом -g.
- Запустите целевое приложение как обычно, но прослушивайте порт для подключения отладчика.
- Запустите ваш робо-отладчик и подключитесь к целевой JVM.
- Прочитайте список спецификаций точек останова, каждая из которых содержит следующую информацию:
- Имя класса и номер строки источника.
- Список переменных в стеке, которые вы хотите проверить.
- Необязательное сообщение в виде отформатированной строки с заполнителями для указанных переменных, извлеченных из стека.
- Необязательный список пар ключ-значение, значения снова извлекаются из стека.
- В каждой определенной точке останова остановите выполнение (кратко!) И сгенерируйте событие, реализованное в виде сообщения журнала (в файл, JDBC и т. Д.), Сообщения JMS и т. Д.
- Извлеките поток событий из вашего приложения, чтобы решить все проблемы, которые беспокоили вас с тех пор, как вы начали работать.
Этот пост будет охватывать все, кроме последнего пункта, который является сложной частью. Я также не буду писать логи или код 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
Прикрепить к цели
Написать программу
- Используйте класс Bootstrap JDI, чтобы получить экземпляр VirtualMachineManager.
- Перебирайте список AttachingConnectors VirtualMachineManager, пока не найдете соединитель, поддерживающий транспортный dt_socket.
- Получите порт Connector.Argument AttachingConnector и установите его в качестве порта, который прослушивает целевое приложение.
- Присоединитесь к 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. Чтобы сделать длинную историю несколько короче:
- Получить список всех классов (как ReferenceTypes).
- Для каждого класса получите все линейные локации (Location).
- В строке, соответствующей строке 863, вырвитесь из цикла поиска.
- Получите экземпляр EventRequestManager из VirtualMachine.
- Создайте BreakpointRequest в EventRequestManager, используя объект Location для строки 863.
- Получить экземпляр EventQueue из VirtualMachine.
- Создайте цикл while (true) для EventQueue, вызвав его метод remove ().
- Для каждого EventSet, удаленного из очереди, обработайте каждое событие.
- Для каждого события проверьте, является ли оно точкой останова, и, если номер строки соответствует точке останова, в которой мы заинтересованы, обработайте событие дальше.
- Для соответствующего события получите верхний элемент 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