Grooy スクリプトをそのまま渡して実行する Spring Boot+Picocli ベースのコマンドラインアプリを作成する ( その7 )( @SpringBootApplication アノテーションを付与した Groovy スクリプトで SFTP クライアントを作成する )
概要
記事一覧はこちらです。
今回から @SpringBootApplication アノテーションを付与した Groovy スクリプトを作成してみます。
- 今回の手順で確認できるのは以下の内容です。
- Spring Integration の SFTP Adapters を利用して簡単な SFTP クライアントを作成します。
参照したサイト・書籍
atmoz/sftp
https://hub.docker.com/r/atmoz/sftpSFTP Adapters
https://docs.spring.io/spring-integration/docs/current/reference/html/sftp.html#sftp@Unmatched annotation
https://picocli.info/#unmatched-annotationConcatenate Two Arrays in Java
https://www.baeldung.com/java-concatenate-arraysspring-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
目次
- SFTP クライアントの仕様をまとめる
- docker-compose.yml に SFTP サーバを起動するための設定を追加する
- build.gradle に Spring Integration の SFTP Adapter を依存関係に追加する
- Groovy スクリプト側で Picocli の @Option アノテーションが使用できるよう groovy-script-executor.jar を変更する
- Groovy スクリプトを配置する package を sample → ksby.cmdapp.groovyscriptexecutor.script に変更する
- groovy-script-executor.jar 内に SFTP のアップロード・ダウンロード処理のための helper クラスを作成する
- Groovy スクリプトで SFTP クライアントを作成する
- groovy-script-executor.jar と Groovy スクリプトが出力するログを調整する
- 動作確認
- メモ書き
手順
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 してサーバを起動します。
WinSCP で接続できることを確認します。
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=OFF
→logging.level.root=ERROR
に変更します。- 以下の行を追加します。
logging.level.com.jcraft.jsch=ERROR
- SFTP のライブラリのログです。SFTP の処理の詳細を知りたい時にはこのログのレベルを変更します。
logging.level.ksby.cmdapp.groovyscriptexecutor.script=INFO
- Groovy スクリプトのログレベルを 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 ディレクトリに何もファイルがないことを確認してから、
gse SftpClient.groovy --user=user01 --password=pass01 --upload-dir=upload --upload-file=publications.csv
コマンドを実行するとログが出力されて、
upload ディレクトリに publications.csv がアップロードされます。
次に gse SftpClient.groovy --user=user01 --password=pass01 --download-src=upload/publications.csv --download-dst=D:\tmp\sample.csv
コマンドを実行すると、
upload/publications.csv が D:\tmp の下に sample.csv としてダウンロードされます。
メモ書き
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 が表示されます。
ただし Groovy スクリプト自体は実行されており、ファイルはアップロードされていました。
メッセージと help が表示されるのは紛らわしいので、groovy-script-executor.jar 内の Application クラス(@SpringBootApplication を付与したクラス)が ComponentScan の対象に含まれないようにした方がよいでしょう。
履歴
2021/11/27
初版発行。