LoginSignup
22
22

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-04-12

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

やりたかったこと

spring bootアプリをdockerで起動してローカルで開発環境を整える。
公式ドキュメントに従って実行自体はすぐできたが、この方法だとspring-dev-toolsが使えなかった1
以下の構成にしてマウントしてファイル更新の反映を楽にしたかった。コンテナログインとかsshなしで反映作業をホストで完結させたかった。
spring構成.jpeg

以下を行った。

  • コンテナとホストの共有ディレクトリを作成(マウント)
    → ローカルのみでファイル変更可能
  • 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でひな形作成

ひな形作成した。
image.png

メモ

  • java8で最初作っていて後から11に変更したが、以下2点の変更だけでもよさそう。
    • build.gradlesourceCompatibility
    • 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されるまでアプリの起動を待つ。

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ファイルを作成。.envdocker-compose.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を表示するために以下のように変更。

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(デバッガ起動)。

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

メモ

  • 画面上部の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で実行する違い。ここのページに書いてあったけど、まだ力不足でソースが追えず。
  2. VSCodeのF5デバッグ。あれって結局コマンドラインから再現できるんだろうか。ローカルで試してた時にデバッガをattachじゃなくてlaunchするよう設定していたら、ファイル変更保存即反映って感じだった。どんなコマンドでアプリ実行したらその挙動が真似られるのかわからなかった。
  3. マウントしたデータはdocker commitしてもイメージに入らない。そうするとdocker化したメリットって結局開発環境のバージョンが好き勝手変えられることだけなんだろうか?(作ったものがdockerイメージとして他の人と受け渡ししやすいっていうのをイメージしていた)

  1. 公式によると、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の稲妻ボタンが出てこなかったような気が。。。うろ覚え。 

22
22
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
22
22