APサーバーのスレッドダンプをWeb画面からダウンロードする。

# 概要
Javaで実装されているAPサーバーのWeb画面に、スレッドダンプをダウンロードできるボタンを設置します。

# 問題
Javaで動いているAPサーバーのスレッドダンプを取りたいとき、以下の手順を取るケースがあると思います。
1. jpsコマンドでダンプを取得するAPサーバーのプロセスIDを取得する
1. jstackコマンドの引数にプロセスIDを与えて実行
1. ダンプを取得

これはこれで良いと思うのですが、問題が発生しているタイミングで、今すぐスレッドダンプを取得したいということもあると思います。
その場合、対象サーバーにログインして、jps→jstackがちょっとまどろっこしい場合もありうると思います。

# 解決策
この、思い付いたタイミングですぐにスレッドダンプが取得できない、という問題の解決策として、APサーバーの画面上にスレッドダンプを取得できるボタンを設置し、必要なタイミングでボタンを押して、スレッドダンプをダウンロードできるようにします。

# 実装
## ManagementFactoryクラス
ManagementFactoryクラスを使うと、Javaのコード内でスレッド情報を取得できます。

```java:
import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;

// （中略）
// スレッド情報が入っているBeanをManagementFactoryクラスから取得する。
ThreadMXBean threadMxBean = ManagementFactory.getThreadMXBean();
ThreadInfo[] threadInfoArray = threadMxBean.dumpAllThreads(false, false);
```

## スレッドの状態
スレッドの状態を取得するには以下のようにします。

```java:
ThreadInfo threadInfo = threadInfoArray[0];
Thread.State state = threadInfo.getThreadState();
```

スレッドの状態は以下のいずれかです。(参考：[列挙型クラスThread.State](https://docs.oracle.com/javase/jp/17/docs/api/java.base/java/lang/Thread.State.html))
- NEW
起動していないスレッドの状態。 
- RUNNABLE
 Java仮想マシンで実行されているスレッドの状態。 
- BLOCKED
ブロックされ、モニター・ロックを待機しているスレッドの状態。 
- WAITING
ほかのスレッドが特定のアクションを実行するのを無期限に待機しているスレッドの状態。 
- TIMED_WAITING
指定された待機時間、ほかのスレッドがアクションを実行するのを待機しているスレッドの状態。 
- TERMINATED
終了したスレッドの状態。 

## スレッドダンプ
スレッドダンプはtoString()で取れます。

```java:
ThreadInfo threadInfo = threadInfoArray[0];
String dumpStr = threadInfo.toString();
```
以下は出力した例です。スレッド名やスレッドの状態、スタックトレースの一部が出力されます。
```console:出力例
"RMI TCP Accept-0" daemon prio=5 Id=19 RUNNABLE (in native)
	at java.base@17.0.11/sun.nio.ch.Net.accept(Native Method)
	at java.base@17.0.11/sun.nio.ch.NioSocketImpl.accept(NioSocketImpl.java:760)
	at java.base@17.0.11/java.net.ServerSocket.implAccept(ServerSocket.java:675)
	at java.base@17.0.11/java.net.ServerSocket.platformImplAccept(ServerSocket.java:641)
	at java.base@17.0.11/java.net.ServerSocket.implAccept(ServerSocket.java:617)
	at java.base@17.0.11/java.net.ServerSocket.implAccept(ServerSocket.java:574)
	at java.base@17.0.11/java.net.ServerSocket.accept(ServerSocket.java:532)
	at java.rmi@17.0.11/sun.rmi.transport.tcp.TCPTransport$AcceptLoop.executeAcceptLoop(TCPTransport.java:413)
	...
```

## 全体の実装
この辺りの仕組みを使って、画面からボタンを押すとスレッドダンプが取れる仕組みを作ってみます。APサーバーはSpring bootを使って作成しています。
### サーバーサイド
以下をやっています。
- スレッドの状態ごとにスレッド数を集計
- ダウンロードボタンが押されたとき処理

```Java:サーバーサイド
package com.example.servingwebcontent;

import java.io.IOException;
import java.io.OutputStream;
import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.Map;

import jakarta.servlet.http.HttpServletResponse;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class ThreadDumpController {

	/**
	 * スレッドダンプ取得画面の表示
	 * @param name
	 * @param model
	 * @return
	 */
	@GetMapping("/threadDump")
	public String greeting(Model model) {

		ThreadInfo[] threadInfoArray = getThreadInfoArray();

		// 全体のスレッド数
		int countAll = threadInfoArray.length;

		// 状態別のスレッド数を数える
		Map<Thread.State, Integer> countMap = countThreadState(threadInfoArray);

		model.addAttribute("countAll", countAll);
		model.addAttribute("stateCntNew", countMap.get(Thread.State.NEW));
		model.addAttribute("stateCntRunnable", countMap.get(Thread.State.RUNNABLE));
		model.addAttribute("stateCntBlocked", countMap.get(Thread.State.BLOCKED));
		model.addAttribute("stateCntWaiting", countMap.get(Thread.State.WAITING));
		model.addAttribute("stateCntTimedWaiting", countMap.get(Thread.State.TIMED_WAITING));
		model.addAttribute("stateCntTerminated", countMap.get(Thread.State.TERMINATED));

		return "threadDump";
	}

	/**
	 * 状態別のスレッド数を数える
	 * @param threadInfoArray
	 * @return
	 */
	private Map<Thread.State, Integer> countThreadState(ThreadInfo[] threadInfoArray) {

		Map<Thread.State, Integer> countMap = createCountMap();
		
		for (ThreadInfo threadInfo : threadInfoArray) {
			Thread.State state = threadInfo.getThreadState();
			countMap.put(state, countMap.get(state).intValue() + 1);
		}
		return countMap;
	}

	/**
	 * スレッド情報を取得する。
	 * @return スレッド情報が入っている配列
	 */
	private ThreadInfo[] getThreadInfoArray() {
		ThreadMXBean threadMxBean = ManagementFactory.getThreadMXBean();
		return threadMxBean.dumpAllThreads(false, false);
	}

	/**
	 * 集計用のMapを生成する。
	 * @return 集計用のMap
	 */
	private Map<Thread.State, Integer> createCountMap() {
		Map<Thread.State, Integer> countMap = new HashMap<>();
		countMap.put(Thread.State.NEW, Integer.valueOf(0));
		countMap.put(Thread.State.RUNNABLE, Integer.valueOf(0));
		countMap.put(Thread.State.BLOCKED, Integer.valueOf(0));
		countMap.put(Thread.State.WAITING, Integer.valueOf(0));
		countMap.put(Thread.State.TIMED_WAITING, Integer.valueOf(0));
		countMap.put(Thread.State.TERMINATED, Integer.valueOf(0));
		return countMap;
	}

	/**
	 * スレッドダンプボタンが押されたとき
	 * @param response
	 */
	@GetMapping("/downloadThreadDump")
	public void downloadThreadDump(HttpServletResponse response) {

		ThreadInfo[] threadInfoArray = getThreadInfoArray();

		StringBuilder sb = new StringBuilder();
		String newLine = System.getProperty("line.separator");

		// 状態別のスレッド数を出力
		int countAll = threadInfoArray.length;
		Map<Thread.State, Integer> countMap = countThreadState(threadInfoArray);

		sb.append("全体のスレッド数：" + String.valueOf(countAll) + newLine);
		sb.append("NEW：" + String.valueOf(countMap.get(Thread.State.NEW)) + newLine);
		sb.append("RUNNABLE：" + String.valueOf(countMap.get(Thread.State.RUNNABLE)) + newLine);
		sb.append("BLOCKED：" + String.valueOf(countMap.get(Thread.State.BLOCKED)) + newLine);
		sb.append("WAITING：" + String.valueOf(countMap.get(Thread.State.WAITING)) + newLine);
		sb.append("TIMED_WAITING：" + String.valueOf(countMap.get(Thread.State.TIMED_WAITING)) + newLine);
		sb.append("TERMINATED：" + String.valueOf(countMap.get(Thread.State.TERMINATED)) + newLine + newLine);

		// ダンプファイルを取得した時刻を出力する。
		LocalDateTime nowDate = LocalDateTime.now();

		// スレッドダンプの取得
		sb.append("■ スレッドダンプの取得（" + "取得時刻：" + DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss").format(nowDate) + "）" + newLine);
		for (ThreadInfo threadInfo : threadInfoArray) {
			sb.append(threadInfo.toString());
		}

		// 出力ファイル名にタイムスタンプを付加する。
		String formatNowDate = DateTimeFormatter.ofPattern("yyyyMMddHHmmss").format(nowDate);

		response.setContentType("application/octet-stream");
		response.setHeader("Content-Disposition", "attachment; filename=threaDump" + formatNowDate + ".txt");
		response.setContentLength(sb.toString().getBytes().length);

		try (OutputStream os = response.getOutputStream()) {

			os.write(sb.toString().getBytes());
			os.flush();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
}
```
### クライアントサイド
クライアントサイドはThymeleafを使います。やっているのは以下です。
- スレッドの状態を集計して画面に表示
- スレッドダンプのボタンを表示

```html:threadDump.html
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head> 
    <title>Getting Started: Serving Web Content</title> 
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
	<style type="text/css">
		table {
		  border-collapse: collapse;
		}
		th {
			text-align: left;
		}
		table, th, td {
		  border:1px solid #333;
		  padding: 10px;
		}
		
		.count {
			text-align: right;
		}
		.downloadThreadDump {
			margin:10px;
		}
	</style>
</head>
<body>
	<dev>APサーバー 1号機</dev>
	<h1>スレッドの状態</h1>
	
	<table th:inline="text">
		<tr><th>ALL</th><td class="count">[[${countAll}]]</td><td>全体のスレッド数</td></tr>
		<tr><th>NEW</th><td class="count">[[${stateCntNew}]]</td><td>まだ起動されていないスレッド</td></tr>
		<tr><th>RUNNABLE</th><td class="count">[[${stateCntRunnable}]]</td><td>実行可能なスレッド</td></tr>
		<tr><th>BLOCKED</th><td class="count">[[${stateCntBlocked}]]</td><td>ブロックされ、モニター・ロックを待機しているスレッド</td></tr>
		<tr><th>WAITING</th><td class="count">[[${stateCntWaiting}]]</td><td>待機中のスレッド</td></tr>
		<tr><th>TIMED_WAITING</th><td class="count">[[${stateCntTimedWaiting}]]</td><td>指定された待機時間、待機中のスレッド</td></tr>
		<tr><th>TERMINATED</th><td  class="count">[[${stateCntTerminated}]]</td><td>終了したスレッド</td></tr>
	</table>
	
	<form th:action="@{/downloadThreadDump}" method="get">
		<input id='downloadThreadDump' type="submit" class="downloadThreadDump" value="スレッドダンプのダウンロード"/>
	</form>
</body>
</html>
```

### 画面の様子
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/10723/af2ec2d1-c8cf-3148-4247-0349c29df8b5.png)

### ダウンロードファイル
「スレッドダンプのダウンロード」ボタンを押すと、以下の形式でファイルがダウンロードされます。
- ファイル名
形式：threaDumpYYYYMMDDHHMMSS.txt
例：threaDump20240803174318.txt

```text:ファイルの中身
全体のスレッド数：33
NEW：0
RUNNABLE：15
BLOCKED：0
WAITING：11
TIMED_WAITING：7
TERMINATED：0

■ スレッドダンプの取得（取得時刻：2024/08/03 17:43:18）
"Reference Handler" daemon prio=10 Id=2 RUNNABLE
	at java.base@17.0.11/java.lang.ref.Reference.waitForReferencePendingList(Native Method)
	at java.base@17.0.11/java.lang.ref.Reference.processPendingReferences(Reference.java:253)
	at java.base@17.0.11/java.lang.ref.Reference$ReferenceHandler.run(Reference.java:215)

"Finalizer" daemon prio=8 Id=3 WAITING on java.lang.ref.ReferenceQueue$Lock@9a36226
	at java.base@17.0.11/java.lang.Object.wait(Native Method)
	-  waiting on java.lang.ref.ReferenceQueue$Lock@9a36226
	at java.base@17.0.11/java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:155)
	at java.base@17.0.11/java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:176)
	at java.base@17.0.11/java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:172)

"Signal Dispatcher" daemon prio=9 Id=4 RUNNABLE

"Attach Listener" daemon prio=5 Id=5 RUNNABLE

"Common-Cleaner" daemon prio=8 Id=13 TIMED_WAITING on java.lang.ref.ReferenceQueue$Lock@69349f57
	at java.base@17.0.11/java.lang.Object.wait(Native Method)
	-  waiting on java.lang.ref.ReferenceQueue$Lock@69349f57
	at java.base@17.0.11/java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:155)
	at java.base@17.0.11/jdk.internal.ref.CleanerImpl.run(CleanerImpl.java:140)
	at java.base@17.0.11/java.lang.Thread.run(Thread.java:840)
	at java.base@17.0.11/jdk.internal.misc.InnocuousThread.run(InnocuousThread.java:162)

"JDWP Transport Listener: dt_socket" daemon prio=10 Id=14 RUNNABLE

...（後略）
```

# まとめ
jstackでスレッドダンプを取るケースが多いと思いますが、この方法は、自分の取りたい情報が詳しく取れて、これはこれでいいと思います。もっといろいろな情報が欲しいときは、自分で取得する情報を工夫できるのも利点であると思います。

# 環境
- Spring boot 3.3.0
- Java 17