Spring boot (with spring-boot-devtools) + Docker でリモートデバッグ環境を整える

2020年12月末追記
VSCodeのRemote Developtmentっていうextensionを知って、もうローカルで作業する必要ないじゃん(この記事の内容自分で使わなさそう)、、、となったので一応追記。

## やりたかったこと
spring bootアプリをdockerで起動してローカルで開発環境を整える。
[公式ドキュメント](https://spring.pleiades.io/guides/gs/spring-boot-docker/)に従って実行自体はすぐできたが、この方法だとspring-dev-toolsが使えなかった[^1]。
以下の構成にしてマウントしてファイル更新の反映を楽にしたかった。コンテナログインとかsshなしで反映作業をホストで完結させたかった。
![spring構成.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/490290/b11e9e04-5e9b-b9bc-7405-28d85c2624e7.jpeg)


以下を行った。

- コンテナとホストの共有ディレクトリを作成(マウント)  
→ ローカルのみでファイル変更可能
- spring-dev-toolsをコンテナ内で有効にした(bootRunで実行)  
→ javaファイルの変更反映が1コマンドでできる(「イメージ再ビルド」や「コンテナ内でjarビルドし直してアプリ再起動」をする必要がない) [^2]。F5デバッグ中はhot code deployっていうボタンで反映できる。
- デバッグモードでアプリ実行  
→ リモートデバッグができる

## 完成品
https://github.com/PG-practice/lyricWall/tree/setup_DEV_environment

## 環境
- VSCode 1.43.2 (spring boot tools (ver. 1.16.0-RC.2)っていうextension一応入れたけど今回は使ってないはず)
- spring boot 2.2.6
- Windows10
- VMWare上VM Ubuntu 18.04.3 LTS
- docker 19.03.6
- docker-compose version 1.17.1
- gradle 6.3

## 手順
### spring initializerでひな形作成
ひな形作成した。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/490290/bd7a2d4f-c017-de01-ab4f-481c15b437eb.png)

#### メモ
- java8で最初作っていて後から11に変更したが、以下2点の変更だけでもよさそう。
    - `build.gradle`の`sourceCompatibility`
    - dockerのベースイメージ
- Dependenciesで今回使っているのはSpring Boot DevToolsのみ。

### `build.gradle`に追記
以下を追記した。これで`./gradlew bootRun`を実行すると`./gradlew bootRun -agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=*:5050`に変換されて実行される。
これによりデバッグモード実行になって外部からのデバッガアタッチを許可する。`suspend=y`を設定するとデバッガがattachされるまでアプリの起動を待つ。

```gradle:build.gradle
//以下を追加
bootJar {
    baseName = 'lyricWallSpring'
    version =  '0.1.0'
}
//debug用にgradleがJVMに引数を渡せる必要がある
bootRun {
   systemProperties = System.properties  //左がjdkの引数、右がgradleの引数らしい。
   //上記の行により、gradle実行時の以下のコマンドライン引数がjdkに渡される。
   jvmArgs=["-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=*:5050"]
}
//以上を追加
```

#### メモ
- Java 11では`address=*:5050`の形式でホスト指定しないとデバッガのattach時に以下のエラーが出て失敗する[^3]。Java 8では`address=5050`でattachできる。

> Failed to attach remote debuggee VM. Reason: java.net.ConnectException: Connection timed out: connect

- spring bootアプリのデフォルトポートは8080で、デバッガのlisten addressはそれ以外にしないといけない。デバッグ用ポートを8080に指定すると1回目のデバッガアタッチは成功するのだが、切断→2回目のアタッチをしようとしたときに`transport error 202: bind failed: Address in use`だの`transport dt_socket failed to initialize`だのエラーを吐く[^4]。

### `docker-compose.yaml`作成
以下の2ファイルを作成。`.env`で`docker-compose.yaml`で使う変数を定義。

```yaml:docker-compose.yaml
version: '3.5'

services:
    spring:
        image: "adoptopenjdk/openjdk11:jdk-11.0.6_10-alpine"
        volumes: 
            - "./spring:${SPRING_PATH_IN_CONTAINER}"
        ports:
            - "8080:8080"
            # デバッグ用ポート
            - "5050:5050"
        working_dir: ${SPRING_PATH_IN_CONTAINER}
        # aplineにはbashがなくashを使用
        command: /bin/ash -c "./gradlew bootRun"
```
```:.env
SPRING_PATH_IN_CONTAINER=/app/spring
```
#### メモ
- `Dockerfile`は使っていない。
- `java -jar`ではなく`./gradlew bootRun`によって実行することで`spring-dev-tools`が有効になる[^1]。
- `working_dir`の指定なしで`command: /bin/ash -c "${SPRING_PATH_IN_CONTAINER}/gradlew bootRun"`と絶対パスを指定していたら以下のエラーが出た。gradleのディレクトリで実行しないとダメなよう。

> FAILURE: Build failed with an exception.

> What went wrong:
> A problem occurred configuring root project ''.
> The project name must not be empty. Set the 'rootProject.name' or adjust the 'include' statement (see https://docs.gradle.org/6.3/dsl/org.gradle.api.initialization.Settings.html#org.gradle.api.initialization.Settings:include(java.lang.String[]) for more details).


### `Application.java`変更
なんでかよくわからんけどinitializerで作ったばっかりのひな形にアクセスしたらルーティングができてなくてerrorページが表示されたので、Hello Worldを表示するために以下のように変更。

```java:Application.java
package lyricWallGroup.spring;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@SpringBootApplication
@RestController
public class Application {

	@RequestMapping("/")
	public String home() {
	  return "Hello World";
	}

	public static void main(String[] args) {
		SpringApplication.run(Application.class, args);
	}

}
```


### `.gitignore`作成
giboっていうOSS初めて使った。使い方が簡単で、docker環境があれば以下のコマンドを打つだけでカレントディレクトリに`.gitignore`のテンプレを作ってくれた。`sudo docker run --rm simonwhitaker/gibo list`から使用したソフトを探して以下のように書く。今回はJavaと VisualStudioCodeとGradle。

```
sudo docker run --rm simonwhitaker/gibo dump Java  VisualStudioCode Gradle >> .gitignore
```

#### メモ
- `.gitignore`を作り直したときに変更が反映されない場合がある。"gitignore キャッシュ"とかでググって解決。

### アプリデバッグ実行
`docker-compose.yaml`があるディレクトリで以下を実行してコンテナ起動。
`sudo docker-compose up -d`
このとき`docker-compose.yaml`で定義したコマンド`./gradlew bootRun`がコンテナ内で実行される(`build.gradle`で定義したjvmArgsによりデバッグ実行される)。
2分くらい待つとアプリの起動準備が整う(gradleのダウンロードとかに時間がかかる)。
通常の実行ならばこの段階でhttp://[dockerホスト]:8080 にブラウザからアクセスするとHello Worldが表示されるが、
今はデバッグ実行で`suspend=y`オプションをつけているので、デバッガをアタッチするまでアクセスできない(起動していない)。

### デバッガアタッチ
VSCodeでF5を押すと`launch.json`が作成される。以下のような項目を追加し、その項目を選択して再度F5(デバッガ起動)。

```json:launch.json
        {
            "type": "java",
            "name": "remote debug",
            "request": "attach",
            "port": 5050,
            "hostName": "[dockerホスト]"
        }
```
ブレークポイントがあればそこで止まるし、なければ素通りしてアプリが起動する。ここでhttp://[dockerホスト]:8080にブラウザアクセスするとHello Worldが表示される。

### コード変更
リモートのF5デバッグ中は以下のような画面になる。`Application.java`に変更を加えたら上部の稲妻マーク(hot code replace)を押すと変更が反映される[^5]。ブラウザの更新は必要。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/490290/1e39d7cc-e342-0236-80c5-95ab8e0539f0.png)

#### メモ
- 画面上部のdisconnectボタンを押すとattachが終わり、アプリも普通に起動した状態になる(ブラウザアクセスも再アタッチもできる)。
アタッチしていない状態で`Application.java`の変更を反映させるには以下手順。
    1. `Application.java`のHello WorldをHello World!に変更
    2. ローカルで`./gradlew classes`(jarコンパイル)
    3. ブラウザ更新



## 学んだこと
- giboで`.gitignore`が簡単に作れる
- リモートデバッグするにはサーバ側でアプリをデバッグモードで実行する必要がある
- spring-dev-toolsは`java -jar`で実行した場合無効になる
- マウントしたディレクトリのデータは`docker commit`してもイメージに含まれない
- `.env`ファイルに定義した変数を`docker-compose.yaml`で使用できる
- `./gradlew`はカレントディレクトリが大事
- gradleは実行環境がないときに使おうとすると(コンテナ内で使おうとすると)`gradle-wrapper.properties`で指定したversionを勝手にダウンロードしてくれる
- デバッグ用とアクセス用はポートを分けないと2回目にアクセスできない
- (今回使っていないが、開発環境そのものをdocker仮想化する記事を見つけた)

## 残った疑問
1. `java -jar`で実行するのと`./gradlew bootRun`で実行する違い。[ここのページ](https://living-sun.com/ja/java/483597-spring-boot-application-difference-in-startup-time-with-ldquomvn-spring-bootrunrdquo-and-ldquojava-jarrdquo-java-spring-maven-spring-mvc-spring-boot.html)に書いてあったけど、まだ力不足でソースが追えず。
2. VSCodeのF5デバッグ。あれって結局コマンドラインから再現できるんだろうか。ローカルで試してた時にデバッガをattachじゃなくてlaunchするよう設定していたら、ファイル変更保存即反映って感じだった。どんなコマンドでアプリ実行したらその挙動が真似られるのかわからなかった。
3. マウントしたデータは`docker commit`してもイメージに入らない。そうするとdocker化したメリットって結局開発環境のバージョンが好き勝手変えられることだけなんだろうか？(作ったものがdockerイメージとして他の人と受け渡ししやすいっていうのをイメージしていた)

[^1]: [公式](https://docs.spring.io/spring-boot/docs/current/reference/html/using-spring-boot.html#using-boot-devtools)によると、`java -jar`でアプリを実行すると"production application"とみなされてspring-dev-toolsがdisabledされる。

[^2]: 多分jarはコンパイルしないといけないからで、フロント側では効果を発揮してコマンド打たなくてもいいようにしてくれるはず...。
[^3]: `*:`指定はセキュリティ上よくはないそう。
[^4]: 8080のアプリ自体の実行と5050のデバッグの実行が別だということを知らなかった。1回目は8080のアプリが起動される前にattachする(debug実行時のオプション`suspend=y`による)からうまくいっているみたい。
[^5]: java 8でやってたときはhot code replaceの稲妻ボタンが出てこなかったような気が。。。うろ覚え。