Скажем, у вас есть большой кусок кода в работе, который не всегда работает так, как ожидалось, но так часто, что каждый желает продолжать его использовать (включая ваших клиентов). У вас есть много в вашем списке дел, и вы достаточно заняты только тем, что справляетесь с серьезными сбоями, настолько, что у вас нет времени исследовать случайный сбой анализа и загрузки или таинственную трассировку стека, должен быть безвредным. Кроме того: ваше приложение делает так много; по статистике, все время не получается все правильно, не так ли?
Для проблемы, которую нелегко повторить, но которая считается серьезной, работа в отладчике может привести к деморализации. Что если бы вы могли создать эквивалент «робо-отладчика», процесса, который будет непрерывно запускать отладчик для вас и с бесконечным терпением ждать этого редкого случая? И тогда есть здравый смысл собирать информацию со стека и — задыхаться — даже рассказать вам об этом? Если это не звучит революционно, то хорошо для вас. Почему люди когда-либо сидят перед монитором, все равно обходя отладчик вручную? Мы «сделали вручную» операцию, которая должна быть автоматизирована.
Конечно, регистрация может сделать то же самое для вас. Мой интерес к этой идее возник, когда я поддерживал приложение, для которого у меня был исходный код, но мне не разрешили его изменять. Однако мне было разрешено перекомпилировать приложение с включенным параметром отладки, и мне было разрешено подключиться с помощью отладчика. После того, как я написал монитор на основе 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