Java Debug Interface(JDI)の紹介

1. 概要

link:/intellij-basics[IntelliJ IDEA]やEclipseのような広く認識されているIDEがlink:/eclipse-debugging[debugging features]を実装しているのではないかと思うかもしれません。 これらのツールは、Java Platform Debugger Architecture(JPDA)に大きく依存しています。
この入門記事では、JPDAで利用可能なJava Debug Interface API(JDI)について説明します。
同時に、*カスタムデバッガープログラム*を段階的に作成し、便利なJDIインターフェイスに慣れます。

2. JPDAの概要

Javaプラットフォームデバッガーアーキテクチャ(JPDA)は、Javaのデバッグに使用される適切に設計されたインターフェイスとプロトコルのセットです。
デスクトップシステムの開発環境にカスタムデバッガを実装するために、3つの特別に設計されたインターフェイスを提供します。
まず、Java Virtual Machine Tool Interface(JVMTI)を使用して、https://www.baeldung.com/jvm-vs-jre-vs-jdk [JVM]で実行されているアプリケーションの実行を対話および制御できます。
次に、テスト対象のアプリケーション(デバッグ対象)とデバッガーの間で使用されるプロトコルを定義するJava Debug Wire Protocol(JDWP)があります。
最後に、Java Debug Interface(JDI)を使用して、デバッガアプリケーションを実装します。

3. JDIとは

Java Debug Interface APIは、デバッガーのフロントエンドを実装するためにJavaが提供する一連のインターフェースです。 * JDIはJPDAの最上位層です*。
JDIで構築されたデバッガーは、JPDAをサポートするJVMで実行されているアプリケーションをデバッグできます。 同時に、我々は、デバッグのいずれかの層にそれをフックすることができます。
デバッグ対象の変数へのアクセスとともに、VMとその状態にアクセスする機能を提供します。 同時に、ブレークポイント、ステッピング、ウォッチポイントを設定し、スレッドを処理できます。

4. セットアップ

JDIの実装を理解するには、2つの別個のプログラム(デバッグ対象とデバッガー)が必要です。
最初に、デバッグ対象としてサンプルプログラムを作成します。
いくつかの_String_変数と_println_ステートメントを使用して_JDIExampleDebuggee_クラスを作成しましょう。
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);
    }
}
次に、デバッガープログラムを作成します。
デバッグプログラム(_debugClass_)とブレークポイントの行番号(_breakPointLines_)を保持するプロパティを持つ_JDIExampleDebugger_クラスを作成しましょう。
public class JDIExampleDebugger {
    private Class debugClass;
    private int[] breakPointLines;

    // getters and setters
}

4.1. LaunchingConnector

最初に、デバッガーはターゲットの仮想マシン(VM)との接続を確立するためのコネクターを必要とします。
次に、デバッグ対象をコネクタの_main_引数として設定する必要があります。 最後に、コネクタはデバッグのためにVMを起動する必要があります。
そのために、JDIは_LaunchingConnector_のインスタンスを提供する_Bootstrap_クラスを提供します。 _LaunchingConnector_は、_main_引数を設定できるhttps://docs.oracle.com/javase/8/docs/technotes/guides/jpda/conninv.html#Connectors [デフォルト引数]のマップを提供します。
したがって、_connectAndLaunchVM_メソッドを_JDIDebuggerExample_クラスに追加しましょう。
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);
}
ここで、_main_メソッドを_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_(debugger)の両方のクラスをコンパイルしましょう。
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ライブラリは、JDKの_tools.jar_で使用できます。*したがって、コンパイル時と実行時の両方で、クラスパスに_tools.jar_を追加してください。
    これで、カスタムデバッガ_JDIExampleDebugger:_を実行する準備ができました。
java -cp "/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/lib/tools.jar:."
JDIExampleDebugger
_tools.jar._を含む「:。」に注意してください。これにより、現在の実行時のクラスパスに_tools.jar_が追加されます(Windowsでは「;。」を使用します)。

4.2. _bootstrap _および_クラスPrepareRequest _

ここでデバッガプログラムを実行しても、デバッグ用のクラスを準備してブレークポイントを設定していないため、結果は得られません。
_VirtualMachine_クラスには、_ClassPrepareRequest _、_ BreakpointRequest _、_ StepEventRequest._などのさまざまな要求を作成する_eventRequestManager_メソッドがあります。
したがって、_enableClassPrepareRequest_メソッドを_JDIExampleDebugger_クラスに追加しましょう。
これにより、_JDIExampleDebuggee_クラスがフィルタリングされ、_ClassPrepareRequest:_が有効になります。
public void enableClassPrepareRequest(VirtualMachine vm) {
    ClassPrepareRequest classPrepareRequest = vm.eventRequestManager().createClassPrepareRequest();
    classPrepareRequest.addClassFilter(debugClass.getName());
    classPrepareRequest.enable();
}

4.3. ClassPrepareEvent_および_BreakpointRequest

_JDIExampleDebuggee_クラスの_ClassPrepareRequest_が有効になると、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_クラスを提供して、デバッグ対象のすべての可視変数のリストを取得します。
したがって、_JDIExampleDebugger_クラスに_displayVariables_メソッドを追加しましょう。
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_の_main_メソッドを更新してデバッグを開始することだけです。
したがって、_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();
}
最初に、すでに説明した_javac_コマンドを使用して_JDIDebuggerExample_クラスを再度コンパイルします。
最後に、すべての変更とともにデバッガープログラムを実行して、出力を確認します。
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.
万歳! _JDIExampleDebuggee_クラスのデバッグに成功しました。 同時に、ブレークポイントの位置(行番号6および9)で変数の値を表示しました。
したがって、カスタムデバッガの準備ができました。

5.1. StepRequest

*デバッグには、コードをステップ実行し、後続のステップで変数の状態を確認する必要もあります。*したがって、ブレークポイントでステップ要求を作成します。
_StepRequestのインスタンスを作成中に、_ステップのサイズと深さを指定する必要があります。 https://docs.oracle.com/javase/8/docs/jdk/api/jpda/jdi/com/sun/jdi/request/StepRequest.html#STEP_LINE[_STEP_LINE_]およびhttps:// docsを定義します.oracle.com / javase / 8 / docs / jdk / api / jpda / jdi / com / sun / jdi / request / StepRequest.html#STEP_OVER [_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_の_main_メソッドを更新して、_BreakPointEvent_である場合にステップ要求を有効にできます。
if (event instanceof BreakpointEvent) {
    debuggerInstance.enableStepRequest(vm, (BreakpointEvent)event);
}

5.2. StepEvent

_BreakPointEvent_と同様に、_StepEvent_で変数を表示することもできます。
それに応じて_main_メソッドを更新しましょう:
if (event instanceof StepEvent) {
    debuggerInstance.displayVariables((StepEvent) event);
}
最後に、デバッガーを実行して、コードをステップ実行しながら変数の状態を確認します。
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.
出力を比較すると、デバッガーが行番号9からステップインし、後続のすべてのステップで変数が表示されることがわかります。

[[“executionOutput]]
=== 6. 実行出力の読み取り

_JDIExampleDebuggee_クラスの_println_ステートメントは、デバッガー出力の一部ではないことに気付くかもしれません。
JDIのドキュメントによると、_LaunchingConnectorを介してVMを起動する場合は、_Process_オブジェクトがその出力ストリームとエラーストリームを読み取る必要があります。
したがって、それを_main_メソッドの_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();
}
デバッガープログラムを実行すると、_JDIExampleDebuggee_クラスの_println_ステートメントもデバッグ出力に追加されます。
Hi Everyone, Welcome to Java Platform Debugger Architecture
Today, we'll dive into Java Debug Interface

7. 結論

この記事では、Java Platform Debugger Architecture(JPDA)で利用可能なJava Debug Interface(JDI)APIを検討しました。
その過程で、JDIが提供する便利なインターフェイスを利用したカスタムデバッガーを構築しました。 同時に、デバッガーにステッピング機能も追加しました。
これはJDIの単なる紹介であるため、https://github.com/openjdk-mirror/jdk7u-jdk [JDI API]で利用可能な他のインターフェイスの実装を確認することをお勧めします。
いつものように、すべてのコード実装はhttps://github.com/eugenp/tutorials/tree/master/java-jdi[GitHubで]で入手できます。