かんがるーさんの日記

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

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