かんがるーさんの日記

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

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
初版発行。

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

概要

記事一覧はこちらです。

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

  • 今回の手順で確認できるのは以下の内容です。
    • CSV ファイルのデータを MySQL の testdb の book テーブルに登録する Groovy スクリプトを作成します。

参照したサイト・書籍

目次

  1. CSV ファイルのデータを MySQL の testdb の book テーブルに登録する

手順

CSV ファイルのデータを MySQL の testdb の book テーブルに登録する

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

  • 今回は commit を明記する方式にします。トランザクションの処理が始まる前に sql.connection.autoCommit = false を記述して AutoCommit を OFF にします。
  • CSV ファイルの publications.csv は PublicationsTableToFileUsingUnivocityParsers.groovy が出力したファイルを使用します。ただしそのままだと全部で 154,729件あって多いので、先頭から 20件だけ使用します。
  • publications.csv の title_author には title だけか、/ を区切り文字として title と authror が入っているので、/ で分割して author はデータがある時だけセットします。
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

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")

        new File("publications.csv").withReader { reader ->
            CsvRoutines csvRoutines = new CsvRoutines(settings)
            for (CsvRecord csvRecord : csvRoutines.iterate(CsvRecord, reader)) {
                String[] titleAndAuthor = csvRecord.title_author.split(" / ")
                sql.execute("""
                                insert into book (isbm, title, author)
                                values (:isbm, :title, :author)
                            """,
                        isbm: csvRecord.isbm,
                        title: titleAndAuthor[0],
                        author: titleAndAuthor.size() == 2 ? titleAndAuthor[1] : null)
            }
        }

        sql.commit()
        sql.close()
    }

}

CsvFileToBookTable.groovy を D:\tmp にコピーし gse CsvFileToBookTable.groovy を実行すると book テーブルにデータが登録されます。

f:id:ksby:20211107172250p:plain

履歴

2021/11/07
初版発行。

Grooy スクリプトをそのまま渡して実行する Spring Boot+Picocli ベースのコマンドラインアプリを作成する ( その4 )( テーブルのデータを CSV フォーマットで出力する Groovy スクリプトを作成する )

概要

記事一覧はこちらです。

Grooy スクリプトをそのまま渡して実行する Spring Boot+Picocli ベースのコマンドラインアプリを作成する ( その3 )( サンプルの Groovy スクリプトをプロジェクト内に置く+PostgreSQL、MySQL を起動するための docker-compose.yml を作成してデータベース・テーブルを作成する ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • PostgreSQL の sampledb の publications テーブルのデータを CSV フォーマットで出力する Groovy スクリプトを作成します。

参照したサイト・書籍

  1. Connecting to an Amazon Aurora DB cluster
    https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/Aurora.Connecting.html

  2. Dependency management with Grape
    http://docs.groovy-lang.org/latest/html/documentation/grape.html

  3. Groovy - File I/O
    https://www.tutorialspoint.com/groovy/groovy_file_io.htm

目次

  1. groovy-script-executor.jar に PostgreSQL、MariaDB の JDBC Driver を入れる
  2. PostgreSQL の sampledb の publications テーブルのデータを CSV フォーマットで出力する
    1. 標準出力にカンマ区切りで出力する
    2. ファイルにカンマ区切りで出力する
    3. univocity-parsers を使用してファイルに出力する
  3. groovy-script-executor.jar 内に作成したクラスを Groovy スクリプトから呼び出せるのか?

手順

groovy-script-executor.jar に PostgreSQLMariaDBJDBC Driver を入れる

JDBC Driver は @GrabDependency management with Grape 参照)で指定するのではなく groovy-script-executor.jar に入れます。

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

dependencies {
    def postgresqlJdbcDriver = "org.postgresql:postgresql:42.3.1"
    def mariadbJdbcDriver = "org.mariadb.jdbc:mariadb-java-client:2.7.4"
    def groovyVersion = "3.0.9"
    ..........

    // dependency-management-plugin によりバージョン番号が自動で設定されないもの、あるいは最新バージョンを指定したいもの
    runtimeOnly("${postgresqlJdbcDriver}")
    runtimeOnly("${mariadbJdbcDriver}")
    implementation("com.univocity:univocity-parsers:2.9.1")
    ..........
  • dependencies block に以下の行を追加します。
    • def postgresqlJdbcDriver = "org.postgresql:postgresql:42.3.1"
    • def mariadbJdbcDriver = "org.mariadb.jdbc:mariadb-java-client:2.7.4"
    • runtimeOnly("${postgresqlJdbcDriver}")
    • runtimeOnly("${mariadbJdbcDriver}")

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

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

PostgreSQL の sampledb の publications テーブルのデータを CSV フォーマットで出力する

標準出力にカンマ区切りで出力する

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

  • Windows のコンソールに出力するので、出力時の文字コードwindows-31j を指定しています。
  • isbm は NULL のデータが存在し、そのままでは NULL と出力されるため、coalesce 関数で NULL の場合には空文字列を取得するようにしています。
  • select 文を実行するのに sql.query(...) { resultSet -> }sql.eachRow(...) { row -> } のメソッドも存在しますが、row.values().join(",") が使用したいので sql.rows(...) メソッドを使用しています。
package sample

import groovy.sql.Sql

class PublicationsTableToSystemOutInCSV {

    static void main(args) {
        def sql = Sql.newInstance("jdbc:postgresql://localhost:5432/sampledb",
                "sampledb_user",
                "xxxxxxxx",
                "org.postgresql.Driver")

        try (Writer writer = System.out.newWriter("windows-31j")) {
            def rows = sql.rows("""\
                select coalesce(isbm, ''),
                       title_author
                from publications
            """)
            rows.each { row ->
                writer.println(row.values().join(","))
            }
        }

        sql.close()
    }

}

PublicationsTableToSystemOutInCSV.groovy を D:\tmp にコピーし gse PublicationsTableToSystemOutInCSV.groovy を実行すると取得したデータが出力されます。

f:id:ksby:20211103100036p:plain

ファイルにカンマ区切りで出力する

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

package sample

import groovy.sql.Sql

class PublicationsTableToFileInCSV {

    static void main(String[] args) {
        def sql = Sql.newInstance("jdbc:postgresql://localhost:5432/sampledb",
                "sampledb_user",
                "xxxxxxxx",
                "org.postgresql.Driver")

        new File("publications.csv").withWriter { writer ->
            def rows = sql.rows("""\
                select coalesce(isbm, ''),
                       title_author
                from publications
            """)
            rows.each { row ->
                writer.println(row.values().join(","))
            }
        }

        sql.close()
    }

}

PublicationsTableToFileInCSV.groovy を D:\tmp にコピーし gse PublicationsTableToFileInCSV.groovy を実行すると取得したデータがファイルに出力されます。

f:id:ksby:20211103113754p:plain

univocity-parsers を使用してファイルに出力する

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

package sample

import com.univocity.parsers.annotations.Parsed
import com.univocity.parsers.common.processor.BeanWriterProcessor
import com.univocity.parsers.csv.CsvWriter
import com.univocity.parsers.csv.CsvWriterSettings
import groovy.sql.Sql

class PublicationsTableToFileUsingUnivocityParsers {

    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:postgresql://localhost:5432/sampledb",
                "sampledb_user",
                "xxxxxxxx",
                "org.postgresql.Driver")

        CsvWriterSettings settings = new CsvWriterSettings()
        settings.setQuoteAllFields(true)
        BeanWriterProcessor<CsvRecord> writerProcessor = new BeanWriterProcessor<>(CsvRecord)
        settings.setRowWriterProcessor(writerProcessor)
        CsvWriter writer = new CsvWriter(new File("publications.csv"), settings)

        writer.writeHeaders()

        CsvRecord publications = new CsvRecord()
        sql.eachRow("select * from publications") { row ->
            publications.isbm = row.isbm
            publications.title_author = row.title_author
            writer.processRecord(publications)
        }

        writer.close()
        sql.close()
    }

}

PublicationsTableToFileInCSV.groovy を D:\tmp にコピーし gse PublicationsTableToFileUsingUnivocityParsers.groovy を実行すると取得したデータがファイルに出力されます。

f:id:ksby:20211103115600p:plain

groovy-script-executor.jar 内に入れている univocity-parsers のモジュールを問題なく呼び出すことが出来ています。

groovy-script-executor.jar 内に作成したクラスを Groovy スクリプトから呼び出せるのか?

groovy-script-executor.jar 内に共通ライブラリとして利用するクラスを作成して Groovy スクリプトから呼び出せるのか試してみます。

groovy-script-executor/src/main/groovy の下に ksby.util パッケージを作成し、その下に DbUtils.groovy を作成して以下のコードを記述します。

package ksby.util

import groovy.sql.Sql

class DbUtils {

    static Sql connectSampleDb() {
        return Sql.newInstance("jdbc:postgresql://localhost:5432/sampledb",
                "sampledb_user",
                "xxxxxxxx",
                "org.postgresql.Driver")
    }

}

groovy-script-executor/src/main/groovy/sample/PublicationsTableToSystemOutInCSV.groovy の DB への接続処理を Sql.newInstance(...)DbUtils.connectSampleDb() に変更します。

package sample

import ksby.util.DbUtils

class PublicationsTableToSystemOutInCSV {

    static void main(args) {
        def sql = DbUtils.connectSampleDb()

        try (Writer writer = System.out.newWriter("windows-31j")) {
            def rows = sql.rows("""\
                select coalesce(isbm, ''),
                       title_author
                from publications
            """)
            rows.each { row ->
                writer.println(row.values().join(","))
            }
        }

        sql.close()
    }

}

build タスクを実行して生成された groovy-script-executor.jar と PublicationsTableToSystemOutInCSV.groovy を D:\tmp へコピーした後、gse PublicationsTableToSystemOutInCSV.groovy を実行すると取得したデータが出力されました。

f:id:ksby:20211103213649p:plain

groovy-script-executor.jar 内に入っているクラスを Groovy スクリプトから問題なく呼び出すことが出来ました。

履歴

2021/11/03
初版発行。

Grooy スクリプトをそのまま渡して実行する Spring Boot+Picocli ベースのコマンドラインアプリを作成する ( その3 )( サンプルの Groovy スクリプトをプロジェクト内に置く+PostgreSQL、MySQL を起動するための docker-compose.yml を作成してデータベース・テーブルを作成する )

概要

記事一覧はこちらです。

Grooy スクリプトをそのまま渡して実行する Spring Boot+Picocli ベースのコマンドラインアプリを作成する ( その2 )( groovy-script-executor.jar を作成する ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • サンプルの Groovy スクリプトを置く場所を決めます。
    • サンプルの Groovy スクリプト で利用する PostgreSQLMySQL を起動するための docker-compose.yml を作成します。
    • PostgreSQL に sampledb、MySQL に testdb を作成します。
    • Flyway でテーブルを作成してデータを入れます。

参照したサイト・書籍

  1. Exclude file from jar using Gradle
    https://stackoverflow.com/questions/53394850/exclude-file-from-jar-using-gradle

  2. new collation (ja_JP.UTF-8) is incompatible with the collation of the template database (C)
    https://symfoware.blog.fc2.com/blog-entry-1737.html

  3. ロケール(国際化と地域化)
    https://lets.postgresql.jp/documents/technical/text-processing/2

  4. 国立国会図書館 - オープンデータセット
    https://www.ndl.go.jp/jp/dlib/standards/opendataset/index.html

目次

  1. groovy-script-executor/src/main/groovy の下に sample パッケージを作成し、その下にサンプルの Groovy スクリプトを置く
  2. サンプルの Groovy スクリプト で利用する PostgreSQL、MySQL を起動するための docker-compose.yml を作成する+PostgreSQL に sampledb、MySQL に testdb を作成する
  3. PostgreSQL の sampledb にテーブルを作成する
  4. MySQL の testdb にテーブルを作成する

手順

groovy-script-executor/src/main/groovy の下に sample パッケージを作成し、その下にサンプルの Groovy スクリプトを置く

groovy-script-executor/src/main/groovy の下にサンプルの Groovy スクリプトを置くと jar ファイルを作成した時にサンプルの Groovy スクリプトの class ファイルが入ってしまうと思っていたのですが、Exclude file from jar using Gradle のページを見つけて build.gradle に設定すれば jar ファイルに入らないようにできることが分かりました。

groovy-script-executor/src/main/groovy の下に sample パッケージを作成して、その下にサンプルの Groovy スクリプトを置くことにします。まずは Hello, World を出力する簡単な Groovy スクリプトを作成して動作確認する で作成した HelloWorld.groovy を置きます。

jar ファイル作成時に sample パッケージの下の *.groovy が入らないよう build.gradle に設定を追加します。

[compileJava, compileTestGroovy, compileTestJava]*.options*.encoding = "UTF-8"
[compileJava, compileTestGroovy, compileTestJava]*.options*.compilerArgs = ["-Xlint:all,-options,-processing,-path"]
sourceSets {
    main {
        compileGroovy {
            exclude 'sample/*.groovy'
        }
    }
}
..........
  • sourceSets { ... } の設定を追加します。

sourceSets { ... } の設定がないと生成された jar ファイルの中に HelloWorld.class が入りますが、

f:id:ksby:20211029232710p:plain

sourceSets { ... } の設定があると入りません。

f:id:ksby:20211029233027p:plain

また groovy-script-executor/src/main/groovy/sample の下に置いた Groovy スクリプトは、クラス名か main メソッドの左側に表示されている矢印アイコンをクリックして表示される「Run 'HelloWorld.main()'」メニューを選択する(あるいは Ctrl+Shift+F10 を押す)ことで、

f:id:ksby:20211029234611p:plain

実行して結果を確認することが出来ます。

f:id:ksby:20211029234913p:plain

サンプルの Groovy スクリプト で利用する PostgreSQLMySQL を起動するための docker-compose.yml を作成する+PostgreSQL に sampledb、MySQL に testdb を作成する

プロジェクトの root ディレクトリ直下に .env を作成し、以下の内容を記述します。PostgreSQLMySQL のバージョンは AWS の Aurora でサポートされているバージョンにします(PostgreSQL は LTS の 11.9、MySQL は version 2 の 5.7)。

POSTGRESQL_VERSION=11.9
PGADMIN4_VERSION=6.1

MYSQL_VERSION=5.7.35
ADMINER_VERSION=4.8.1

PostgreSQL には sampledb、MySQL には testdb という名前のデータベースを作成します。

root ディレクトリ直下に docker/flyway/db/init ディレクトリを作成し(この次に Flyway でテーブルを作成するので docker/flyway ディレクトリの下に SQL ファイルを置いています)、その下に create_database_postgresql.sql を作成して以下の内容を記述します。

CREATE USER sampledb_user PASSWORD 'xxxxxxxx';
GRANT pg_read_server_files TO sampledb_user;
CREATE DATABASE sampledb OWNER sampledb_user ENCODING 'UTF8' LC_COLLATE 'ja_JP.UTF-8' LC_CTYPE 'ja_JP.UTF-8';

同じ場所に create_database_mysql.sql を作成して以下の内容を記述します。

create database if not exists testdb character set utf8mb4;

create user 'testdb_user'@'%' identified by 'xxxxxxxx';
grant all privileges ON testdb.* to 'testdb_user'@'%' with grant option;
grant select ON performance_schema.user_variables_by_thread to 'testdb_user'@'%';
flush privileges;

root ディレクトリ直下に docker-compose.yml を作成し、以下の内容を記述します。

# docker-compose build --no-cache
# docker-compose up -d
# docker-compose down
version: '3'

services:
  #############################################################################
  # PostgreSQL
  #
  postgresql:
    image: postgres:${POSTGRESQL_VERSION}-alpine
    container_name: postgresql
    ports:
      - "5432:5432"
    environment:
      - TZ=Asia/Tokyo
      - LANG=ja_JP.UTF-8
      - POSTGRES_PASSWORD=xxxxxxxx
    volumes:
      - ./docker/flyway/db/init/create_database_postgresql.sql:/docker-entrypoint-initdb.d/create_database.sql
      - ./docker/flyway/db/migration/postgresql_sampledb/jm2020.csv:/docker-entrypoint-initdb.d/jm2020.csv

  #############################################################################
  # pgAdmin 4
  #
  # URL
  # http://localhost:12000/
  pgadmin4:
    build:
      context: ./docker/pgadmin4
      args:
        - PGADMIN4_VERSION=${PGADMIN4_VERSION}
    image: dpage/pgadmin4:${PGADMIN4_VERSION}-custom
    container_name: pgadmin4
    ports:
      - "12000:80"
    environment:
      # TZ=Asia/Tokyo を設定してみたが日本時間に変わらなかったのでコメントアウトしておく
      # - TZ=Asia/Tokyo
      # PGADMIN_DEFAULT_EMAIL には接続する PostgreSQL の ユーザ名を設定する(サーバを追加する時楽なため)
      - PGADMIN_DEFAULT_EMAIL=postgres@example.com
      - PGADMIN_DEFAULT_PASSWORD=xxxxxxxx
      # PGADMIN_CONFIG_CONSOLE_LOG_LEVEL は debug 用
      # 設定値は https://www.pgadmin.org/docs/pgadmin4/development/config_py.html の CONSOLE_LOG_LEVEL 参照
      - PGADMIN_CONFIG_CONSOLE_LOG_LEVEL=10
      - PGADMIN_CONFIG_SESSION_DB_PATH='/var/lib/pgadmin_session'
    volumes:
      - ./docker/pgadmin4/data:/var/lib/pgadmin

  #############################################################################
  # MySQL
  #
  mysql:
    image: mysql:${MYSQL_VERSION}
    container_name: mysql
    command: --default-authentication-plugin=mysql_native_password
    ports:
      - 3306:3306
    environment:
      - TZ=Asia/Tokyo
      - MYSQL_ROOT_PASSWORD=xxxxxxxx
    volumes:
      - ./docker/flyway/db/init/create_database_mysql.sql:/docker-entrypoint-initdb.d/create_database.sql

  #############################################################################
  # Adminer
  #
  # URL
  # http://localhost:13000/
  adminer:
    image: adminer:${ADMINER_VERSION}
    container_name: adminer
    ports:
      - 13000:8080
    environment:
      - TZ=Asia/Tokyo
      - ADMINER_DEFAULT_SERVER=mysql

docker-compose up -d コマンドを実行します。

f:id:ksby:20211030223058p:plain

http://localhost:12000/ にアクセス・ログインして pgAdmin 4 で sampledb が作成されていることを確認します。

f:id:ksby:20211030223432p:plain

http://localhost:13000/ にアクセス・ログインして Adminer で testdb が作成されていることを確認します。

f:id:ksby:20211030223646p:plain

docker-compose down コマンドを実行します。

PostgreSQL の sampledb にテーブルを作成する

国立国会図書館オープンデータセット の「国内刊行出版物の書誌情報 2020年分(tsv形式)」を取り込んだテーブルを作成します。

.env に FLYWAY_VERSION と sampledb に接続するための URL、USER、PASSWORD を記述します。

POSTGRESQL_VERSION=11.9
PGADMIN4_VERSION=6.1

MYSQL_VERSION=5.7.35
ADMINER_VERSION=4.8.1

FLYWAY_VERSION=8.0.2
SAMPLEDB_URL=jdbc:postgresql://postgresql/sampledb
SAMPLEDB_USER=sampledb_user
SAMPLEDB_PASSWORD=xxxxxxxx

docker/flyway/db の下に migration/postgresql_sampledb ディレクトリを作成し、その下に V1_0_0__create_publications.sql を作成して以下の内容を記述します。

create table publications
(
    id                bigserial constraint publications_pk primary key,
    url               text,
    fmt               text,
    book_number       text,
    isbm              text,
    issn              text,
    title_author      text,
    edition_indicates text,
    series            text,
    publication       text,
    remarks           text
);

docker-compose.yml の一番下に flyway_sampledb を追加します。

  ..........

  #############################################################################
  # Flyway for postgresql@sampledb
  #
  flyway_sampledb:
    image: flyway/flyway:${FLYWAY_VERSION}-alpine
    container_name: flyway_sampledb
    environment:
      - TZ=Asia/Tokyo
    volumes:
      - ./docker/flyway/db/migration/postgresql_sampledb:/flyway/sql
    command: -url="${SAMPLEDB_URL}" -user=${SAMPLEDB_USER} -password=${SAMPLEDB_PASSWORD} -connectRetries=60 migrate
    depends_on:
      - postgresql
    # 下の3行は debug 用
    # うまく動かない時はコメントアウトを解除した後、
    # docker exec -it flyway_sampledb /bin/sh
    # で接続してから
    # flyway <command に記述した文字列>
    # を実行してみる
    #
    # entrypoint: /bin/sh
    # stdin_open: true
    # tty: true

docker-compose up -d コマンドを実行します。

IntelliJ IDEA の Database Tool Window を設定して publications テーブルが作成されていることを確認します。

f:id:ksby:20211031105032p:plain

国内刊行出版物の書誌情報(直近年1年分) から jm2020.txt をダウンロードし、Database Tool Window で import します。import できないデータが数十件出ますが、それらは無視します。

f:id:ksby:20211031110601p:plain f:id:ksby:20211031110715p:plain

データが import できたので今度は CSV ファイル(jm2020.csv)に export します。

f:id:ksby:20211031111952p:plain f:id:ksby:20211031112048p:plain

export した jm2020.csv を docker/flyway/db/migration/postgresql_sampledb の下に移動した後、docker-compose.yml を以下のように変更します。

  postgresql:
    image: postgres:${POSTGRESQL_VERSION}-alpine
    container_name: postgresql
    ports:
      - "5432:5432"
    environment:
      - TZ=Asia/Tokyo
      - LANG=ja_JP.UTF-8
      - POSTGRES_PASSWORD=xxxxxxxx
    volumes:
      - ./docker/flyway/db/init/create_database_postgresql.sql:/docker-entrypoint-initdb.d/create_database.sql
      - ./docker/flyway/db/migration/postgresql_sampledb/jm2020.csv:/docker-entrypoint-initdb.d/jm2020.csv
  • postgresql の volumes に - ./docker/flyway/db/migration/postgresql_sampledb/jm2020.csv:/docker-entrypoint-initdb.d/jm2020.csv を追加します。

docker/flyway/db/migration/postgresql_sampledb/V1_0_0__create_publications.sql を以下のように変更します。

create table publications
(
    ..........
);

COPY publications
FROM '/docker-entrypoint-initdb.d/jm2020.csv'
WITH CSV;

  • COPY publications FROM '/docker-entrypoint-initdb.d/jm2020.csv' WITH CSV; を追加します。

docker-compose downdocker-compose up -d コマンドを実行し、publications テーブルにデータが取り込まれることを確認します。

f:id:ksby:20211031113453p:plain

MySQL の testdb にテーブルを作成する

.env に testdb に接続するための URL、USER、PASSWORD を記述します。

..........

FLYWAY_VERSION=8.0.2
SAMPLEDB_URL=jdbc:postgresql://postgresql/sampledb
SAMPLEDB_USER=sampledb_user
SAMPLEDB_PASSWORD=xxxxxxxx
TESTDB_URL=jdbc:mysql://mysql:3306/testdb?sslMode=DISABLED&characterEncoding=utf8
TESTDB_USER=testdb_user
TESTDB_PASSWORD=xxxxxxxx

docker/flyway/db/migration の下に mysql_testdb ディレクトリを作成し、その下に V1_0_0__create_book.sql を作成して以下の内容を記述します。

create table book
(
    id     bigint auto_increment primary key,
    isbm   text null,
    title  text null,
    author text null
);

docker-compose.yml の一番下に flyway_testdb を追加します。

  ..........

  #############################################################################
  # Flyway for mysql@testdb
  #
  flyway_testdb:
    image: flyway/flyway:${FLYWAY_VERSION}-alpine
    container_name: flyway_testdb
    environment:
      - TZ=Asia/Tokyo
    volumes:
      - ./docker/flyway/db/migration/mysql_testdb:/flyway/sql
    command: -url="${TESTDB_URL}" -user=${TESTDB_USER} -password=${TESTDB_PASSWORD} -connectRetries=60 migrate
    depends_on:
      - mysql
    # 下の3行は debug 用
    # うまく動かない時はコメントアウトを解除した後、
    # docker exec -it flyway_testdb /bin/sh
    # で接続してから
    # flyway <command に記述した文字列>
    # を実行してみる
    #
    # entrypoint: /bin/sh
    # stdin_open: true
    # tty: true

docker-compose downdocker-compose up -d コマンドを実行します。

IntelliJ IDEA の Database Tool Window を設定して book テーブルが作成されていることを確認します。。。が、作成されていませんね?

f:id:ksby:20211031114908p:plain

flyway_testdb コンテナのログを見ると ERROR: Flyway Teams Edition or MySQL upgrade required: MySQL 5.7 is no longer supported by Flyway Community Edition, but still supported by Flyway Teams Edition. のログが出力されていました。

f:id:ksby:20211031115110p:plain

Flyway のバージョンを 7 系の最新(7.15.0)に変更します。.env で FLYWAY_VERSION=8.0.2FLYWAY_VERSION=7.15.0 に変更します。

..........

FLYWAY_VERSION=7.15.0
SAMPLEDB_URL=jdbc:postgresql://postgresql/sampledb
SAMPLEDB_USER=sampledb_user
SAMPLEDB_PASSWORD=xxxxxxxx
TESTDB_URL=jdbc:mysql://mysql:3306/testdb?sslMode=DISABLED&characterEncoding=utf8
TESTDB_USER=testdb_user
TESTDB_PASSWORD=xxxxxxxx

docker-compose downdocker-compose up -d コマンドを実行すると今度は book テーブルが作成されました。MySQL 5.7 のサポートが終了したのは Flyway 8 からでした。

f:id:ksby:20211031115740p:plain

履歴

2021/10/31
初版発行。

Grooy スクリプトをそのまま渡して実行する Spring Boot+Picocli ベースのコマンドラインアプリを作成する ( その2 )( groovy-script-executor.jar を作成する )

概要

記事一覧はこちらです。

Grooy スクリプトをそのまま渡して実行する Spring Boot+Picocli ベースのコマンドラインアプリを作成する ( その1 )( 概要 ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • Spring Boot+Picocli ベースのコマンドラインアプリ(groovy-script-executor.jar)を作成します。
    • Hello, World を出力する簡単な Groovy スクリプトを作成して動作確認します。

参照したサイト・書籍

  1. picocli - a mighty tiny command line interface
    https://picocli.info/

目次

  1. https://github.com/ksby/groovy-script-executor を clone して Gradle の Multi-project を作成する
  2. groovy-script-executor サブプロジェクトを作成して実装する
  3. Hello, World を出力する簡単な Groovy スクリプトを作成して動作確認する
  4. 起動時オプションで -Dfile.encoding=UTF-8 等を指定したいので gse.bat を作成する

手順

https://github.com/ksby/groovy-script-executor を clone して Gradle の Multi-project を作成する

https://github.com/ksby/groovy-script-executor を clone した後、Spring Initializr で demo プロジェクトを作成してから以下のファイルをコピーします。

gradlew init コマンドを実行します。

f:id:ksby:20211024174915p:plain

settings.gradle を以下の内容に書き替えます。

rootProject.name = 'groovy-script-executor'

rootDir.eachFileRecurse { f ->
    if (f.name == "build.gradle") {
        String relativePath = f.parentFile.absolutePath - rootDir.absolutePath
        String projectName = relativePath.replaceAll("[\\\\\\/]", ":")
        if (projectName != ":buildSrc") {
            include projectName
        }
    }
}

groovy-script-executor サブプロジェクトを作成して実装する

IntelliJ IDEA のメインメニューから「File」-「New」-「Project...」を選択して「New Project」ダイアログを表示した後、以下の値を入力します。Dependencies では何も選択せず「Finish」ボタンをクリックします。

f:id:ksby:20211024180747p:plain f:id:ksby:20211024180837p:plain

作成されたサブプロジェクトの以下の点を変更します。

  • src ディレクトリ、build.gradle 以外のディレクトリ・ファイルを削除します。
  • GroovyScriptExecutorApplication クラスの名前を Application に変更します。
  • src/test の下をクリアした後、その下に .gitkeep を作成します。

build.gradle を以下の内容にします。

buildscript {
    ext {
        group "ksby"
        version "1.0.0-RELEASE"
    }
    repositories {
        mavenCentral()
        gradlePluginPortal()
    }
}

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

sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17

[compileJava, compileTestGroovy, compileTestJava]*.options*.encoding = "UTF-8"
[compileJava, compileTestGroovy, compileTestJava]*.options*.compilerArgs = ["-Xlint:all,-options,-processing,-path"]
bootJar {
    duplicatesStrategy = DuplicatesStrategy.INCLUDE
}
jar {
    enabled = false
}

springBoot {
    buildInfo()
}

configurations {
    compileOnly.extendsFrom annotationProcessor

    // annotationProcessor と testAnnotationProcessor、compileOnly と testCompileOnly を併記不要にする
    testAnnotationProcessor.extendsFrom annotationProcessor
    testImplementation.extendsFrom compileOnly

    // JUnit 4 が依存関係に入らないようにする
    all {
        exclude group: "junit", module: "junit"
    }
}

repositories {
    mavenCentral()
}

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.1")
    }
}

dependencies {
    def groovyVersion = "3.0.9"
    def picocliVersion = "4.6.1"
    def lombokVersion = "1.18.22"
    def spockVersion = "2.0-groovy-3.0"

    // 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")
    testImplementation("org.springframework.boot:spring-boot-starter-test")

    // dependency-management-plugin によりバージョン番号が自動で設定されないもの、あるいは最新バージョンを指定したいもの
    implementation("com.univocity:univocity-parsers:2.9.1")
    testImplementation("org.assertj:assertj-core:3.21.0")

    // for Groovy
    implementation("org.codehaus.groovy:groovy-all:${groovyVersion}")

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

    // for lombok
    // testAnnotationProcessor、testCompileOnly を併記しなくてよいよう configurations で設定している
    annotationProcessor("org.projectlombok:lombok:${lombokVersion}")
    compileOnly("org.projectlombok:lombok:${lombokVersion}")

    // for JUnit 5
    // junit-jupiter で junit-jupiter-api, junit-jupiter-params, junit-jupiter-engine の3つが依存関係に追加される
    testImplementation("org.junit.jupiter:junit-jupiter")
    testRuntimeOnly("org.junit.platform:junit-platform-launcher")

    // for Spock
    testImplementation("org.spockframework:spock-core:${spockVersion}")
    testImplementation("org.spockframework:spock-spring:${spockVersion}")
}

def jvmArgsForTask = [
        "-ea",
        "-Dfile.encoding=UTF-8",
        "-XX:TieredStopAtLevel=1",
        "-Dspring.main.lazy-initialization=true"
]
def printTestCount = { desc, result ->
    if (!desc.parent) { // will match the outermost suite
        println "Results: ${result.resultType} (${result.testCount} tests, ${result.successfulTestCount} successes, ${result.failedTestCount} failures, ${result.skippedTestCount} skipped)"
    }
}

bootRun {
    jvmArgs = jvmArgsForTask
}

clean {
    doLast {
        project.file("out").deleteDir()
        project.file("src/main/generated").deleteDir()
        project.file("src/test/generated_tests").deleteDir()
    }
}

test {
    jvmArgs = jvmArgsForTask

    // for JUnit 5
    useJUnitPlatform()

    testLogging {
        afterSuite printTestCount
    }
}

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

ksby.cmdapp.groovyscriptexecutor の下に command パッケージを作成した後、GroovyScriptExecutorCommand クラスを新規作成して以下のコードを記述します。

package ksby.cmdapp.groovyscriptexecutor.command;

import groovy.lang.Binding;
import groovy.lang.GroovyShell;
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.*;

@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;

    @Override
    public Integer call() throws IOException {
        Binding binding = new Binding();
        GroovyShell shell = new GroovyShell(binding);
        shell.run(groovyScript, args);
        return ExitCode.OK;
    }

    @Override
    public int getExitCode(Throwable exception) {
        if (exception instanceof Exception) {
            return 1;
        }

        return ExitCode.OK;
    }

    @Override
    public String[] getVersion() {
        return new String[]{buildProperties.getVersion()};
    }

}

src/main/java/ksby/cmdapp/groovyscriptexecutor/Application.java を以下のコードに書き替えます。

package ksby.cmdapp.groovyscriptexecutor;

import ksby.cmdapp.groovyscriptexecutor.command.GroovyScriptExecutorCommand;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.ExitCodeGenerator;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import picocli.CommandLine;

import static picocli.CommandLine.IFactory;

@SpringBootApplication
public class Application implements CommandLineRunner, ExitCodeGenerator {

    private int exitCode;

    private final GroovyScriptExecutorCommand groovyScriptExecutorCommand;

    private final IFactory factory;

    public Application(GroovyScriptExecutorCommand groovyScriptExecutorCommand,
                       IFactory factory) {
        this.groovyScriptExecutorCommand = groovyScriptExecutorCommand;
        this.factory = factory;
    }

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

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

    @Override
    public int getExitCode() {
        return exitCode;
    }

}

src/main/resources/application.properties に以下の内容を記述します。

spring.main.banner-mode=off
logging.level.root=OFF

clean タスク実行 → Rebuild Project 実行 → build タスクを実行します。

f:id:ksby:20211026200530p:plain

build/libs/groovy-script-executor-1.0.0-RELEASE.jar が生成されるので D:\tmp の下にコピーした後、java -jar groovy-script-executor-1.0.0-RELEASE.jar -Hjava -jar groovy-script-executor-1.0.0-RELEASE.jar -V が動作することを確認します。

f:id:ksby:20211026200800p:plain

Hello, World を出力する簡単な Groovy スクリプトを作成して動作確認する

src/main の下に groovy ディレクトリを新規作成し、その下に .gitkeep を作成します。

src/main/groovy の下に HelloWorld.groovy を新規作成した後、以下のコードを記述します。

class HelloWorld {

    static void main(args) {
        println "Hello, World"
    }

}

src/main/groovy/HelloWorld.groovy を D:\tmp の下に移動した後、java -jar groovy-script-executor-1.0.0-RELEASE.jar HelloWorld.groovy を実行すると Hello, World が出力されました。コマンド実行直後は build しているので、Hello, World が出力されるまでに 4~5 秒程度かかります。

f:id:ksby:20211026201835p:plain

起動時オプションで -Dfile.encoding=UTF-8 等を指定したいので gse.bat を作成する

D:\tmp の下に gse.bat を新規作成し、以下のコードを記述します。

@echo off

java -Dfile.encoding=UTF-8 ^
     -XX:TieredStopAtLevel=1 ^
     -Dspring.main.lazy-initialization=true ^
     -jar groovy-script-executor-1.0.0-RELEASE.jar ^
     %*

gse HelloWorld.groovy を実行すると java -jar groovy-script-executor-1.0.0-RELEASE.jar HelloWorld.groovy と同様に Hello, World が出力されます。

f:id:ksby:20211026203900p:plain

履歴

2021/10/26
初版発行。

Grooy スクリプトをそのまま渡して実行する Spring Boot+Picocli ベースのコマンドラインアプリを作成する ( その1 )( 概要 )

概要

記事一覧はこちらです。

  • CSVファイルを読み込んで DB のテーブルを更新したり、DB のテーブルのデータを CSVファイルに出力したりするちょっとした Groovy スクリプトを build せずにそのまま実行したいと思ったので、作成してみることにします。
  • Groovy スクリプトは Groovy をインストールすれば groovy コマンドでそのまま実行出来ますが、今回は以下の内容で実現します。
    • Spring Boot+Picocli ベースのコマンドラインアプリ(groovy-script-executor-xxx.jar)を作成します。
    • コマンドラインアプリ(groovy-script-executor-xxx.jar)の中に必要なモジュールを全て入れておきます。
    • java -jar groovy-script-executor.jar <Groovyスクリプト> <引数...> のコマンドで実行できるようにします。
  • @SpringBootApplication アノテーションを付与した Groovy スクリプトのアプリケーションも実行できるといいな。。。 実行できれば Spring Integeration の SFTP Adapters を利用してファイルのアップロード・ダウンロードができるかもしれません。コマンドラインアプリを作成してから試してみることにします。
  • GraalVM で Windows の exe ファイル作成にもチャレンジしてみることにします。