Javaデバッガーインターフェースを使用してメソッドトレースを出力する

### 概要
Javaデバッガーインターフェース(JDI)を使用して、プログラムが動作しているJVMのデバッガー用のポートに接続し、実行された、メソッドのクラス名、メソッド名、メソッドの定義行を出力するコマンドラインツールをつくる。java6、7の時代にサンプルで公開されていたものを改造した。ステップ実行させることなく、メソッドの実行順を追ってアプリケーションの動作を理解するのに使用する。

https://docs.oracle.com/javase/jp/6/jdk/api/jpda/jdi/index.html

### 環境
* JDIプログラムのコンパイラ、実行環境 Jdk11
* 接続先のJavaアプリケーションのバージョン Java1.8

JDK8まではtools.jarライブラリに含まれていたため、クラスパスにtools.jarをインクルードする必要があったが、それ以降のJavaのバージョンでは不要。今回は、Jdk11使用。

### 事前に
ツールが接続する、JVMのデバッグ用のポート(jdwpのポート)を開けておく。jdwpのポートについては、以下。

https://qiita.com/tthogho1/items/601b88bc9b1a2d45b363

### ツールの動作
1\.ネットワーク越しにデバッガポートを通して解析対象のJVMに接続
2\.接続したJVMを監視し、メソッドが実行されたときに発生したイベントを取得。
3\.メソッド実行時にEVENTが発生するようJVMにリクエスト。
4\.受け取ったイベントから情報を取り出して標準出力

### プログラムについて
JDIMethodTrace.java / JDIEventMonitoer.javaの2ファイル。

####  jvm接続開始箇所（JDIMethodTrace.java）
第１引数に、接続先JVMのIPまたはホスト名、第２引数にポート番号を受け取る。
new JDIEventMonitor(vm).start()で、接続したJVMの監視を開始する。
```java
	public static void main(String[] args) throws Exception {
		String host = args[0];
		String port = args[1];

		AttachingConnector connector = getConnector();
		VirtualMachine vm = connect(connector, host, port);
		new JDIEventMonitor(vm).start();
	}
```

JDIMethodTrace.javaコード全体

```java
import java.util.Map;

import com.sun.jdi.Bootstrap;
import com.sun.jdi.VirtualMachine;
import com.sun.jdi.VirtualMachineManager;
import com.sun.jdi.connect.Connector;
import com.sun.jdi.connect.AttachingConnector;

public class JDIMethodTrace {

	public static void main(String[] args) throws Exception {
		String host = args[0];
		String port = args[1];

		AttachingConnector connector = getConnector();
		VirtualMachine vm = connect(connector, host, port);
		new JDIEventMonitor(vm).start();
	}

	private static AttachingConnector getConnector() {
		VirtualMachineManager vmManager = Bootstrap.virtualMachineManager();
		for (Connector connector : vmManager.attachingConnectors()) {
			if ("com.sun.jdi.SocketAttach".equals(connector.name())) {
				return (AttachingConnector) connector;
			}
		}
		throw new IllegalStateException();
	}

	private static VirtualMachine connect(AttachingConnector connector, String host, String port) throws Exception {
		Map<String, Connector.Argument> args = connector.defaultArguments();
		args.get("hostname").setValue(host);
		args.get("port").setValue(port);

		return connector.attach(args);
	}
}
```

#### イベントリクエスト箇所（JDIEventMonitoer.java）
MethodEntryRequestで、Method実行時にJVMでイベントが発生する。addClassExclusionFilterには、メソッドが実行されてもイベントを発生させないパッケージ名を登録する。

```java
	private void setEventRequests() {
		EventRequestManager mgr = vm.eventRequestManager();
		MethodEntryRequest menr = mgr.createMethodEntryRequest();
		for (int i = 0; i < excludes.length; ++i) {
			menr.addClassExclusionFilter(excludes[i]);
		}
		menr.setSuspendPolicy(EventRequest.SUSPEND_EVENT_THREAD);
		menr.enable();
	}
```
excludesは、配列でパッケージ名を指定している。接続対象によって除外するパッケージは変わるものなので本来は、外部から変更可能にすべき。
```java
	private final String[] excludes = { "java.*", "javax.*", "sun.*", "com.sun.*","org.jboss.*","org.wildfly.*" };
```

#### イベント取得箇所（JDIEventMonitoer.java）
イベントを広い、methodEntryEvent内部で、メソッドの情報を取り出して表示する。
```java
	public void run() {
		setEventRequests();
		EventQueue queue = vm.eventQueue();
		while (connected) {
			try {
				EventSet eventSet = queue.remove();
				for (Event event : eventSet) {
					if (event instanceof MethodEntryEvent) {
						methodEntryEvent((MethodEntryEvent) event);
					}
				}
				eventSet.resume();
			} catch (InterruptedException e) {
				e.printStackTrace();
			} catch (VMDisconnectedException discExc) {
				handleDisconnectedException();
				break;
			}
		}
	}
```

JDIEventMonitoer.javaコード全体
```java
import com.sun.jdi.request.*;
import com.sun.jdi.*;
import com.sun.jdi.event.*;

public class JDIEventMonitor extends Thread {

	private final String[] excludes = { "java.*", "javax.*", "sun.*", "com.sun.*","org.jboss.*","org.wildfly.*" };
	private final VirtualMachine vm;

	public JDIEventMonitor(VirtualMachine vm) {
		this.vm = vm;
	}

	private void setEventRequests() {
		EventRequestManager mgr = vm.eventRequestManager();
		MethodEntryRequest menr = mgr.createMethodEntryRequest();
		for (int i = 0; i < excludes.length; ++i) {
			menr.addClassExclusionFilter(excludes[i]);
		}
		menr.setSuspendPolicy(EventRequest.SUSPEND_EVENT_THREAD);
		menr.enable();
	}

	private boolean connected = true; 

	public void run() {
		setEventRequests();
		EventQueue queue = vm.eventQueue();
		while (connected) {
			try {
				EventSet eventSet = queue.remove();
				for (Event event : eventSet) {
					if (event instanceof MethodEntryEvent) {
						methodEntryEvent((MethodEntryEvent) event);
					}
				}
				eventSet.resume();
			} catch (InterruptedException e) {
				e.printStackTrace();
			} catch (VMDisconnectedException discExc) {
				handleDisconnectedException();
				break;
			}
		}
	}
	// クラス名、メソッド名を出力
	private void methodEntryEvent(MethodEntryEvent event) {
		Method meth = event.method();
		String threadName = event.thread().name();
		String className = meth.declaringType().name();
		if (meth.isConstructor()) {
			System.out.println(threadName + "entered " + className + " constructor");
		}else {
			System.out.println(threadName + "entered " + className + "." + meth.name() + "()" + " " + meth.location().lineNumber());
		}
	}

	private synchronized void handleDisconnectedException() {
		EventQueue queue = vm.eventQueue();
		while (connected) {
			try {
				EventSet eventSet = queue.remove();
				for (Event event : eventSet) {
					if (event instanceof VMDeathEvent)
						vmDeathEvent((VMDeathEvent) event);
					else if (event instanceof VMDisconnectEvent)
						vmDisconnectEvent((VMDisconnectEvent) event);
				}
				eventSet.resume();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}

	private boolean vmDied;
	private void vmDeathEvent(VMDeathEvent event) {
		vmDied = true;
		System.out.println("-- The application has exited --");
	}

	private void vmDisconnectEvent(VMDisconnectEvent event) {
		connected = false;
		if (!vmDied)
			System.out.println("- The application has been disconnected -");
	}
}
```

### ツール実行
ローカルPCのJBossに接続して確認した。jdwpは以下の設定のため、接続ポートは8787。
【JBoss起動時の設定ファイル(standard.conf.bat)のデバッグ設定箇所】
```batch
set "JAVA_OPTS=%JAVA_OPTS% -agentlib:jdwp=transport=dt_socket,address=8787,server=y,suspend=n"
```

ツールのjavaファイルをjdk11でビルドしてJDIMethodTrace.jarにパッケージングした。
コマンドプロンプトからJava11で実行する。
```batch
java -cp JDIMethodTrace.jar JDIMethodTrace 127.0.0.1 8787
```
コンソールに接続先で実行されたメソッドが出力される。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1284235/977f06e3-6888-3fe8-22b7-10a5fbb7db01.png)