2020年12月末追記
VSCodeのRemote Developtmentっていうextensionを知って、もうローカルで作業する必要ないじゃん(この記事の内容自分で使わなさそう)、、、となったので一応追記。
やりたかったこと
spring bootアプリをdockerで起動してローカルで開発環境を整える。
公式ドキュメントに従って実行自体はすぐできたが、この方法だとspring-dev-toolsが使えなかった1。
以下の構成にしてマウントしてファイル更新の反映を楽にしたかった。コンテナログインとかsshなしで反映作業をホストで完結させたかった。
以下を行った。
- コンテナとホストの共有ディレクトリを作成(マウント)
→ ローカルのみでファイル変更可能 - spring-dev-toolsをコンテナ内で有効にした(bootRunで実行)
→ javaファイルの変更反映が1コマンドでできる(「イメージ再ビルド」や「コンテナ内でjarビルドし直してアプリ再起動」をする必要がない) 2。F5デバッグ中はhot code deployっていうボタンで反映できる。 - デバッグモードでアプリ実行
→ リモートデバッグができる
完成品
環境
- 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でひな形作成
メモ
- 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されるまでアプリの起動を待つ。
//以下を追加
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
で使う変数を定義。
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"
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を表示するために以下のように変更。
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(デバッガ起動)。
{
"type": "java",
"name": "remote debug",
"request": "attach",
"port": 5050,
"hostName": "[dockerホスト]"
}
ブレークポイントがあればそこで止まるし、なければ素通りしてアプリが起動する。ここでhttp://[dockerホスト]:8080にブラウザアクセスするとHello Worldが表示される。
コード変更
リモートのF5デバッグ中は以下のような画面になる。Application.java
に変更を加えたら上部の稲妻マーク(hot code replace)を押すと変更が反映される5。ブラウザの更新は必要。
メモ
- 画面上部のdisconnectボタンを押すとattachが終わり、アプリも普通に起動した状態になる(ブラウザアクセスも再アタッチもできる)。
アタッチしていない状態でApplication.java
の変更を反映させるには以下手順。-
Application.java
のHello WorldをHello World!に変更 - ローカルで
./gradlew classes
(jarコンパイル) - ブラウザ更新
-
学んだこと
- giboで
.gitignore
が簡単に作れる - リモートデバッグするにはサーバ側でアプリをデバッグモードで実行する必要がある
- spring-dev-toolsは
java -jar
で実行した場合無効になる - マウントしたディレクトリのデータは
docker commit
してもイメージに含まれない -
.env
ファイルに定義した変数をdocker-compose.yaml
で使用できる -
./gradlew
はカレントディレクトリが大事 - gradleは実行環境がないときに使おうとすると(コンテナ内で使おうとすると)
gradle-wrapper.properties
で指定したversionを勝手にダウンロードしてくれる - デバッグ用とアクセス用はポートを分けないと2回目にアクセスできない
- (今回使っていないが、開発環境そのものをdocker仮想化する記事を見つけた)
残った疑問
-
java -jar
で実行するのと./gradlew bootRun
で実行する違い。ここのページに書いてあったけど、まだ力不足でソースが追えず。 - VSCodeのF5デバッグ。あれって結局コマンドラインから再現できるんだろうか。ローカルで試してた時にデバッガをattachじゃなくてlaunchするよう設定していたら、ファイル変更保存即反映って感じだった。どんなコマンドでアプリ実行したらその挙動が真似られるのかわからなかった。
- マウントしたデータは
docker commit
してもイメージに入らない。そうするとdocker化したメリットって結局開発環境のバージョンが好き勝手変えられることだけなんだろうか?(作ったものがdockerイメージとして他の人と受け渡ししやすいっていうのをイメージしていた)
-
公式によると、
java -jar
でアプリを実行すると"production application"とみなされてspring-dev-toolsがdisabledされる。 ↩ ↩2 -
多分jarはコンパイルしないといけないからで、フロント側では効果を発揮してコマンド打たなくてもいいようにしてくれるはず...。 ↩
-
*:
指定はセキュリティ上よくはないそう。 ↩ -
8080のアプリ自体の実行と5050のデバッグの実行が別だということを知らなかった。1回目は8080のアプリが起動される前にattachする(debug実行時のオプション
suspend=y
による)からうまくいっているみたい。 ↩ -
java 8でやってたときはhot code replaceの稲妻ボタンが出てこなかったような気が。。。うろ覚え。 ↩