1. 概要

IntelliJIDEAやEclipseのような広く認識されているIDEがデバッグ機能をどのように実装しているか疑問に思うかもしれません。 これらのツールは、Java Platform Debugger Architecture(JPDA)に大きく依存しています。

この紹介記事では、JPDAで利用可能なJava Debug Interface API(JDI)について説明します。

同時に、カスタムデバッガプログラムを段階的に作成し、便利なJDIインターフェイスに慣れます。

2. JPDAの紹介

Java Platform Debugger Architecture(JPDA)は、Javaのデバッグに使用される適切に設計されたインターフェースとプロトコルのセットです。

デスクトップシステムの開発環境用のカスタムデバッガーを実装するために、3つの特別に設計されたインターフェイスを提供します。

まず、Java仮想マシンツールインターフェイス(JVMTI)は、JVMで実行されているアプリケーションの実行を操作および制御するのに役立ちます。

次に、Java Debug Wire Protocol(JDWP)があります。これは、テスト対象のアプリケーション(debuggee)とデバッガーの間で使用されるプロトコルを定義します。

最後に、Java Debug Interface(JDI)を使用してデバッガーアプリケーションを実装します。

3. JDI とは何ですか?

Java Debug Interface APIは、デバッガーのフロントエンドを実装するためにJavaによって提供される一連のインターフェースです。 JDIはJPDAの最上位層です。

JDIで構築されたデバッガーは、JPDAをサポートする任意のJVMで実行されているアプリケーションをデバッグできます。 同時に、それをデバッグの任意のレイヤーにフックすることができます。

これは、デバッグ対象の変数へのアクセスとともに、VMとその状態にアクセスする機能を提供します。 同時に、ブレークポイント、ステッピング、ウォッチポイントを設定し、スレッドを処理することができます。

4. 設定

JDIの実装を理解するには、2つの別個のプログラム(debuggeeとデバッガー)が必要です。

まず、デバッグ対象としてサンプルプログラムを作成します。

いくつかの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)との接続を確立するためのコネクターを必要とします。

次に、debuggeeをコネクタのmain引数として設定する必要があります。 最後に、コネクタはデバッグのためにVMを起動する必要があります。

そのために、JDIは、LaunchingConnectorのインスタンスを提供するBootstrapクラスを提供します。 LaunchingConnector は、デフォルト引数のマップを提供し、メイン引数を設定できます。

したがって、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

「:。」に注意してください with tools.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

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 クラスを提供して、デバッグ対象のすべての表示可能な変数のリストを取得します。

したがって、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. デバッグターゲット

このステップで必要なのは、JDIExampleDebuggermainメソッドを更新してデバッグを開始することだけです。

したがって、 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、のインスタンスを作成するときに、ステップのサイズと深さを指定する必要があります。 STEP_LINESTEP_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();    
    }
}

これで、JDIExampleDebuggermainメソッドを更新して、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からステップインし、後続のすべてのステップで変数を表示していることがわかります。

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プラットフォームデバッガアーキテクチャ(JPDA)で利用可能なJavaデバッグインターフェイス(JDI)APIについて説明しました。

その過程で、JDIが提供する便利なインターフェイスを利用してカスタムデバッガーを構築しました。 同時に、デバッガーにステッピング機能も追加しました。

これはJDIの紹介にすぎなかったため、 JDIAPIで利用可能な他のインターフェースの実装を確認することをお勧めします。

いつものように、すべてのコード実装はGitHub利用できます。