JDIを介したJavaアプリケーションのデバッグ

はじめに


Eclipseのデバッガーを介してJVMで実行されているアプリケーションをデバッグするとき、ストリーム、変数値など、アプリケーションデータにアクセスできるアクセス量に常に感銘を受けました。 同時に、時々、特定のアクションを「スクリプト化」したり、それらをより制御したいという要望がありました。



たとえば、サイクルで変化する変数の状態を「監視」するために、条件付きブレークポイントを使用しました。その条件は「System.out.println(theVariable)」のようなコードでした。 falseを返します。 このハックにより、アプリケーションをほとんど中断することなく、変数値のログを取得することが可能になりました(もちろん、条件コードの実行中は中断されましたが、それ以上は中断されませんでした)。 さらに、多くの場合、表示ビューでデータを表示するときに、入力された表示のコード評価の結果がその直後に追加されるのは面倒でした。



一般に、たとえば、原則としてソフトウェアデバッグに似ているBean ShellまたはGroovy Shellを介して、同じことを実行できるようにしたかったのです。 論理的には、これは難しいことではないはずです-結局のところ、Eclipse自体が何らかの形でそれを行うのですよね?



図面を作成した後、プログラムでJVMデバッグ情報にアクセスすることができ、急いで例を共有しました。





JPDAとJDIについて


JVMのデバッグのために、 JPDA -Java Platform Debugger Architectureという包括的な用語の下にまとめられた特別な標準が考案されました。 これらには、 JVMTI( sysh関数を呼び出してJVMでアプリケーションをデバッグするためのネイティブインターフェイス)、 JDWP-デバッガーとJVM間のデータ転送プロトコル(アプリケーションがデバッグされるなど)が含まれます。



これはすべて、あまり関連性がありませんでした。 しかし、これに加えて、特定のJDIJPDA -Java Debug Interfaceに含まれています。 これは、JVMアプリケーションをデバッグするためのJava APIです。医師が注文したものです。 JPDAの公式ページでは 、Sun / Oracleからの参照JDI実装の存在を確認しています。 だから、それを使い始めるだけでした





概念実証として、2つのGroovyシェルを実行することにしました。1つは「実験的」なデバッグモードで、もう1つはデバッガーとして実行します。 文字列変数は実験シェルで設定され、その値は「デバッガ」シェルから取得する必要がありました。



被験者は次のパラメーターで開始されました。

-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=7896





つまり JVMはTCP / IPを介してリモートデバッグモードで起動され、ポート7896でデバッガからの接続を待機していました。



次のコマンドも実験的なGroovyシェルで実行されました。

 myVar = “Some special value”;
      
      





したがって、値「Some special value」はデバッガーで取得されているはずです。



なぜなら これは、オブジェクトのフィールドの値だけではありません。それを取得するには、Groovy Shellの内部を少し知っていなければなりませんでした(または、少なくともソースをのぞき見しなければなりません)が、タスクはもっと面白くて現実的だったようです



それからそれは「デバッガ」次第でした:



すべてを段階的に考えてみましょう。



JVM接続


JDIを使用して、デバッグすることにしたJVMに接続します(同じマシンですべてを実行したため、ホスト== localhostですが、リモートマシンでも同じように動作します。ポートは「実験的」JVMのデバッグパラメーターで設定したものと同じです)。

JDIを使用すると、ソケットを介して、またはローカルプロセスに直接JVMに参加できます。 したがって、VirtualMachineManagerは複数のAttachingConnectorを返します。 トランスポートの名前( "dt_socket")で目的のコネクタを選択します

 vmm = com.sun.jdi.Bootstrap.virtualMachineManager(); vmm.attachingConnectors().each{ if("dt_socket".equalsIgnoreCase(it.transport().name())) { atconn = it; } } args = atconn.defaultArguments(); args.get("port").setValue(7896); args.get("hostname").setValue("127.0.0.1"); vm = atconn.attach(args);
      
      







メインのストリームトレースの取得


結果として得られるリモートJVMへのインターフェースにより、実行中のスレッドを確認したり、一時停止したりできます。 ただし、リモートJVMでメソッド呼び出しを行うには、ブレークポイントによって正確に停止されるスレッドが必要です。 JDI javadocの次の段落が実際に言っていること:

「メソッド呼び出しは、指定されたスレッドがそのスレッドで発生したイベントによって中断された場合にのみ発生します。 ターゲットVMがVirtualMachine.suspend()によって中断されている場合、または指定されたスレッドがThreadReference.suspend()によって中断されている場合、メソッド呼び出しはサポートされません。



ブレークポイントを設定するために、私はいくぶん特定の方法で行った-groovyシェルを調べるのではなく、現在JVMで何が起こっているのかを見て、起きていることでブレークポイントを設定します。



実験的なJVMのスレッドでメインストリームが検出され、その開始トラックを調べました。 ストリームは以前に停止されていたため、後続の操作中にキープレースが関連したままになります。

 // Find thread by name "main" vm.allThreads().each{ if(it.name().equals("main")) mainThread = it } // Suspend it mainThread.suspend() // Look what's in it's stack trace i=0; mainThread.frames().each{ println String.valueOf(i++)+": "+it; }; println "";
      
      







その結果、私はこれを得ました:

 0: java.io.FileInputStream.readBytes(byte[], int, int)+-1 in thread instance of java.lang.Thread(name='main', id=1) 1: java.io.FileInputStream:220 in thread instance of java.lang.Thread(name='main', id=1) 2: java.io.BufferedInputStream:218 in thread instance of java.lang.Thread(name='main', id=1) 3: java.io.BufferedInputStream:237 in thread instance of java.lang.Thread(name='main', id=1) 4: jline.Terminal:99 in thread instance of java.lang.Thread(name='main', id=1) 5: jline.UnixTerminal:128 in thread instance of java.lang.Thread(name='main', id=1) 6: jline.ConsoleReader:1453 in thread instance of java.lang.Thread(name='main', id=1) 7: jline.ConsoleReader:654 in thread instance of java.lang.Thread(name='main', id=1) 8: jline.ConsoleReader:494 in thread instance of java.lang.Thread(name='main', id=1) 9: jline.ConsoleReader:448 in thread instance of java.lang.Thread(name='main', id=1) 10: jline.ConsoleReader$readLine.call(java.lang.Object, java.lang.Object)+17 in thread instance of java.lang.Thread(name='main', id=1) 11: org.codehaus.groovy.tools.shell.InteractiveShellRunner:89 in thread instance of java.lang.Thread(name='main', id=1) 12: org.codehaus.groovy.tools.shell.ShellRunner:75 in thread instance of java.lang.Thread(name='main', id=1) 13: org.codehaus.groovy.tools.shell.InteractiveShellRunner.super$2$work()+1 in thread instance of java.lang.Thread(name='main', id=1) ....  .., 65  
      
      







ブレークポイントを設定する


したがって、メインストリームのストップレースストリームがあります。 JDI APIは、ストリームのいわゆるStackFrameを返し、そこからLocationを取得できます。 実際、この場所はブレークポイントを設定するために必要です。

ためらうことなく、「jline.ConsoleReader $ readLine.call」から場所を取得し、その中にブレークポイントを設定し、その後、メインスレッドを開始してさらに作業しました。

 evReqMan = vm.eventRequestManager(); frame = mainThread.frames().get(10); bpReq = evReqMan.createBreakpointRequest(frame.location()); mainThread.resume(); bpReq.enable();
      
      







これでブレークポイントが設定されました。 実験的なGroovyシェルに切り替えてEnterキーを押すと、彼が本当に停止したことがわかりました。 ブレークポイントでフローを停止します。実験的なJVMの動作にすべての準備が整います。



Groovy Shellオブジェクトへの参照を取得する


JDI APIを使用すると、StackFrameから変数を表示できます。 Groovy Shellコンテキストから変数の値を取得するには、最初にシェル自体へのリンクを拡張する必要がありました。 しかし、彼はどこですか?



すべてのスタックフレームのすべての可視変数をスパイします。

 i=0; mainThread.frames().each{ println String.valueOf(i++)+": "+it; try{ it.visibleVariables().each{var-> println " - "+var; }} catch(Exception e) {} }; println;
      
      







オブジェクト「org.codehaus.groovy.tools.shell.Main」で、シェル変数が表示されているスタックフレームが見つかりました。

「48:org.codehaus.groovy.tools.shell.Main:131 java.lang.Threadのスレッドインスタンス(名前= 'main'、id = 1)。」



Groovy Shellから検索された値を取得する


Shell.Mainにはインタープリターフィールドがあります。 Groovy Shellの内部を少し知っていて、GroovyShellコンテキスト変数がgroovy.lang.Binding型のオブジェクトに格納されていることを事前に知っていました。これは、 インタープリターで getContext()を呼び出すことで取得できます(groovy.lang.Binding。通訳者なし)。



Bindingから、getVariable(String varName)メソッドを呼び出すことで変数値を取得できます。



 frame = mainThread.frames().get(48); vShell = frame.getValue(frame.visibleVariableByName("shell")); vInterp = vShell.getValue(vShell.referenceType().fieldByName("interp")); vContext = vInterp.invokeMethod(mainThread, vInterp.referenceType().methodsByName("getContext").get(0), [], 0) varVal = vContext.invokeMethod(mainThread, vContext.referenceType().methodsByName("getVariable").get(0), [vm.mirrorOf("myVar")], 0)
      
      







スクリプトの最後の行は、期待値「いくつかの特別な値」を返しました-すべてが機能します!



最後のタッチ


楽しみのために、デバッガからこの変数の値を変更することも決定しました。このため、BindingでsetVariableメソッド(String varName、Object varValue)を呼び出すだけで十分です。 何がもっと簡単だろうか?

 varVal = vContext.invokeMethod(mainThread, vContext.referenceType().methodsByName("setVariable").get(0), [vm.mirrorOf("myVar"), vm.mirrorOf("Surprise!")], 0); bpReq.disable(); mainThread.resume();
      
      





すべてが機能することを確認するために、ブレークポイントをzadizableにし、ブレークポイントで以前中断されたメインストリームを開始しました。



最後に実験的なGroovyシェルに切り替えて、変数myVarの値を確認したところ、「驚き!」であることがわかりました。



結論


Javaプログラマーであることは祝福です。Sunが強力なツールを提供してくれたからです。つまり、優れた機能(-:

また、JDIの便利なラッパー(メタクラス)をGroovyに追加すると、Groovy Shellからのプログラムのデバッグを非常に楽しいものにすることができます。 残念ながら、現時点では、たとえばリフレクションAPIを介したフィールドやメソッドへのアクセスと同じように見えます。



UPD:

Groovyのスラーおよび劣ったラッパーは、 youdebug.kenai.comにあります。

自分で書き始めた-github.com/mvmn/groovyjdi



All Articles