Перейти к основному содержимому

Введение в интерфейс отладки Java (JDI)

· 8 мин. чтения

1. Обзор

Мы можем задаться вопросом, как широко признанные IDE, такие как IntelliJ IDEA и Eclipse, реализуют функции отладки . Эти инструменты в значительной степени зависят от архитектуры отладчика платформы Java (JPDA).

В этой вводной статье мы обсудим API интерфейса отладки Java (JDI), доступный в рамках JPDA.

Параллельно мы шаг за шагом напишем собственную программу-отладчик, знакомясь с удобными интерфейсами JDI.

2. Введение в JPDA

Архитектура отладчика платформы Java (JPDA) — это набор хорошо разработанных интерфейсов и протоколов, используемых для отладки Java.

Он предоставляет три специально разработанных интерфейса для реализации пользовательских отладчиков для среды разработки в настольных системах.

Для начала интерфейс Java Virtual Machine Tool (JVMTI) помогает нам взаимодействовать и контролировать выполнение приложений, работающих в JVM .

Затем существует Java Debug Wire Protocol (JDWP), который определяет протокол, используемый между тестируемым приложением (отлаживаемым) и отладчиком.

Наконец, интерфейс отладки Java (JDI) используется для реализации приложения отладчика.

3. Что такое JDI ?

Java Debug Interface API — это набор интерфейсов, предоставляемых Java для реализации внешнего интерфейса отладчика. JDI является высшим уровнем JPDA .

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

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

4. Настройка

Нам потребуются две отдельные программы — отладчик и отладчик — для понимания реализаций JDI.

Сначала мы напишем пример программы в качестве отлаживаемой программы.

Давайте создадим класс JDIExampleDebuggee с несколькими строковыми переменными и операторами println :

public class JDIExampleDebuggee {
public static void main(String[] args) {
String jpda = "Java Platform Debugger Architecture";
System.out.println("Hi Everyone, Welcome to " + jpda); // add a break point here

String jdi = "Java Debug Interface"; // add a break point here and also stepping in here
String text = "Today, we'll dive into " + jdi;
System.out.println(text);
}
}

Затем мы напишем программу-отладчик.

Давайте создадим класс JDIExampleDebugger со свойствами для хранения программы отладки ( debugClass ) и номеров строк для точек останова ( breakPointLines ):

public class JDIExampleDebugger {
private Class debugClass;
private int[] breakPointLines;

// getters and setters
}

4.1. Запуск коннектора

Сначала отладчику требуется соединитель для установления соединения с целевой виртуальной машиной (ВМ).

Затем нам нужно установить отлаживаемый объект в качестве основного аргумента коннектора. Наконец, коннектор должен запустить виртуальную машину для отладки.

Для этого JDI предоставляет класс Bootstrap , который предоставляет экземпляр LaunchingConnector . LaunchingConnector предоставляет карту аргументов по умолчанию , в которой мы можем установить главный аргумент.

Поэтому добавим в класс JDIDebuggerExample метод connectAndLaunchVM :

public VirtualMachine connectAndLaunchVM() throws Exception {

LaunchingConnector launchingConnector = Bootstrap.virtualMachineManager()
.defaultConnector();
Map<String, Connector.Argument> arguments = launchingConnector.defaultArguments();
arguments.get("main").setValue(debugClass.getName());
return launchingConnector.launch(arguments);
}

Теперь мы добавим основной метод в класс JDIDEbuggerExample для отладки JDIExampleDebuggee:

public static void main(String[] args) throws Exception {

JDIExampleDebugger debuggerInstance = new JDIExampleDebugger();
debuggerInstance.setDebugClass(JDIExampleDebuggee.class);
int[] breakPoints = {6, 9};
debuggerInstance.setBreakPointLines(breakPoints);
VirtualMachine vm = null;
try {
vm = debuggerInstance.connectAndLaunchVM();
vm.resume();
} catch(Exception e) {
e.printStackTrace();
}
}

Давайте скомпилируем оба наших класса, JDIExampleDebuggee (отладчик ) и JDIExampleDebugger (отладчик):

javac -g -cp "/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/lib/tools.jar" 
com/foreach/jdi/*.java

Давайте подробно обсудим используемую здесь команду javac .

Параметр -g генерирует всю отладочную информацию , без которой мы можем увидеть AbsentInformationException .

И -cp добавит tools.jar в путь к классам для компиляции классов. **

**

Все библиотеки JDI доступны в файле tools.jar JDK. Поэтому не забудьте добавить tools.jar в путь к классам как при компиляции, так и при выполнении.

Вот и все, теперь мы готовы запустить наш собственный отладчик JDIExampleDebugger:

java -cp "/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/lib/tools.jar:." 
JDIExampleDebugger

Обратите внимание ":." с инструментами.jar. Это добавит tools.jar к пути к классам для текущей среды выполнения (используйте «;.» в Windows).

4.2. Bootstrap и ClassPrepareRequest

Выполнение программы-отладчика здесь не даст никаких результатов, так как мы не подготовили класс для отладки и не установили точки останова.

Класс VirtualMachine имеет метод eventRequestManager для создания различных запросов, таких как ClassPrepareRequest , BreakpointRequest и StepEventRequest.

Итак, давайте добавим метод enableClassPrepareRequest в класс JDIExampleDebugger .

Это отфильтрует класс JDIExampleDebuggee и активирует ClassPrepareRequest:

public void enableClassPrepareRequest(VirtualMachine vm) {
ClassPrepareRequest classPrepareRequest = vm.eventRequestManager().createClassPrepareRequest();
classPrepareRequest.addClassFilter(debugClass.getName());
classPrepareRequest.enable();
}

4.3. ClassPrepareEvent и BreakpointRequest

Как только ClassPrepareRequest для класса JDIExampleDebuggee включен, в очереди событий виртуальной машины появятся экземпляры ClassPrepareEvent .

Используя ClassPrepareEvent, мы можем получить местоположение для установки точки останова и создать BreakPointRequest .

Для этого добавим метод setBreakPoints в класс JDIExampleDebugger :

public void setBreakPoints(VirtualMachine vm, ClassPrepareEvent event) throws AbsentInformationException {
ClassType classType = (ClassType) event.referenceType();
for(int lineNumber: breakPointLines) {
Location location = classType.locationsOfLine(lineNumber).get(0);
BreakpointRequest bpReq = vm.eventRequestManager().createBreakpointRequest(location);
bpReq.enable();
}
}

4.4. BreakPointEvent и StackFrame

На данный момент мы подготовили класс для отладки и установили точки останова. Теперь нам нужно поймать BreakPointEvent и отобразить переменные.

JDI предоставляет класс StackFrame для получения списка всех видимых переменных отлаживаемой программы.

Поэтому добавим метод displayVariables в класс JDIExampleDebugger :

public void displayVariables(LocatableEvent event) throws IncompatibleThreadStateException, 
AbsentInformationException {
StackFrame stackFrame = event.thread().frame(0);
if(stackFrame.location().toString().contains(debugClass.getName())) {
Map<LocalVariable, Value> visibleVariables = stackFrame
.getValues(stackFrame.visibleVariables());
System.out.println("Variables at " + stackFrame.location().toString() + " > ");
for (Map.Entry<LocalVariable, Value> entry : visibleVariables.entrySet()) {
System.out.println(entry.getKey().name() + " = " + entry.getValue());
}
}
}

5. Цель отладки

На этом этапе все, что нам нужно, это обновить основной метод JDIExampleDebugger , чтобы начать отладку.

Следовательно, мы будем использовать уже рассмотренные методы, такие как enableClassPrepareRequest , setBreakPoints и displayVariables:

try {
vm = debuggerInstance.connectAndLaunchVM();
debuggerInstance.enableClassPrepareRequest(vm);
EventSet eventSet = null;
while ((eventSet = vm.eventQueue().remove()) != null) {
for (Event event : eventSet) {
if (event instanceof ClassPrepareEvent) {
debuggerInstance.setBreakPoints(vm, (ClassPrepareEvent)event);
}
if (event instanceof BreakpointEvent) {
debuggerInstance.displayVariables((BreakpointEvent) event);
}
vm.resume();
}
}
} catch (VMDisconnectedException e) {
System.out.println("Virtual Machine is disconnected.");
} catch (Exception e) {
e.printStackTrace();
}

Теперь, во-первых, давайте снова скомпилируем класс JDIDebuggerExample с уже обсуждаемой командой javac .

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

Variables at com.foreach.jdi.JDIExampleDebuggee:6 > 
args = instance of java.lang.String[0] (id=93)
Variables at com.foreach.jdi.JDIExampleDebuggee:9 >
jpda = "Java Platform Debugger Architecture"
args = instance of java.lang.String[0] (id=93)
Virtual Machine is disconnected.

Ура! Мы успешно отладили класс JDIExampleDebuggee . В то же время мы отобразили значения переменных в точках останова (строки 6 и 9).

Итак, наш пользовательский отладчик готов.

5.1. ШагЗапрос

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

При создании экземпляра StepRequest мы должны указать размер и глубину шага. Мы определим STEP_LINE и STEP_OVER соответственно.

Давайте напишем метод для включения пошагового запроса.

Для простоты мы начнем с последней точки останова (строка номер 9):

public void enableStepRequest(VirtualMachine vm, BreakpointEvent event) {
// enable step request for last break point
if (event.location().toString().
contains(debugClass.getName() + ":" + breakPointLines[breakPointLines.length-1])) {
StepRequest stepRequest = vm.eventRequestManager()
.createStepRequest(event.thread(), StepRequest.STEP_LINE, StepRequest.STEP_OVER);
stepRequest.enable();
}
}

Теперь мы можем обновить основной метод JDIExampleDebugger , чтобы включить пошаговый запрос, когда он является BreakPointEvent :

if (event instanceof BreakpointEvent) {
debuggerInstance.enableStepRequest(vm, (BreakpointEvent)event);
}

5.2. Шаговое событие

Подобно BreakPointEvent , мы также можем отображать переменные в StepEvent .

Давайте соответствующим образом обновим основной метод:

if (event instanceof StepEvent) {
debuggerInstance.displayVariables((StepEvent) event);
}

Наконец, мы запустим отладчик, чтобы увидеть состояние переменных при пошаговом выполнении кода:

Variables at com.foreach.jdi.JDIExampleDebuggee:6 > 
args = instance of java.lang.String[0] (id=93)
Variables at com.foreach.jdi.JDIExampleDebuggee:9 >
args = instance of java.lang.String[0] (id=93)
jpda = "Java Platform Debugger Architecture"
Variables at com.foreach.jdi.JDIExampleDebuggee:10 >
args = instance of java.lang.String[0] (id=93)
jpda = "Java Platform Debugger Architecture"
jdi = "Java Debug Interface"
Variables at com.foreach.jdi.JDIExampleDebuggee:11 >
args = instance of java.lang.String[0] (id=93)
jpda = "Java Platform Debugger Architecture"
jdi = "Java Debug Interface"
text = "Today, we'll dive into Java Debug Interface"
Variables at com.foreach.jdi.JDIExampleDebuggee:12 >
args = instance of java.lang.String[0] (id=93)
jpda = "Java Platform Debugger Architecture"
jdi = "Java Debug Interface"
text = "Today, we'll dive into Java Debug Interface"
Virtual Machine is disconnected.

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

6. Чтение вывода выполнения

Мы могли заметить, что операторы println класса JDIExampleDebuggee не были частью выходных данных отладчика.

Согласно документации JDI, если мы запускаем виртуальную машину через LaunchingConnector, ее выходные данные и потоки ошибок должны быть прочитаны объектом Process .

Поэтому давайте добавим его в предложение finally нашего основного метода:

finally {
InputStreamReader reader = new InputStreamReader(vm.process().getInputStream());
OutputStreamWriter writer = new OutputStreamWriter(System.out);
char[] buf = new char[512];
reader.read(buf);
writer.write(buf);
writer.flush();
}

Теперь выполнение программы отладчика также добавит операторы println из класса JDIExampleDebuggee в выходные данные отладки:

Hi Everyone, Welcome to Java Platform Debugger Architecture
Today, we'll dive into Java Debug Interface

7. Заключение

В этой статье мы рассмотрели API-интерфейс интерфейса отладки Java (JDI), доступный в архитектуре отладчика платформы Java (JPDA).

Попутно мы создали собственный отладчик, использующий удобные интерфейсы, предоставляемые JDI. В то же время мы добавили в отладчик возможность пошагового выполнения.

Поскольку это было только введение в JDI, рекомендуется посмотреть реализации других интерфейсов, доступных в рамках JDI API .

Как обычно, все реализации кода доступны на GitHub .