Въведение в интерфейса за отстраняване на грешки в Java (JDI)

1. Общ преглед

Може да се чудим как широко признати IDE като IntelliJ IDEA и Eclipse прилагат функции за отстраняване на грешки. Тези инструменти разчитат до голяма степен на архитектурата на Java Platform Debugger (JPDA).

В тази уводна статия ще обсъдим Java Debug Interface API (JDI), наличен под JPDA.

В същото време ще напишем поетапно персонализирана програма за отстраняване на грешки , запознавайки се с удобните JDI интерфейси.

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

Java Platform Debugger Architecture (JPDA) е набор от добре проектирани интерфейси и протоколи, използвани за отстраняване на грешки в Java.

Той осигурява три специално проектирани интерфейса за внедряване на персонализирани дебъгъри за среда за разработка в настолни системи.

За начало интерфейсът на Java Virtual Machine Tool (JVMTI) ни помага да взаимодействаме и контролираме изпълнението на приложения, изпълнявани в JVM.

След това има Java Debug Wire Protocol (JDWP), който определя протокола, използван между тестваното приложение (debuggee) и дебъгера.

Най-накрая, Java Debug Interface (JDI) се използва за реализиране на приложението за отстраняване на грешки.

3. Какво е JDI ?

API за отстраняване на грешки в Java е набор от интерфейси, предоставени от Java, за реализиране на интерфейса на дебъгера. JDI е най-високият слой на JPDA .

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

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

4. Настройка

Ще са ни необходими две отделни програми - програма за отстраняване на грешки и програма за отстраняване на грешки - за да разберем реализациите на JDI.

Първо ще напишем примерна програма като дебъгер.

Нека създадем клас JDIExampleDebuggee с няколко променливи String и инструкции 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. LaunchingConnector

Отначало дебъгерът изисква съединител, за да установи връзка с целевата виртуална машина (VM).

След това ще трябва да зададем дебюджета като основен аргумент на конектора . Най-накрая съединителят трябва да стартира VM за отстраняване на грешки.

За целта JDI предоставя клас Bootstrap, който дава екземпляр на LaunchingConnector . В LaunchingConnector представя карта на аргументи по подразбиране, в които можем да определят основната аргумент.

Затова нека добавим метода connectAndLaunchVM към класа JDIDebuggerExample :

public VirtualMachine connectAndLaunchVM() throws Exception { LaunchingConnector launchingConnector = Bootstrap.virtualMachineManager() .defaultConnector(); Map 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 (debuggee) и JDIExampleDebugger (дебъгер):

javac -g -cp "/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/lib/tools.jar" com/baeldung/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 е активиран, опашката за събития на VM ще започне да има екземпляри на 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 visibleVariables = stackFrame .getValues(stackFrame.visibleVariables()); System.out.println("Variables at " + stackFrame.location().toString() + " > "); for (Map.Entry entry : visibleVariables.entrySet()) { System.out.println(entry.getKey().name() + " = " + entry.getValue()); } } }

5. Цел за отстраняване на грешки

At this step, all we need is to update the main method of the JDIExampleDebugger to start debugging.

Hence, we'll use the already discussed methods like enableClassPrepareRequest, setBreakPoints, and 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(); }

Now firstly, let's compile the JDIDebuggerExample class again with the already discussed javac command.

And last, we'll execute the debugger program along with all the changes to see the output:

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

Hurray! We've successfully debugged the JDIExampleDebuggee class. At the same time, we've displayed the values of the variables at the breakpoint locations (line number 6 and 9).

Therefore, our custom debugger is ready.

5.1. StepRequest

Debugging also requires stepping through the code and checking the state of the variables at subsequent steps. Therefore, we'll create a step request at the breakpoint.

While creating the instance of the StepRequest, we must provide the size and depth of the step. We'll define STEP_LINE and STEP_OVER respectively.

Let's write a method to enable the step request.

For simplicity, we'll start stepping at the last breakpoint (line number 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(); } }

Now, we can update the main method of the JDIExampleDebugger, to enable the step request when it is a BreakPointEvent:

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

5.2. StepEvent

Similar to the BreakPointEvent, we can also display the variables at the StepEvent.

Let's update the main method accordingly:

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

At last, we'll execute the debugger to see the state of the variables while stepping through the code:

Variables at com.baeldung.jdi.JDIExampleDebuggee:6 > args = instance of java.lang.String[0] (id=93) Variables at com.baeldung.jdi.JDIExampleDebuggee:9 > args = instance of java.lang.String[0] (id=93) jpda = "Java Platform Debugger Architecture" Variables at com.baeldung.jdi.JDIExampleDebuggee:10 > args = instance of java.lang.String[0] (id=93) jpda = "Java Platform Debugger Architecture" jdi = "Java Debug Interface" Variables at com.baeldung.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.baeldung.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.

If we compare the output, we'll realize that debugger stepped in from line number 9 and displays the variables at all subsequent steps.

6. Read Execution Output

We might notice that println statements of the JDIExampleDebuggee class haven't been part of the debugger output.

As per the JDI documentation, if we launch the VM through LaunchingConnector, its output and error streams must be read by the Process object.

Therefore, let's add it to the finally clause of our main method:

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(); }

Now, executing the debugger program will also add the println statements from the JDIExampleDebuggee class to the debugging output:

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

7. Conclusion

In this article, we've explored the Java Debug Interface (JDI) API available under the Java Platform Debugger Architecture (JPDA).

Along the way, we've built a custom debugger utilizing the handy interfaces provided by JDI. At the same time, we've also added stepping capability to the debugger.

As this was just an introduction to JDI, it is recommended to look at the implementations of other interfaces available under JDI API.

As usual, all the code implementations are available over on GitHub.