【Java環境構築】WSL+Docker+JDWP+VSCode でリモートデバッグできる開発環境を構築する

# はじめに

これまで、以下の記事にて Java の開発環境を構築する方法を紹介してきましたが、それぞれ以下の問題がありました。

**[Windows (Local)](https://qiita.com/nimzo6689/items/72898eb9dc3896a41269)**

- テスト環境や本番環境と OS や環境構成が違う

**[WSL](https://qiita.com/nimzo6689/items/c399840b360e409b0c61)**
- Docker と同じ構成を再現できない
- 開発環境の構成をそこまでポータブルに共有できるわけではない
→ Ansible とかを使って構築すればましにはなる。手順の詳細は[こちら](https://qiita.com/nimzo6689/items/74e1d305616259ac75f6)。

**[DevContainer](https://qiita.com/nimzo6689/items/078b65f0503716d9a1e0)**
- メモリ使用量が多いため環境を選ぶ
→ スペックが低めの PC や規模の大きいプロジェクトなどでは採用は厳しい
- IDE によって利用可否が分かれるため、チームで採用するにはその辺の統一が必要
→ 現時点では VSCode と Intellij IDEA の Ultimate 版のみ

そこで、これまでの構築方法の折衷案として WSL で Java の開発環境は構築するが、あくまでコーディングや静的解析だけ WSL で行い、デバッグやテストの実行は Docker コンテナ上で実施する方法を紹介します。

全体のソースコードは GitHub に載せました。

https://github.com/nimzo6689/java-wsl-remote-debug-demo

# 環境構築

まず、 [WSL](https://qiita.com/nimzo6689/items/c399840b360e409b0c61) の記事で説明した通り、 WSL に Java の実行環境を構築します。その後、 [DevContainer](https://qiita.com/nimzo6689/items/078b65f0503716d9a1e0) の記事で説明した通り、 Docker を WSL にインストールします。

VSCode の拡張機能は、 [WSL](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-wsl) と WSL 上に [Extension Pack for Java](https://marketplace.visualstudio.com/items?itemName=vscjava.vscode-java-pack) があれば大丈夫です。

# プロジェクトの作成

毎度、繰り返しになりますが、 VSCode からシンプルな Java アプリケーションを作成します。

1. コマンドパレット（Ctrl+Shift+P）を開く
2. 「Java: Create Java Project」を選択
3. Maven を選択してプロジェクトを作成
4. Group Id や Artifact Id、Destination Folder を選択して、ひな形を生成

## pom.xml の修正

VSCode で作成されたひな形のままですと、現時点では 17 になっているので、21 に変更します。

```diff_xml:pom.xml
   <properties>
+        <maven.compiler.source>21</maven.compiler.source>
+        <maven.compiler.target>21</maven.compiler.target>
    </properties>
</project>
```

## Java コードの修正

続いて、 `Main.java` に Java の実行環境の情報を標準出力するコードを追加します。
これは、 WSL ではなく、 コンテナで実行しているか確認するための確認コードです。

```java:Main.java
public class Main {
    public static void main(String[] args) {
        // Java実行環境の情報
        System.out.println("\n=== Java実行環境情報 ===");
        System.out.println("Javaバージョン: " + System.getProperty("java.version"));
        System.out.println("Javaベンダー: " + System.getProperty("java.vendor"));
        System.out.println("Java VMバージョン: " + System.getProperty("java.vm.version"));
        System.out.println("Java VMベンダー: " + System.getProperty("java.vm.vendor"));
        System.out.println("Java VMname: " + System.getProperty("java.vm.name"));
        System.out.println("Java仕様バージョン: " + System.getProperty("java.specification.version"));
        System.out.println("Java仕様ベンダー: " + System.getProperty("java.specification.vendor"));

        // OS情報
        System.out.println("\n=== OS情報 ===");
        System.out.println("OSの名前: " + System.getProperty("os.name"));
        System.out.println("OSのバージョン: " + System.getProperty("os.version"));
        System.out.println("OSのアーキテクチャ: " + System.getProperty("os.arch"));
        System.out.println("");
    }
}
```

## Dockerfile の作成

Docker イメージは `eclipse-temurin:21` を使っています。
マルチステージビルドにする必要は現段階ではほぼないですが、後々パッケージの追加をする設定を入れた時を考慮して最初から base と final でステージを分けました。

また、ファイルが変更されたら再コンパイルされるように [inotify-tools](https://github.com/inotify-tools/inotify-tools) を使っています。

```Dockerfile:Dockerfile
# Java 21のベースイメージとして Eclipse Temurin を使用
FROM eclipse-temurin:21

# 必要なパッケージのインストール
# - maven: Javaプロジェクトのビルドツール
# - inotify-tools: ファイル変更監視ツール
# インストール後はキャッシュを削除してイメージサイズを削減
RUN apt-get update && \
    apt-get install -y maven inotify-tools && \
    rm -rf /var/lib/apt/lists/*

# 作業ディレクトリを /app に設定
WORKDIR /app

# 開発用スクリプトの作成
# このスクリプトは以下の処理を無限ループで実行:
# 1. mvn compile でソースコードをコンパイル
# 2. コンパイル成功時:
#    - Javaアプリケーションをデバッグモードで起動
#    - ポート5005でリモートデバッグを待ち受け
# 3. inotifywait で src ディレクトリの変更を監視
#    - ファイルの修正/作成/削除を検知したら再度コンパイルを実行
RUN <<EOF cat > /app/dev.sh
#!/bin/sh
while true; do
  mvn compile
  if [ \$? -eq 0 ]; then
    java -agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=*:5005 -cp target/classes com.example.Main
  fi
  inotifywait -e modify -e create -e delete -r ./src
done
EOF

# 開発用スクリプトに実行権限を付与
RUN chmod +x /app/dev.sh

# コンテナ起動時に開発用スクリプトを実行
CMD ["/app/dev.sh"]
```

## Docker Compose ファイルの作成

続いて、 `compose.yaml` ファイルを作成します。
ホスト側での編集がコンテナ側へ即時反映されるようにボリュームを作成しています。

```yaml:compose.yaml
services:
  dev:
    build: .

    volumes:
      # Javaのソースコードディレクトリをマウント
      - ./src:/app/src
      # Maven設定ファイルをマウント
      - ./pom.xml:/app/pom.xml
      # ビルド成果物ディレクトリをマウント（キャッシュ効果でビルドを高速化させる）
      - ./target:/app/target

    ports:
      # リモートデバッグ用ポートの公開（ホスト:コンテナ）
      - "5005:5005"

    # コンテナの疑似TTYを有効化（デバッグ出力の表示用）
    tty: true
```

## launch.json でデバッグ実行の設定を追加

最後に VSCode でリモートデバッグの設定をします。
以下の設定で、 Docker コンテナで実行されている Java アプリケーションに対して、 VSCode からデバッグ接続を確立させることができます。
同様の設定は、他の IDE （Intellij IDEA, Eclipse, NetBeans, etc）でもできるので、 VSCode 以外でもリモートデバッグは可能です。（ WSL で実行する必要はあります。ただ、筆者の方で試してはいません。）

```json:.vscode/launch.json
{
    "version": "0.2.0",
    "configurations": [
        {
            "type": "java",
            "name": "Docker Debug",
            "request": "attach",
            "hostName": "localhost",
            "port": 5005,
            "projectName": "java-wsl-remote-debug-demo"
        }
    ]
}
```

# デバッグ実行を試す

準備ができたので、コンテナを立ち上げてデバッグ実行してみます。

コンテナを立ち上げる際に、イメージを再ビルドして、フォアグラウンド実行でログを直接表示させたいので、 `docker compose up -d`（既存イメージを使用して、バッググランド実行） ではなく、 `--build` としています。

```bash:bash on WSL（VSCode 内のターミナル（Ctrl+Shift+@）で実施するのが便利です）
docker compose up --build
```

コンテナの起動後、 `Main.java` にブレークポイントを貼って F5 でデバッグ実行すれば下のスクショのようにステップ実行できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/114728/2214d04d-5c49-b528-c692-3e390e8fb8f1.png)

また、 Java のソースコードや pom.xml ファイルを修正するたびに自動でコンパイルが走るようになっているので、いちいちコンテナを再ビルドする操作をしなくても、デバッガのセッションを維持した状態で再度デバッグ実行ができます。
そのため、操作感としては WSL 上で Java のデバッグ実行をしている時とほぼ変わりません。

デバッグ用に起動したコンテナは、 Ctrl+C で停止できます。

# おわりに

今回のアドカレで Java の開発環境の構築パターンを５つ紹介してきましたが、特にどれがおススメという意見はなく、各開発現場の状況に応じてどの方法を採用するか決めていただくことになるかと思います。

ただ、どの方法で開発環境を構築するか次第で、現場の開発体験が変わってくるというのは確かにあるので、自戒も含めてですが、できれば全パターン検討するようにしたいですし、また、プロジェクトの成長によって適宜見直して行くようにしたいですね。