かんがるーさんの日記

最近自分が興味をもったものを調べた時の手順等を書いています。今は Spring Boot をいじっています。

Grooy スクリプトをそのまま渡して実行する Spring Boot+Picocli ベースのコマンドラインアプリを作成する ( その10 )( GraalVM で groovy-script-executor の Windows 版 Native Image を作成する )

概要

記事一覧はこちらです。

Grooy スクリプトをそのまま渡して実行する Spring Boot+Picocli ベースのコマンドラインアプリを作成する ( その9 )( Gradle を 7.2 → 7.3.3 へ、Spring Boot を 2.5.6 → 2.6.2 へバージョンアップする ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • GraalVM で groovy-script-executor の Windows 版 Native Image を作成します。

参照したサイト・書籍

  1. Using GraalVM and Native Image on Windows 10
    https://medium.com/graalvm/using-graalvm-and-native-image-on-windows-10-9954dc071311

  2. Build Great Native CLI Apps in Java with Graalvm and Picocli
    https://www.infoq.com/articles/java-native-cli-graalvm-picocli/

  3. 2.3.2. Enabling the Annotation Processor
    https://picocli.info/#_enabling_the_annotation_processor

  4. Visual Studio - ダウンロード
    https://visualstudio.microsoft.com/ja/downloads/

  5. GraalVM
    https://www.graalvm.org/

目次

  1. build.gradle に annotationProcessor("info.picocli:picocli-codegen:${picocliVersion}") を追加する
  2. Visual Studio 2022 Community をインストールする
  3. GraalVM Community 21.3.0 をインストールする
  4. groovy-script-executor の Windows 版 Native Image(gse.exe、gse-servlet.exe)を作成する
  5. 動作確認

手順

build.gradle に annotationProcessor("info.picocli:picocli-codegen:${picocliVersion}") を追加する

https://picocli.info/#_enabling_the_annotation_processor を参考に build.gradle を以下のように変更します。

..........

[compileJava, compileTestGroovy, compileTestJava]*.options*.encoding = "UTF-8"
[compileJava, compileTestGroovy, compileTestJava]*.options*.compilerArgs = [
        "-Xlint:all,-options,-processing,-path",
        "-Aproject=${project.group}/${project.name}"
]
..........

dependencies {
    ..........
    def picocliVersion = "4.6.2"
    ..........

    // for Picocli
    implementation("info.picocli:picocli-spring-boot-starter:${picocliVersion}")
    annotationProcessor("info.picocli:picocli-codegen:${picocliVersion}")

    ..........
  • [compileJava, compileTestGroovy, compileTestJava]*.options*.compilerArgs"-Aproject=${project.group}/${project.name}" を追加します。
  • dependencies block に以下の行を追加します。
    • annotationProcessor("info.picocli:picocli-codegen:${picocliVersion}")

Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。

clean タスク実行 → Rebuild Project 実行 → build タスクを実行して生成された build/libs/groovy-script-executor-1.0.0-RELEASE.jar を D:\tmp にコピーします。

Visual Studio 2022 Community をインストールする

Windows 版 Native Image を作成するのに Visual Studio 2022 Community が必要なのでインストールします。

ダウンロード ページの「コミュニティ」の「無料ダウンロード」ボタンをクリックして vs_community__~.exe(~ の部分には英数字の文字列が入ります)をダウンロードします。

f:id:ksby:20211231155617p:plain

vs_community__~.exe を実行してインストールします。以下の画面が表示されたら「個別のコンポーネント」の「MSVC v143 VS 2022 C++ x64/x86 ビルドツール(最新)」をチェックした後「インストール」ボタンをクリックします。

f:id:ksby:20211231201204p:plain

インストールが完了すると「Visual Studio Installer」に「Visual Studio Community 2022」が表示されます。

f:id:ksby:20211231161029p:plain

GraalVM Community 21.3.0 をインストールする

https://www.graalvm.org/downloads/ の「GraalVM Community 21.3.0」の「DOWNLOAD FROM GITHUB」ボタンをクリックすると https://github.com/graalvm/graalvm-ce-builds/releases/tag/vm-21.3.0 へ遷移するので、その下の「Java 17 based」の「Windows (amd64)」の link をクリックして graalvm-ce-java17-windows-amd64-21.3.0.zip をダウンロードします。

D:\graalvm フォルダを新規作成した後、graalvm-ce-java17-windows-amd64-21.3.0.zip を解凍して作成された graalvm-ce-java17-21.3.0 フォルダをその下に移動します。

groovy-script-executor の Windows 版 Native Image(gse.exe、gse-servlet.exe)を作成する

Windows メニューから「Visual Studio 2022」-「x64 Native Tools Command Prompt for VS 2022」をクリックして実行します。

f:id:ksby:20211231201849p:plain

以下のコマンドを実行し native-image コマンドを使用できるようにします。

> cd /d d:\tmp
> set PATH=D:\graalvm\graalvm-ce-java17-21.3.0\bin;%PATH%
> gu install native-image

f:id:ksby:20211231202333p:plain

以下のコマンドを実行し gse.exe を作成します。gse.exe の後に指定しているのは gse.bat 内に記述していた起動時オプションです。ただし -XX:TieredStopAtLevel=1 は指定できませんでした(エラーメッセージが出力されて Native Image の生成が失敗します)。

> native-image -jar groovy-script-executor-1.0.0-RELEASE.jar gse.exe -Dfile.encoding=UTF-8 -Dspring.main.lazy-initialization=true

f:id:ksby:20211231202754p:plain

以下のコマンドを実行し gse-servlet.exe を作成します。gse-servlet.exe の後に指定しているのは gse-servlet.bat 内に記述していた起動時オプションです。

> native-image -jar groovy-script-executor-1.0.0-RELEASE.jar gse-servlet.exe -Dfile.encoding=UTF-8 -Dspring.main.lazy-initialization=true -Dspring.main.web-application-type=servlet -Dlogging.level.root=INFO

f:id:ksby:20211231204211p:plain

動作確認

まずは gse.exe の動作確認を行います。

gse.exe Helloworld.groovy を実行すると Hello, World の文字が出力されました。起動後に4~5秒かかるのは Groovy スクリプトを build している時間なので Native Image にしても変わりませんでした。

f:id:ksby:20220101103325p:plain

docker-compose up -d でコンテナを起動して publications.csv を削除してから gse.exe PublicationsTableToFileUsingUnivocityParsers.groovy を実行するとコマンドはエラーメッセージを出力せずに終了し、

f:id:ksby:20220101104706p:plain

publications.csv が生成されてデータが出力されていました。

f:id:ksby:20220101104842p:plain

gse.exe SftpClient.groovy --user=user01 --password=pass01 --upload-dir=upload --upload-file=publications.csv コマンドを実行するとログが出力されて、

f:id:ksby:20220101105210p:plain

upload ディレクトリに publications.csv がアップロードされました。

f:id:ksby:20220101105100p:plain

次に gse-servlet.exe の動作確認を行います。

gse.exe StubServer.groovy を実行すると何も出力されずにコマンドが終了しますが、

f:id:ksby:20220101105350p:plain

gse-servlet.exe StubServer.groovy を実行するとログが出力されて Tomcat が起動し、

f:id:ksby:20220101105609p:plain

curl -v http://localhost:9080/stub を実行すると {"key":123,"data":"xxxxxxxx"} が返ってきました。

f:id:ksby:20220101105705p:plain

gse.exe、gse-servlet.exe どちらも問題なさそうです。

また環境変数 PATH から JDKD:\java\jdk-17.0.1.12-hotspot\bin)への PATH を削除してから gse.exe Helloworld.groovy を実行してもコマンドが実行されました。作成された Native Image は JDK がなくても動作します(Groovy スクリプトの build にも JDK は必要ないようです)。

f:id:ksby:20220101110135p:plain

履歴

2022/01/01
初版発行。

Grooy スクリプトをそのまま渡して実行する Spring Boot+Picocli ベースのコマンドラインアプリを作成する ( その9 )( Gradle を 7.2 → 7.3.3 へ、Spring Boot を 2.5.6 → 2.6.2 へバージョンアップする )

概要

記事一覧はこちらです。

Grooy スクリプトをそのまま渡して実行する Spring Boot+Picocli ベースのコマンドラインアプリを作成する ( その8 )( @SpringBootApplication アノテーションを付与した Groovy スクリプトで REST API サーバを作成する ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • Gradle を 7.2 → 7.3.3 へ、Spring Boot を 2.5.6 → 2.6.2 へバージョンアップします。

参照したサイト・書籍

目次

  1. Gradle を 7.2 → 7.3.3 へバージョンアップする
  2. Spring Boot を 2.5.6 → 2.6.2 へバージョンアップする

手順

Gradle を 7.2 → 7.3.3 へバージョンアップする

コマンドプロンプトから gradlew wrapper --gradle-version=7.3.3gradlew --version コマンドを実行します。

f:id:ksby:20211229162026p:plain

gradle/wrapper/gradle-wrapper.properties は以下の内容になります。

distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。

Spring Boot を 2.5.6 → 2.6.2 へバージョンアップする

build.gradle の以下の点を変更します。

..........

plugins {
    id 'java'
    id 'groovy'
    id 'org.springframework.boot' version '2.6.2'
    id 'io.spring.dependency-management' version '1.0.11.RELEASE'
}

..........

dependencyManagement {
    imports {
        // bomProperty に指定可能な property は以下の URL の BOM に記述がある
        // https://repo1.maven.org/maven2/org/springframework/boot/spring-boot-dependencies/2.5.6/spring-boot-dependencies-2.5.6.pom
        mavenBom(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES) {
            // Spring Boot の BOM に定義されているバージョンから変更する場合には、ここに以下のように記述する
            // bomProperty "thymeleaf.version", "3.0.9.RELEASE"
        }
        mavenBom("org.junit:junit-bom:5.8.2")
    }
}

dependencies {
    ..........
    def picocliVersion = "4.6.2"
    ..........

    // dependency-management-plugin によりバージョン番号が自動で設定されないもの、あるいは最新バージョンを指定したいもの
    ..........
    implementation("net.logstash.logback:logstash-logback-encoder:7.0.1")
    ..........
  • plugins block の以下の点を変更します。
    • id 'org.springframework.boot' version '2.5.6'id 'org.springframework.boot' version '2.6.2'

各種ライブラリのバージョンアップとして以下の点を変更します。

  • dependencyManagement block の以下の点を変更します。
    • mavenBom("org.junit:junit-bom:5.8.1")mavenBom("org.junit:junit-bom:5.8.2")
  • dependencies block の以下の点を変更します。
    • def picocliVersion = "4.6.1"def picocliVersion = "4.6.2"
    • implementation("net.logstash.logback:logstash-logback-encoder:6.6")implementation("net.logstash.logback:logstash-logback-encoder:7.0.1")

Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。

clean タスク実行 → Rebuild Project 実行 → build タスクを実行して、"BUILD SUCCESSFUL" のメッセージが出力されることを確認します。

f:id:ksby:20211229172305p:plain

履歴

2021/12/29
初版発行。

Eclipse Temurin を 17+35 → 17.0.1+12 へ、IntelliJ IDEA を 2021.2.3 → 2021.2.4 へ、Git for Windows を 2.33.1 → 2.34.1 へバージョンアップ

Eclipse Temurin を 17+35 → 17.0.1+12 へバージョンアップする

※ksbysample-webapp-lending プロジェクトを開いた状態でバージョンアップしています。

  1. https://adoptium.net/index.html?variant=openjdk17&jvmVariant=hotspot から OpenJDK17U-jdk_x64_windows_hotspot_17.0.1_12.msi をダウンロードします。

    f:id:ksby:20211229035223p:plain

  2. インストール時に削除されるかもしれないので D:\Java\jdk-17.0.0.35-hotspot → D:\Java\jdk-17.0.0.35-hotspotx にリネームします。

  3. インストーラーを実行して D:\Java\jdk-17.0.1.12-hotspot へインストールした後、環境変数 JAVA_HOME のパスを D:\Java\jdk-17.0.1.12-hotspot へ変更します。

    コマンドプロンプトから java -version を実行し、17.0.1 に変更されていることを確認します。

    f:id:ksby:20211229035957p:plain

  4. D:\Java\jdk-17.0.0.35-hotspotx → D:\Java\jdk-17.0.0.35-hotspot に戻します。

  5. IntelliJ IDEA を再起動します。

  6. ksbysample-webapp-lending プロジェクトが使用する JDK を 17.0.1.12 に変更します。

  7. IntelliJ IDEA のメイン画面が開いたら、メニューから「File」-「Project Structure...」を選択します。

  8. 「Project Structure」ダイアログが表示されます。「Project SDK」で D:\Java\jdk-17.0.1.12-hotspot を選択します。

    f:id:ksby:20211229053744p:plain

  9. 「Project SDK」の「Edit」ボタンをクリックします。

    f:id:ksby:20211229053858p:plain

  10. 「Project Structure」ダイアログが表示されます。画面左側で「Platform Settings」-「SDKs」を選択して、中央のリストから「17+35」を選択した後、リストの上の「-」ボタンをクリックして削除します。

    f:id:ksby:20211229054143p:plain

  11. 中央のリストから「11」を選択した後、"11" → "17.0.1.12" へ変更します。

    f:id:ksby:20211229054509p:plain

  12. 「OK」ボタンをクリックして「Project Structure for New Projects」ダイアログを閉じます。

  13. メイン画面に戻ると画面右下に「Indexing...」の表示が出るので、終了するまで待ちます。

  14. Gradle Tool Window の左上にある「Refresh All Gradle Projects」ボタンをクリックして更新します。

  15. clean タスク実行 → Rebuild Project 実行 → build タスクを実行して、"BUILD SUCCESSFUL" のメッセージが出力されることを確認します。

    f:id:ksby:20211229063131p:plain

  16. Project Tool Window で src/test でコンテキストメニューを表示して「More Run/Debug」-「Run 'All Tests' with Coverage」を選択し、テストが全て成功することを確認します。

    f:id:ksby:20211229063648p:plain

  17. 特に問題は発生しませんでした。Eclipse Temurin 17.0.1+12 で開発を進めます。

IntelliJ IDEA を 2021.2.3 → 2021.2.4 へバージョンアップする

IntelliJ IDEA の 2021.2.4 がリリースされているのでバージョンアップします。

※ksbysample-webapp-lending プロジェクトを開いた状態でバージョンアップしています。

  1. IntelliJ IDEA のメインメニューから「Help」-「Check for Updates...」を選択します。

  2. IntelliJ IDEA and Plugin Updates」ダイアログが表示されます。右下に「Update and Restart」ボタンが表示されていますので、「Update and Restart」ボタンをクリックします。

    f:id:ksby:20211229092220p:plain

  3. Plugin の update も表示されました。このまま「Update」ボタンをクリックします。

    f:id:ksby:20211229092329p:plain

  4. Patch がダウンロードされて IntelliJ IDEA が再起動します。

  5. IntelliJ IDEA が起動すると画面下部に「Indexing」のメッセージが表示されますので、終了するまで待機します。

  6. IntelliJ IDEA のメインメニューから「Help」-「About」を選択し、2021.2.4 へバージョンアップされていることを確認します。

  7. clean タスク実行 → Rebuild Project 実行 → build タスクを実行して、"BUILD SUCCESSFUL" のメッセージが出力されることを確認します。

    f:id:ksby:20211229095040p:plain

  8. Project Tool Window で src/test でコンテキストメニューを表示して「More Run/Debug」-「Run 'All Tests' with Coverage」を選択し、テストが全て成功することを確認します。

    f:id:ksby:20211229095505p:plain

Git for Windows を 2.33.1 → 2.34.1 へバージョンアップする

Git for Windows の 2.34.1 がリリースされていたのでバージョンアップします。

  1. https://gitforwindows.org/ の「Download」ボタンをクリックして Git-2.34.1-64-bit.exe をダウンロードします。

  2. Git-2.34.1-64-bit.exe を実行します。

  3. 「Git 2.34.1 Setup」ダイアログが表示されます。インストーラーの画面を一通り見たいので「Only show new options」のチェックを外してから [Next >] ボタンをクリックします。

  4. 「Select Components」画面が表示されます。「Git LFS(Large File Support)」だけチェックした状態で [Next >]ボタンをクリックします。

  5. 「Choosing the default editor used by Git」画面が表示されます。「Use Vim (the ubiquitous text editor) as Git's default editor」が選択された状態で [Next >]ボタンをクリックします。

  6. 「Adjusting the name of the initial branch in new repositories」画面が表示されます。「Let Git decide」が選択されていることを確認後、[Next >]ボタンをクリックします。

  7. 「Adjusting your PATH environment」画面が表示されます。中央の「Git from the command line and also from 3rd-party software」が選択されていることを確認後、[Next >]ボタンをクリックします。

  8. 「Choosing the SSH executable」画面が表示されます。「Use bundled OpenSSL」が選択されていることを確認後、[Next >]ボタンをクリックします。

  9. 「Choosing HTTPS transport backend」画面が表示されます。「Use the OpenSSL library」が選択されていることを確認後、[Next >]ボタンをクリックします。

  10. 「Configuring the line ending conversions」画面が表示されます。一番上の「Checkout Windows-style, commit Unix-style line endings」が選択されていることを確認した後、[Next >]ボタンをクリックします。

  11. 「Configuring the terminal emulator to use with Git Bash」画面が表示されます。「Use Windows'default console window」が選択されていることを確認した後、[Next >]ボタンをクリックします。

  12. 「Choose the default behavior of git pull」画面が表示されます。「Default (fast-forward or merge)」が選択されていることを確認した後、[Next >]ボタンをクリックします。

  13. 「Choose a credential helper」画面が表示されます。今回は「Git Credential Manager」を選択した後、[Next >]ボタンをクリックします。

  14. 「Configuring extra options」画面が表示されます。「Enable file system caching」だけがチェックされていることを確認した後、[Next >]ボタンをクリックします。

  15. 「Configuring experimental options」画面が表示されます。何もチェックせずに [Install]ボタンをクリックします。

  16. インストールが完了すると「Completing the Git Setup Wizard」のメッセージが表示された画面が表示されます。中央の「View Release Notes」のチェックを外した後、[Next >]ボタンをクリックしてインストーラーを終了します。

  17. コマンドプロンプトを起動して git --version を実行し、git のバージョンが git version 2.34.1.windows.1 になっていることを確認します。

    f:id:ksby:20211229100617p:plain

  18. 特に問題はないようですので、2.34.1 で作業を進めたいと思います。

Grooy スクリプトをそのまま渡して実行する Spring Boot+Picocli ベースのコマンドラインアプリを作成する ( 番外編 )( ConEmu 上で WSL 2 の Ubuntu-20.04 の bash を起動する+Ubuntu-20.04 の別インスタンスを作成する )

概要

記事一覧はこちらです。

GraalVM で groovy-script-executor の Linux 用の Native Image を作成するために WSL 2 で稼働する Ubuntu-20.04 に容易にアクセスする方法等を調べます。

参照したサイト・書籍

  1. Bash on Ubuntu on Windows (WSL)
    https://conemu.github.io/en/wsl.html

  2. WSL: Am I running version 1 or version 2?
    https://askubuntu.com/questions/1177729/wsl-am-i-running-version-1-or-version-2

  3. Windows への PowerShell のインストール
    https://docs.microsoft.com/ja-jp/powershell/scripting/install/installing-powershell-on-windows?view=powershell-7.2

  4. PowerShell / PowerShell - Releases
    https://github.com/PowerShell/PowerShell/releases/

  5. How to install multiple instances of Ubuntu in WSL2
    https://cloudbytes.dev/snippets/how-to-install-multiple-instances-of-ubuntu-in-wsl2

  6. WSL の基本的なコマンド
    https://docs.microsoft.com/ja-jp/windows/wsl/basic-commands

  7. Developing on Amazon Linux 2 using Windows
    https://aws.amazon.com/jp/blogs/developer/developing-on-amazon-linux-2-using-windows/

  8. yosukes-dev / AmazonWSL - Releases
    https://github.com/yosukes-dev/AmazonWSL/releases

目次

  1. Ubuntu-20.04 を以前インストールはしていたが、WSL 1 or WSL 2 のどちらで動いているのか?
  2. ConEmu で {Bash::bash} を起動しようとすると wslbridge error: failed to start backend process のエラーが出力されて起動しない問題を解消する
  3. Ubuntu-20.04 の別インスタンスを起動する

手順

Ubuntu-20.04 を以前インストールはしていたが、WSL 1 or WSL 2 のどちらで動いているのか?

WSL: Am I running version 1 or version 2? を読むと wsl -l -v コマンドで分かるそうなので実行してみると、Ubuntu-20.04 は VERSION 1(WSL 1) になっていました。

f:id:ksby:20211223041509p:plain

wsl --set-version Ubuntu-20.04 2 コマンドを実行して VERSION 2(WSL 2)に変換します。

f:id:ksby:20211223041739p:plain

ConEmu で {Bash::bash} を起動しようとすると wslbridge error: failed to start backend process のエラーが出力されて起動しない問題を解消する

WSL 2 に変換後、ConEmu で {Bash::bash} を起動しようとすると以下のエラーメッセージが表示されて起動できなくなるので、起動できるようにします。

f:id:ksby:20211223042248p:plain

まず ConEmu の左上のアイコンをクリックしてメニューを表示後、「Settings...」を選択します。

f:id:ksby:20211223043105p:plain

「Settings」ダイアログが表示されるので、左側のツリーから「Startup」-「Tasks」を選択した後、中央のリストから {Bash::bash} を選択し、右側の「Commands」を

  • set "PATH=%ConEmuBaseDirShort%\wsl;%PATH%" & %ConEmuBaseDirShort%\conemu-cyg-64.exe --wsl -cur_console:pm:/mntwsl.exe -cur_console:pm:/mnt

に変更して「Save settings」ボタンをクリックして保存します。

f:id:ksby:20211223043430p:plain

再度 {Bash::bash} を起動しようとすると今度は無事起動しました。タブのアイコンもペンギンになっていました。

f:id:ksby:20211223043918p:plain

Ubuntu-20.04 の別インスタンスを起動する

作業するのに PowerShell を使用します。PowerShell のバージョンを確認すると 5.1 だったので 7.2.1 にバージョンアップします。

f:id:ksby:20211226201316p:plain

https://github.com/PowerShell/PowerShell/releases/tag/v7.2.1 から PowerShell-7.2.1-win-x64.msi をダウンロードしてインストールします。

ConEmu の {Shells::PowerShell}{Shells::PowerShell (Admin)} の設定を変更します。

f:id:ksby:20211226202256p:plain

  • powershell.exepwsh.exe

f:id:ksby:20211226202602p:plain

  • powershell.exe -new_console:apwsh.exe -new_console:a

f:id:ksby:20211226203107p:plain

D:\WSL\Ubuntu-20.04-2\ ディレクトリを新規作成した後、以下のコマンドを実行して Ubuntu-20.04-2 インスタンスを作成します。

PS> cd D:\WSL\Ubuntu-20.04-2\
PS> Invoke-WebRequest -Uri https://cloud-images.ubuntu.com/releases/hirsute/release/ubuntu-21.04-server-cloudimg-amd64-wsl.rootfs.tar.gz -OutFile $env:TMP\ubuntu-21.04.tar.gz
PS> wsl --import Ubuntu-20.04-2 D:\WSL\Ubuntu-20.04-2 $env:TMP\ubuntu-21.04.tar.gz

wsl -l -v コマンドを実行すると作成した Ubuntu-20.04-2 インスタンスは VERSION 1 になっていたので、wsl --set-version Ubuntu-20.04-2 2 コマンドを実行します。

f:id:ksby:20211227013957p:plain

WSL の基本的なコマンド を見ると wsl --set-default-version コマンドで既定の WSL バージョンを設定できるそうなので、wsl --set-default-version 2 コマンドを実行して WSL 2 がデフォルトになるように設定します。

wsl -d Ubuntu-20.04-2 コマンドを実行すれば作成した Ubuntu-20.04-2 インスタンスに接続できます。

f:id:ksby:20211227015945p:plain

wsl --unregister Ubuntu-20.04-2 コマンドを実行すると作成したインスタンスが削除されます。

f:id:ksby:20211227020305p:plain

履歴

2021/12/29
初版発行。

Grooy スクリプトをそのまま渡して実行する Spring Boot+Picocli ベースのコマンドラインアプリを作成する ( その8 )( @SpringBootApplication アノテーションを付与した Groovy スクリプトで REST API サーバを作成する )

概要

記事一覧はこちらです。

Grooy スクリプトをそのまま渡して実行する Spring Boot+Picocli ベースのコマンドラインアプリを作成する ( その7 )( @SpringBootApplication アノテーションを付与した Groovy スクリプトで SFTP クライアントを作成する ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。

今回は特別なことは何もしていないのに、妙に難しかったです。。。

参照したサイト・書籍

目次

  1. 今回実装する Groovy スクリプトの全体図
  2. build.gradle に記述しているモジュールを spring-boot-starter → spring-boot-starter-web に変更する
  3. Application.java の main メソッド内の処理を spring.main.web-application-type の値で切り替えるよう変更する
  4. application.properties に spring.main.web-application-type=none を追加する
  5. gse-servlet.bat を新規作成する
  6. JSON データを返す REST API サーバを起動する Groovy スクリプトを作成する
  7. 動作確認

手順

今回実装する Groovy スクリプトの全体図

f:id:ksby:20211219213612p:plain

  • StubServer.groovy という Groovy スクリプトを作成します。
  • groovy-script-executor と StubServer.groovy の2つで Tomcat が起動します。ポート番号は同じものが使用できませんので、groovy-script-executor は 8080番ポートを、StubServer.groovy は 9080番ポートを使用します。

build.gradle に記述しているモジュールを spring-boot-starter → spring-boot-starter-web に変更する

groovy-script-executor/build.gradle を以下のように変更します。

dependencies {
    ..........

    // dependency-management-plugin によりバージョン番号が自動で設定されるもの
    // Dependency Versions ( https://docs.spring.io/spring-boot/docs/current/reference/html/dependency-versions.html#dependency-versions ) 参照
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("org.springframework.integration:spring-integration-sftp")
    ..........
  • implementation("org.springframework.boot:spring-boot-starter")implementation("org.springframework.boot:spring-boot-starter-web") に変更します。

Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。

Application.java の main メソッド内の処理を spring.main.web-application-type の値で切り替えるよう変更する

groovy-script-executor/src/main/java/ksby/cmdapp/groovyscriptexecutor/Application.java を以下のように変更します。

@SpringBootApplication
public class Application implements CommandLineRunner, ExitCodeGenerator {

    ..........

    public static void main(String[] args) {
        String springMainWebApplicationType = System.getProperty("spring.main.web-application-type");
        ApplicationContext context = SpringApplication.run(Application.class, args);
        if (StringUtils.equals(springMainWebApplicationType, "none")) {
            System.exit(SpringApplication.exit(context));
        }
    }

    ..........

application.properties に spring.main.web-application-type=none を追加する

デフォルトでは groovy-script-executor で Tomcat が起動しないようにしたいので、groovy-script-executor/src/main/resources/application.properties に spring.main.web-application-type=none を追加します。

spring.main.banner-mode=off
spring.main.web-application-type=none

logging.level.root=ERROR
logging.level.com.jcraft.jsch=ERROR
logging.level.ksby.cmdapp.groovyscriptexecutor.script=INFO
logging.level.org.springframework.integration.expression.ExpressionUtils=ERROR

build タスクを実行し、生成した groovy-script-executor.jar を D:\tmp にコピーします。

gse-servlet.bat を新規作成する

groovy-script-executor/src/main/groovy/ksby/cmdapp/groovyscriptexecutor/script の下に gse.bat をコピーして gse-servlet.bat を新規作成し、以下の内容を記述します。

@echo off

java -Dfile.encoding=UTF-8 ^
     -XX:TieredStopAtLevel=1 ^
     -Dspring.main.lazy-initialization=true ^
     -Dspring.main.web-application-type=servlet ^
     -Dlogging.level.root=INFO ^
     -jar groovy-script-executor-1.0.0-RELEASE.jar ^
     %*
  • gse.bat との違いは以下の2行を追記している点です。
    • -Dspring.main.web-application-type=servlet ^
    • -Dlogging.level.root=INFO ^
  • -Dlogging.level.root=INFO ^Tomcat 起動時のログを出力しておいた方が動作が分かりやすいので追加しています。

gse-servlet.bat を D:\tmp にコピーします。

JSON データを返す REST API サーバを起動する Groovy スクリプトを作成する

groovy-script-executor/src/main/groovy/ksby/cmdapp/groovyscriptexecutor/script の下に StubServer.groovy を新規作成し、以下のコードを記述します。

package ksby.cmdapp.groovyscriptexecutor.script


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

@SpringBootApplication
// @RestController アノテーションはここに付ける
@RestController
@RequestMapping("/stub")
class StubServer {

    static void main(String[] args) {
        // application.properties で指定した設定は groovy-script-executor に反映されて
        // Groovy スクリプトには反映されないので、Groovy スクリプトに設定したい項目は
        // main メソッドで System.setProperty(...) を呼び出して設定する
        System.setProperty("server.port", "9080")

        // args に null が渡されるが、null のまま SpringApplication.run(...) を呼び出すと
        // エラーになるので、args = new String[0] をセットする
        if (args == null) {
            args = new String[0]
        }
        SpringApplication.run(StubServer.class, args)
    }

    // フィールドに private を付けないこと
    static class ResponseData {

        int key

        String data

    }

    @GetMapping
    ResponseData stub() {
        return new ResponseData(key: 123, data: "xxxxxxxx")
    }

}

StubServer.groovy を D:\tmp にコピーします。

動作確認

gse-servlet.bat StubServer.groovy コマンドを実行すると Tomcat が 8080番ポートと 9080番ポートで起動します。

f:id:ksby:20211222233253p:plain

curl -v http://localhost:8080/stub コマンドで 8080番ポートにアクセスしても HTTP ステータスコード = 404 が返ってきます。

f:id:ksby:20211222233431p:plain

curl -v http://localhost:9080/stub コマンドで 9080番ポートにアクセスすると HTTP ステータスコード = 200 と {"key":123,"data":"xxxxxxxx"}JSON データが返ってきます。

f:id:ksby:20211222233809p:plain

Ctrl+C を入力して groovy-script-executor を終了します。

docker-compose up -d コマンドを実行して SFTP サーバを起動した後、gse SftpClient.groovy --user=user01 --password=pass01 --upload-dir=upload --upload-file=publications.csv コマンドを実行すると SFTP クライアントの Groovy スクリプトも問題なく動作してファイルをアップロードしました。

f:id:ksby:20211222234426p:plain

履歴

2021/12/22
初版発行。

Grooy スクリプトをそのまま渡して実行する Spring Boot+Picocli ベースのコマンドラインアプリを作成する ( その7 )( @SpringBootApplication アノテーションを付与した Groovy スクリプトで SFTP クライアントを作成する )

概要

記事一覧はこちらです。

Grooy スクリプトをそのまま渡して実行する Spring Boot+Picocli ベースのコマンドラインアプリを作成する ( その6 )( Groovy スクリプトからログをコンソールやファイルに出力する ) の続きです。

今回から @SpringBootApplication アノテーションを付与した Groovy スクリプトを作成してみます。

  • 今回の手順で確認できるのは以下の内容です。
    • Spring Integration の SFTP Adapters を利用して簡単な SFTP クライアントを作成します。

参照したサイト・書籍

  1. atmoz/sftp
    https://hub.docker.com/r/atmoz/sftp

  2. SFTP Adapters
    https://docs.spring.io/spring-integration/docs/current/reference/html/sftp.html#sftp

  3. @Unmatched annotation
    https://picocli.info/#unmatched-annotation

  4. Concatenate Two Arrays in Java
    https://www.baeldung.com/java-concatenate-arrays

  5. spring-integration/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/session/SftpRemoteFileTemplateTests.java
    https://github.com/spring-projects/spring-integration/blob/main/spring-integration-sftp/src/test/java/org/springframework/integration/sftp/session/SftpRemoteFileTemplateTests.java

目次

  1. SFTP クライアントの仕様をまとめる
  2. docker-compose.yml に SFTP サーバを起動するための設定を追加する
  3. build.gradle に Spring Integration の SFTP Adapter を依存関係に追加する
  4. Groovy スクリプト側で Picocli の @Option アノテーションが使用できるよう groovy-script-executor.jar を変更する
  5. Groovy スクリプトを配置する package を sample → ksby.cmdapp.groovyscriptexecutor.script に変更する
  6. groovy-script-executor.jar 内に SFTP のアップロード・ダウンロード処理のための helper クラスを作成する
  7. Groovy スクリプトで SFTP クライアントを作成する
  8. groovy-script-executor.jar と Groovy スクリプトが出力するログを調整する
  9. 動作確認
  10. メモ書き
    1. groovy-script-executor.jar 内の Application クラス(@SpringBootApplication を付与したクラス)が ComponentScan の対象に含まれると Groovy スクリプトは実行されるが help が表示される

手順

SFTP クライアントの仕様をまとめる

  • ファイルのアップロード・ダウンロードを以下のコマンドで実行できるようにします。
    • gse SftpClient.groovy --host=<ホスト名> --port=<ポート番号> --user=<ユーザ名> --password=<パスワード> --upload-dir=<アップロード先ディレクトリ> --upload-file=<アップロードするファイル>
    • gse SftpClient.groovy --host=<ホスト名> --port=<ポート番号> --user=<ユーザ名> --password=<パスワード> --download-src=<ダウンロード元ファイル> --download-dst=<ダウンロード先ファイル>
  • これまで Groovy スクリプトは sample package の下に作成していましたが、groovy-script-executor.jar 内に作成する Spring の Component のメソッドを呼び出せるようにするために ksby.cmdapp.groovyscriptexecutor.script package の下に作成します(これまで作成した Groovy スクリプトもこの下に移動します)。
  • SFTP のアップロード・ダウンロード処理は groovy-script-executor.jar 内に Spring の Component として作成します。Groovy スクリプトはこの Component のメソッドを呼び出します。またこの Component は ComponentScan の記述を省略するため ksby.cmdapp.groovyscriptexecutor.script.helper package の下に作成します。

docker-compose.yml に SFTP サーバを起動するための設定を追加する

atmoz/sftp を利用して SFTP サーバを構築します。Spring Boot + Spring Integration でいろいろ試してみる ( その29 )( Docker Compose でサーバを構築する、FTP+SFTPサーバ編 ) も参考にしてください。

まず docker/sftp-server/config ディレクトリを作成し、この下に users.conf を新規作成して以下の内容を記述します。このファイルは改行コードを LF にします。

user01:pass01:::upload,download

.gitattributes を以下のように変更します。

#
# https://help.github.com/articles/dealing-with-line-endings/
#
# These are explicitly windows files and should use crlf
*.bat                                   text eol=crlf
docker/sftp-server/config/users.conf    text eol=lf
  • docker/sftp-server/config/users.conf text eol=lf を追加します。

docker-compose.yml の一番下に sftp-server の設定を追加します。

  #############################################################################
  # sftp-server
  #
  sftp-server:
    image: atmoz/sftp:alpine-3.7
    container_name: sftp-server
    ports:
      - "22:22"
    volumes:
      - ./docker/sftp-server/config/users.conf:/etc/sftp/users.conf:ro

docker-compose up -d コマンドを実行して atmoz/sftp の Docker Image を pull してサーバを起動します。

f:id:ksby:20211124210744p:plain

WinSCP で接続できることを確認します。

f:id:ksby:20211124211242p:plain f:id:ksby:20211124211342p:plain f:id:ksby:20211124211421p:plain

build.gradle に Spring Integration の SFTP Adapter を依存関係に追加する

groovy-script-executor/build.gradle を以下のように変更します。

dependencies {
    ..........

    // dependency-management-plugin によりバージョン番号が自動で設定されるもの
    // Dependency Versions ( https://docs.spring.io/spring-boot/docs/current/reference/html/dependency-versions.html#dependency-versions ) 参照
    implementation("org.springframework.boot:spring-boot-starter")
    implementation("org.springframework.integration:spring-integration-sftp")
    ..........
  • implementation("org.springframework.integration:spring-integration-sftp") を追加します。

Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。

Groovy スクリプト側で Picocli の @Option アノテーションが使用できるよう groovy-script-executor.jar を変更する

今の groovy-script-executor.jar の実装ではコマンドライン引数を @Parameters アノテーションで Groovy スクリプト側で受け取ることは出来るのですが、@Option アノテーションで受け取ることが出来ません。

Groovy スクリプト側で @Option アノテーションを付与したフィールドを定義しても、groovy-script-executor.jar の GroovyScriptExecutorCommand クラスで先に @Option アノテーションで定義されているかがチェックされて、定義されていないとエラーになります。

GroovyScriptExecutorCommand クラスに @Unmatched アノテーション を付与したフィールドを定義しておくと @Option アノテーションで定義されていなくてもエラーにならず @Unmatched アノテーションを付与したフィールドにセットされますので、この動作を利用します。

groovy-script-executor/src/main/java/ksby/cmdapp/groovyscriptexecutor/command/GroovyScriptExecutorCommand.java を以下のように変更します。

package ksby.cmdapp.groovyscriptexecutor.command;

import groovy.lang.Binding;
import groovy.lang.GroovyShell;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ArrayUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.info.BuildProperties;
import org.springframework.stereotype.Component;

import java.io.File;
import java.io.IOException;
import java.util.concurrent.Callable;

import static picocli.CommandLine.*;

@Slf4j
@Component
@Command(name = "groovy-script-executor", mixinStandardHelpOptions = true,
        versionProvider = GroovyScriptExecutorCommand.class,
        description = "Groovyスクリプトを実行するコマンド")
public class GroovyScriptExecutorCommand
        implements Callable<Integer>, IExitCodeExceptionMapper, IVersionProvider {

    @Autowired
    private BuildProperties buildProperties;

    @Parameters(index = "0", paramLabel = "Groovyスクリプト",
            description = "実行する Groovyスクリプトを指定する")
    private File groovyScript;

    @Parameters(index = "1..*", paramLabel = "引数",
            description = "Groovyスクリプトに渡す引数を指定する")
    private String[] args;

    @Unmatched
    private String[] unmatched;

    @Override
    public Integer call() throws IOException {
        try {
            Binding binding = new Binding();
            GroovyShell shell = new GroovyShell(binding);
            shell.run(groovyScript, ArrayUtils.addAll(args, unmatched));
        } catch (Exception e) {
            log.error("Groovyスクリプトでエラーが発生しました。", e);
        }

        return ExitCode.OK;
    }

    ..........
  • @Unmatched private String[] unmatched; を追加します。
  • call メソッド内の以下の点を変更します。
    • shell.run(groovyScript, args);shell.run(groovyScript, ArrayUtils.addAll(args, unmatched)); に変更します。

Groovy スクリプトを配置する package を sample → ksby.cmdapp.groovyscriptexecutor.script に変更する

groovy-script-executor/src/main/groovy の下に ksby.cmdapp.groovyscriptexecutor.script package を新規作成し、sample package の下の Groovy スクリプトをこの下に移動します。移動後、sample package を削除します。

ksby.cmdapp.groovyscriptexecutor.script package の下の Groovy スクリプトが groovy-script-executor.jar に含まれないようにするために groovy-script-executor/build.gradle を以下のように変更します。

[compileJava, compileTestGroovy, compileTestJava]*.options*.encoding = "UTF-8"
[compileJava, compileTestGroovy, compileTestJava]*.options*.compilerArgs = ["-Xlint:all,-options,-processing,-path"]
sourceSets {
    main {
        compileGroovy {
            exclude "ksby/cmdapp/groovyscriptexecutor/script/*.groovy"
        }
    }
}
..........
  • SourceSets 内で exclude "sample/*.groovy"exclude "ksby/cmdapp/groovyscriptexecutor/script/*.groovy" に変更します。

また groovy-script-executor/src/main/java/ksby/cmdapp/groovyscriptexecutor の下にも script package を新規作成します。

groovy-script-executor.jar 内に SFTP のアップロード・ダウンロード処理のための helper クラスを作成する

groovy-script-executor/src/main/java/ksby/cmdapp/groovyscriptexecutor/script の下に helper.sftp package を新規作成した後、この下に SftpHelper.java を新規作成し以下のコードを記述します。

package ksby.cmdapp.groovyscriptexecutor.script.helper.sftp;

import com.jcraft.jsch.ChannelSftp;
import com.jcraft.jsch.SftpException;
import org.springframework.expression.common.LiteralExpression;
import org.springframework.integration.file.remote.ClientCallbackWithoutResult;
import org.springframework.integration.file.support.FileExistsMode;
import org.springframework.integration.sftp.session.DefaultSftpSessionFactory;
import org.springframework.integration.sftp.session.SftpRemoteFileTemplate;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.messaging.Message;
import org.springframework.stereotype.Component;

import java.io.File;

@Component
public class SftpHelper {

    public SftpRemoteFileTemplate createSftpRemoteFileTemplate(
            String host,
            int port,
            String user,
            String password) {
        DefaultSftpSessionFactory factory = new DefaultSftpSessionFactory(false);
        factory.setHost(host);
        factory.setPort(port);
        factory.setUser(user);
        factory.setPassword(password);
        factory.setAllowUnknownKeys(true);

        return new SftpRemoteFileTemplate(factory);
    }

    public void upload(SftpRemoteFileTemplate sftpRemoteFileTemplate,
                       String uploadDir, File uploadFile) {
        sftpRemoteFileTemplate.setRemoteDirectoryExpression(new LiteralExpression(uploadDir));
        Message<File> message = MessageBuilder.withPayload(uploadFile).build();
        sftpRemoteFileTemplate.send(message, FileExistsMode.REPLACE);
    }

    public void download(SftpRemoteFileTemplate sftpRemoteFileTemplate,
                         String downlaodSrc, String downloadDst) {
        sftpRemoteFileTemplate.executeWithClient(
                (ClientCallbackWithoutResult<ChannelSftp>) client -> {
                    try {
                        client.get(downlaodSrc, downloadDst);
                    } catch (SftpException e) {
                        throw new RuntimeException(e);
                    }
                });
    }
}

Groovy スクリプトで SFTP クライアントを作成する

groovy-script-executor/src/main/groovy/ksby/cmdapp/groovyscriptexecutor/script の下に SftpClient.groovy を新規作成し、以下のコードを記述します。

package ksby.cmdapp.groovyscriptexecutor.script

import groovy.util.logging.Slf4j
import ksby.cmdapp.groovyscriptexecutor.script.helper.sftp.SftpHelper
import org.springframework.boot.CommandLineRunner
import org.springframework.boot.ExitCodeGenerator
import org.springframework.boot.SpringApplication
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.integration.sftp.session.SftpRemoteFileTemplate
import org.springframework.stereotype.Component
import picocli.CommandLine
import picocli.CommandLine.ArgGroup
import picocli.CommandLine.Command
import picocli.CommandLine.ExitCode
import picocli.CommandLine.IExitCodeExceptionMapper
import picocli.CommandLine.IFactory
import picocli.CommandLine.Option

import java.util.concurrent.Callable

@Slf4j
@SpringBootApplication
class SftpClient implements CommandLineRunner, ExitCodeGenerator {

    private int exitCode

    private final SftpClientCommand sftpClientCommand

    private final IFactory factory

    SftpClient(SftpClientCommand sftpClientCommand, IFactory factory) {
        this.sftpClientCommand = sftpClientCommand
        this.factory = factory
    }

    static void main(String[] args) {
        System.exit(SpringApplication.exit(SpringApplication.run(SftpClient.class, args)));
    }

    @Override
    void run(String... args) throws Exception {
        exitCode = new CommandLine(sftpClientCommand, factory)
                .setExitCodeExceptionMapper(sftpClientCommand)
                .execute(args)
    }

    @Override
    int getExitCode() {
        return exitCode
    }

    @Component
    @Command(name = "SftpClient",
            mixinStandardHelpOptions = true,
            description = "SFTPサーバにファイルをアップロード・ダウンロードするコマンド")
    static class SftpClientCommand
            implements Callable<Integer>, IExitCodeExceptionMapper {

        @Option(names = "--host", required = false, description = "ホスト名")
        String host = "localhost"

        @Option(names = "--port", required = false, description = "ポート番号")
        int port = 22

        @Option(names = ["-u", "--user"], description = "ユーザ名")
        String user

        @Option(names = ["-p", "--password"], description = "パスワード")
        String password

        @ArgGroup(exclusive = true, multiplicity = "1")
        SftpClientOperation sftpClientOperation

        static class SftpClientOperation {

            @ArgGroup(exclusive = false, multiplicity = "1")
            UploadOption uploadOption

            @ArgGroup(exclusive = false, multiplicity = "1")
            DownloadOption downloadOption

        }

        static class UploadOption {

            @Option(names = "--upload-dir", required = true, description = "アップロード先ディレクトリ")
            String uploadDir

            @Option(names = "--upload-file", required = true, description = "アップロードするファイル")
            File uploadFile

        }

        static class DownloadOption {

            @Option(names = "--download-src", required = true, description = "ダウンロード元ファイル")
            String downlaodSrc

            @Option(names = "--download-dst", required = true, description = "ダウンロード先ファイル")
            String downloadDst

        }

        private final SftpHelper sftpHelper

        SftpClientCommand(SftpHelper sftpHelper) {
            this.sftpHelper = sftpHelper
        }

        @Override
        Integer call() throws Exception {
            SftpRemoteFileTemplate sftpRemoteFileTemplate =
                    sftpHelper.createSftpRemoteFileTemplate(host, port, user, password)

            if (sftpClientOperation.uploadOption != null) {
                log.info("{} へ {} をアップロードします",
                        sftpClientOperation.uploadOption.uploadDir,
                        sftpClientOperation.uploadOption.uploadFile)
                sftpHelper.upload(sftpRemoteFileTemplate,
                        sftpClientOperation.uploadOption.uploadDir,
                        sftpClientOperation.uploadOption.uploadFile)
            } else {
                log.info("{} を {} へダウンロードします",
                        sftpClientOperation.downloadOption.downlaodSrc,
                        sftpClientOperation.downloadOption.downloadDst)
                sftpHelper.download(sftpRemoteFileTemplate,
                        sftpClientOperation.downloadOption.downlaodSrc,
                        sftpClientOperation.downloadOption.downloadDst)
            }

            return ExitCode.OK
        }

        @Override
        int getExitCode(Throwable exception) {
            if (exception instanceof RuntimeException) {
                return 101
            }

            return ExitCode.OK
        }
    }

}

groovy-script-executor.jar と Groovy スクリプトが出力するログを調整する

groovy-script-executor/src/main/resources/application.properties を以下のように変更します。

spring.main.banner-mode=off

logging.level.root=ERROR
logging.level.com.jcraft.jsch=ERROR
logging.level.ksby.cmdapp.groovyscriptexecutor.script=INFO
logging.level.org.springframework.integration.expression.ExpressionUtils=ERROR
  • logging.level.root=OFFlogging.level.root=ERROR に変更します。
  • 以下の行を追加します。
    • logging.level.com.jcraft.jsch=ERROR
      • SFTP のライブラリのログです。SFTP の処理の詳細を知りたい時にはこのログのレベルを変更します。
    • logging.level.ksby.cmdapp.groovyscriptexecutor.script=INFO
    • logging.level.org.springframework.integration.expression.ExpressionUtils=ERROR
      • あまり意味のない WARN ログが出力されるのでログレベルを ERROR に変更して出力されないようにします。

ログレベルは groovy-script-executor.jar で設定したので D:\tmp\application.properties を削除します。

コマンドラインで Groovy スクリプトを実行する際には JSON フォーマットのログは不便なので CONSOLE に戻します。groovy-script-executor/src/main/resources/logback-spring.xml を以下のように変更します。

    ..........

    <if condition='isDefined("LOG_FILE")'>
        <then>
            <root>
                <appender-ref ref="${LOGGING_APPENDER}"/>
            </root>
        </then>
        <else>
            <root>
                <appender-ref ref="CONSOLE"/>
                <!--<appender-ref ref="JSON"/>-->
            </root>
        </else>
    </if>
</configuration>

build タスクを実行し、生成した groovy-script-executor.jar を D:\tmp にコピーします。

動作確認

SFTP サーバの upload ディレクトリに何もファイルがないことを確認してから、

f:id:ksby:20211127162239p:plain

gse SftpClient.groovy --user=user01 --password=pass01 --upload-dir=upload --upload-file=publications.csv コマンドを実行するとログが出力されて、

f:id:ksby:20211127162513p:plain

upload ディレクトリに publications.csv がアップロードされます。

f:id:ksby:20211127162629p:plain

次に gse SftpClient.groovy --user=user01 --password=pass01 --download-src=upload/publications.csv --download-dst=D:\tmp\sample.csv コマンドを実行すると、

f:id:ksby:20211127162926p:plain

upload/publications.csv が D:\tmp の下に sample.csv としてダウンロードされます。

f:id:ksby:20211127163013p:plain

メモ書き

groovy-script-executor.jar 内の Application クラス(@SpringBootApplication を付与したクラス)が ComponentScan の対象に含まれると Groovy スクリプトは実行されるが help が表示される

SftpClient.groovy の @SpringBootApplication アノテーションscanBasePackages = "ksby.cmdapp.groovyscriptexecutor" を指定して ComponentScan の対象に groovy-script-executor.jar 内の Application クラスが含まれるようにしてから、

@Slf4j
@SpringBootApplication(scanBasePackages = "ksby.cmdapp.groovyscriptexecutor")
class SftpClient implements CommandLineRunner, ExitCodeGenerator {

gse SftpClient.groovy --user=user01 --password=pass01 --upload-dir=upload --upload-file=publications.csv コマンドを実行すると Missing required parameter: 'Groovyスクリプト のメッセージと help が表示されます。

f:id:ksby:20211127165407p:plain

ただし Groovy スクリプト自体は実行されており、ファイルはアップロードされていました。

メッセージと help が表示されるのは紛らわしいので、groovy-script-executor.jar 内の Application クラス(@SpringBootApplication を付与したクラス)が ComponentScan の対象に含まれないようにした方がよいでしょう。

履歴

2021/11/27
初版発行。

Grooy スクリプトをそのまま渡して実行する Spring Boot+Picocli ベースのコマンドラインアプリを作成する ( その6 )( Groovy スクリプトからログをコンソールやファイルに出力する )

概要

記事一覧はこちらです。

Grooy スクリプトをそのまま渡して実行する Spring Boot+Picocli ベースのコマンドラインアプリを作成する ( その5 )( CSV ファイルのデータをテーブルに登録する Groovy スクリプトを作成する ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • Groovy スクリプトからログをコンソール、あるいはファイルに出力できるようにします。

参照したサイト・書籍

  1. Runtime and compile-time metaprogramming
    https://groovy-lang.org/metaprogramming.html

  2. Unicode Support
    https://conemu.github.io/en/UnicodeSupport.html

  3. Chapter 3: Logback configuration - Conditional processing of configuration files
    http://logback.qos.ch/manual/configuration.html#conditional

  4. バッチファイルでファイルパスからファイル名や拡張子を自由に取り出す方法
    https://orangeclover.hatenablog.com/entry/20101004/1286120668

  5. Get Log Output in JSON
    https://www.baeldung.com/java-log-json-output

  6. Structured logging with SLF4J and Logback
    https://gquintana.github.io/2017/12/01/Structured-logging-with-SL-FJ-and-Logback.html

  7. logfellow / logstash-logback-encoder
    https://github.com/logfellow/logstash-logback-encoder

  8. Java ログ収集
    https://docs.datadoghq.com/ja/logs/log_collection/java/?tab=logback

目次

  1. Groovy スクリプトからログを出力する
    1. CsvFileToBookTable.groovy にログ出力処理を追加する
    2. コンソールにログを出力する
    3. ファイルにログを出力する
    4. ログを JSON フォーマットで出力する

手順

Groovy スクリプトからログを出力する

CsvFileToBookTable.groovy にログ出力処理を追加する

groovy-script-executor/src/main/groovy/sample/CsvFileToBookTable.groovy にログ出力処理を追加します。

package sample

import com.univocity.parsers.annotations.Parsed
import com.univocity.parsers.common.processor.BeanListProcessor
import com.univocity.parsers.csv.CsvParserSettings
import com.univocity.parsers.csv.CsvRoutines
import groovy.sql.Sql
import groovy.util.logging.Slf4j

@Slf4j
class CsvFileToBookTable {

    static class CsvRecord {
        @Parsed(index = 0, field = "isbm")
        String isbm
        @Parsed(index = 1, field = "title_author")
        String title_author
    }

    static void main(args) {
        def sql = Sql.newInstance("jdbc:mysql://localhost:3306/testdb?sslMode=DISABLED&characterEncoding=utf8",
                "testdb_user",
                "xxxxxxxx",
                "org.mariadb.jdbc.Driver")
        sql.connection.autoCommit = false

        CsvParserSettings settings = new CsvParserSettings()
        settings.format.lineSeparator = "\r\n"
        settings.headerExtractionEnabled = true
        BeanListProcessor<CsvRecord> rowProcessor = new BeanListProcessor<>(CsvRecord)
        settings.processor = rowProcessor

        sql.execute("truncate table book")
        log.info("bookテーブルをtruncateしました。")

        new File("publications.csv").withReader { reader ->
            CsvRoutines csvRoutines = new CsvRoutines(settings)
            for (CsvRecord csvRecord : csvRoutines.iterate(CsvRecord, reader)) {
                String[] titleAndAuthor = csvRecord.title_author.split(" / ")
                def title = titleAndAuthor[0]
                def author = null
                if (titleAndAuthor.size() == 1) {
                    log.warn("title_authorカラムにはauthorが記載されていません。")
                } else {
                    author = titleAndAuthor[1]
                }

                sql.execute("""
                                insert into book (isbm, title, author)
                                values (:isbm, :title, :author)
                            """,
                        isbm: csvRecord.isbm,
                        title: title,
                        author: author)
                log.info("bookテーブルに登録しました (isbm = {}, title = {}, author = {})",
                        csvRecord.isbm, title, author)
            }
        }

        sql.commit()
        sql.close()
    }

}

コンソールにログを出力する

D:\tmp の下に application.properties を新規作成し、以下の内容を記述します。Groovy スクリプトと同じディレクトリにある application.properties の設定で groovy-script-executor.jar の groovy-script-executor/src/main/resources/application.properties の設定が上書きされます。

logging.level.root=INFO

#sample パッケージのログだけ出力したい場合には、root → sample に変更する
#logging.level.sample=INFO

gse CsvFileToBookTable.groovy で Groovy スクリプトを実行するとログが出力されますが、gse.bat 内で -Dfile.encoding=UTF-8 を指定しているのでログの文字コードUTF-8 になります。そのままでは Windowsコマンドプロンプトでは文字化けします。

f:id:ksby:20211112235312p:plain

chcp 65001 & cmd コマンドを実行してから gse CsvFileToBookTable.groovy を実行すれば文字化けしなくなります(Unicode Support 参照)。

f:id:ksby:20211112235826p:plain

ファイルにログを出力する

logback-spring.xml の記述で janino を使いたいので build.gradle に追加します。

dependencies {
    ..........

    // dependency-management-plugin によりバージョン番号が自動で設定されるもの
    // Dependency Versions ( https://docs.spring.io/spring-boot/docs/current/reference/html/dependency-versions.html#dependency-versions ) 参照
    implementation("org.springframework.boot:spring-boot-starter")
    implementation("org.apache.commons:commons-lang3")
    implementation("org.codehaus.janino:janino")
    testImplementation("org.springframework.boot:spring-boot-starter-test")

    ..........
  • dependencies block に implementation("org.codehaus.janino:janino") を追加します。

groovy-script-executor/src/main/resources の下に logback-spring.xml を新規作成し、以下の内容を記述します。logging.file.name が設定されていればログファイルへ、設定されていなければコンソールへログを出力します。

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <include resource="org/springframework/boot/logging/logback/defaults.xml"/>
    <include resource="org/springframework/boot/logging/logback/console-appender.xml"/>
    <property name="LOGGING_APPENDER" value="${logging.appender:-FILE}"/>

    <if condition='isDefined("LOG_FILE")'>
        <then>
            <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
                <encoder>
                    <pattern>${FILE_LOG_PATTERN}</pattern>
                </encoder>
                <file>${LOG_FILE}</file>
                <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
                    <fileNamePattern>${LOG_FILE}.%d{yyyy-MM-dd}</fileNamePattern>
                    <maxHistory>30</maxHistory>
                </rollingPolicy>
            </appender>
        </then>
    </if>

    <if condition='isDefined("LOG_FILE")'>
        <then>
            <root>
                <appender-ref ref="${LOGGING_APPENDER}"/>
            </root>
        </then>
        <else>
            <root>
                <appender-ref ref="CONSOLE"/>
            </root>
        </else>
    </if>
</configuration>

build タスクを実行し、生成した groovy-script-executor.jar を D:\tmp にコピーします。

Groovy スクリプトと同じディレクトリにログを出力するよう gse.bat を以下のように変更します。

@echo off

java -Dfile.encoding=UTF-8 ^
     -XX:TieredStopAtLevel=1 ^
     -Dspring.main.lazy-initialization=true ^
     -Dlogging.file.name=%~n1.log ^
     -jar groovy-script-executor-1.0.0-RELEASE.jar ^
     %*
  • -Dlogging.file.name=%~n1.log ^ を追加します。

gse CsvFileToBookTable.groovy で Groovy スクリプトを実行するとコンソールには何も出力されず、

f:id:ksby:20211115062824p:plain

CsvFileToBookTable.groovy と同じディレクトリに CsvFileToBookTable.log が作成されて、その中にログが出力されます。

f:id:ksby:20211115062958p:plain f:id:ksby:20211115063111p:plain

gse.bat 内の -Dlogging.file.name=%~n1.log ^ を削除すれば、コンソールにログが出力されます。

f:id:ksby:20211115063443p:plain

ログを JSON フォーマットで出力する

logstash-logback-encoder を導入してログファイルを JSON フォーマットで出力してみます。

build.gradle を以下のように変更します。

dependencies {
    ..........

    // dependency-management-plugin によりバージョン番号が自動で設定されないもの、あるいは最新バージョンを指定したいもの
    runtimeOnly("${postgresqlJdbcDriver}")
    runtimeOnly("${mariadbJdbcDriver}")
    implementation("com.univocity:univocity-parsers:2.9.1")
    implementation("net.logstash.logback:logstash-logback-encoder:6.6")
    testImplementation("org.assertj:assertj-core:3.21.0")
  • dependencies block に implementation("net.logstash.logback:logstash-logback-encoder:6.6") を追加します。

logback-spring.xml を以下のように変更します。

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <include resource="org/springframework/boot/logging/logback/defaults.xml"/>
    <include resource="org/springframework/boot/logging/logback/console-appender.xml"/>
    <property name="LOGGING_APPENDER" value="${logging.appender:-FILE}"/>

    <appender name="JSON" class="ch.qos.logback.core.ConsoleAppender">
        <encoder class="net.logstash.logback.encoder.LogstashEncoder">
            <jsonGeneratorDecorator class="net.logstash.logback.decorate.PrettyPrintingJsonGeneratorDecorator"/>
            <throwableConverter class="net.logstash.logback.stacktrace.ShortenedThrowableConverter">
                <maxDepthPerThrowable>30</maxDepthPerThrowable>
                <maxLength>2048</maxLength>
                <shortenedClassNameLength>20</shortenedClassNameLength>
                <rootCauseFirst>true</rootCauseFirst>
                <inlineHash>true</inlineHash>
            </throwableConverter>
        </encoder>
    </appender>

    <if condition='isDefined("LOG_FILE")'>
        <then>
            <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
                <encoder>
                    <pattern>${FILE_LOG_PATTERN}</pattern>
                </encoder>
                <file>${LOG_FILE}</file>
                <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
                    <fileNamePattern>${LOG_FILE}.%d{yyyy-MM-dd}</fileNamePattern>
                    <maxHistory>30</maxHistory>
                </rollingPolicy>
            </appender>
        </then>
    </if>

    <if condition='isDefined("LOG_FILE")'>
        <then>
            <root>
                <appender-ref ref="${LOGGING_APPENDER}"/>
            </root>
        </then>
        <else>
            <root>
                <!--<appender-ref ref="CONSOLE"/>-->
                <appender-ref ref="JSON"/>
            </root>
        </else>
    </if>
</configuration>
  • <appender name="JSON" class="ch.qos.logback.core.ConsoleAppender">...</appender> を追加します。
  • <appender-ref ref="CONSOLE"/>コメントアウトして <appender-ref ref="JSON"/> を追加します。

今のままでは stack trace が JSON フォーマットで出力されないので、groovy-script-executor/src/main/java/ksby/cmdapp/groovyscriptexecutor/command/GroovyScriptExecutorCommand.java を以下のように変更します。

public class GroovyScriptExecutorCommand
        implements Callable<Integer>, IExitCodeExceptionMapper, IVersionProvider {

    ..........

    @Override
    public Integer call() throws IOException {
        try {
            Binding binding = new Binding();
            GroovyShell shell = new GroovyShell(binding);
            shell.run(groovyScript, args);
        } catch (Exception e) {
            log.error("Groovyスクリプトでエラーが発生しました。", e);
        }

        return ExitCode.OK;
    }

    ..........

}
  • call メソッド内の処理を try { ... } catch (Exception e) { log.error("Groovyスクリプトでエラーが発生しました。", e); } で囲みます。

build タスクを実行し、生成した groovy-script-executor.jar を D:\tmp にコピーします。

gse.bat から -Dlogging.file.name=%~n1.log を削除してログがコンソールに出力されるようにしてから gse CsvFileToBookTable.groovy を実行すると、コンソールにログが JSON フォーマットで出力されます。

f:id:ksby:20211116230032p:plain

また CsvFileToBookTable.groovy 内でエラーが発生するよう変更してから実行すると stack trace も JSON フォーマットで出力されます。

f:id:ksby:20211116230314p:plain

履歴

2021/11/16
初版発行。