かんがるーさんの日記

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

Spring Boot + Spring Integration でいろいろ試してみる ( その31 )( Aggregator のサンプルを作ってみる )

概要

記事一覧はこちらです。

  • Spring Integration DSL8.4. Aggregator を使用したサンプルを作成します。
  • Aggregator でメッセージがどのように集約されるのかを見られるようにするために、単体の Redis サーバを Docker Compose で構築して MessageStore として利用します。Redis クライアントには JSON のデータを見たいので Medis を使用します。
  • サンプルは以前流行ったズンドコキヨシをベースに Aggregator の動きが分かるように少しルールを変えたものを作成してみます。

参照したサイト・書籍

  1. Spring Integration - 8.4. Aggregator
    https://docs.spring.io/spring-integration/docs/current/reference/html/#aggregator

  2. luin/medis
    https://github.com/luin/medis

目次

  1. ksbysample-eipapp-aggregator プロジェクトを作成する
  2. Docker Compose で単一の Redis サーバを構築する
  3. Redis の GUI クライアントとして Medis をインストールする
  4. "ずん" あるいは "どこ" のメッセージを送信して5つずつ aggregate するサンプルを作成する
  5. "ずん"、"ずん"、"ずん"、"ずん"、"どこ" で1セットのメッセージを送信後にバラバラに分解してから再び1セットのメッセージに aggregate するサンプルを作成する

手順

ksbysample-eipapp-aggregator プロジェクトを作成する

Spring Initializr でプロジェクトの雛形を作成した後、build.gradle を以下のように変更します。

plugins {
    id 'org.springframework.boot' version '2.1.5.RELEASE'
    id 'java'
}

apply plugin: 'io.spring.dependency-management'

group = 'ksbysample.eipapp'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'

repositories {
    mavenCentral()
}

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

dependencies {
    def lombokVersion = "1.18.6"

    implementation("org.springframework.boot:spring-boot-starter-integration")
    implementation("org.springframework.boot:spring-boot-starter-data-redis")
    implementation("org.springframework.integration:spring-integration-redis")
    implementation("org.springframework.boot:spring-boot-starter-json") {
        exclude group: "org.springframework", module: "spring-web"
    }
    testImplementation("org.springframework.boot:spring-boot-starter-test")

    // for lombok
    // testAnnotationProcessor、testCompileOnly を併記しなくてよいよう configurations で設定している
    annotationProcessor("org.projectlombok:lombok:${lombokVersion}")
    compileOnly("org.projectlombok:lombok:${lombokVersion}")
}
  • Redis を MessageStore として利用するので以下の行を追加します。
    • implementation("org.springframework.boot:spring-boot-starter-data-redis")
    • implementation("org.springframework.integration:spring-integration-redis")
  • Redis に JSON フォーマットでメッセージを格納したいので以下の行を追加します。spring-boot-starter-json を追加すると org.springframework:spring-web も依存関係に追加されてしまうのですが、今回は不要なので exclude で除外します。
    • implementation("org.springframework.boot:spring-boot-starter-json")
  • ログを出力する時に @Slf4j アノテーションを使いたいので以下の行を追加して lombok を使えるようにします。
    • annotationProcessor("org.projectlombok:lombok:${lombokVersion}")
    • compileOnly("org.projectlombok:lombok:${lombokVersion}")

ディレクトリ構成は以下のようにしています。

f:id:ksby:20190615005011p:plain

Docker Compose で単一の Redis サーバを構築する

プロジェクトのルートディレクトリ直下に docker-compose.yml を新規作成し、以下の内容を記述します。

version: '3'

services:
  # 起動したコンテナに /bin/sh でアクセスする場合には以下のコマンドを実行する
  # docker exec -it redis /bin/sh
  #
  # 起動したコンテナの redis に redis-cli でアクセスするには以下のコマンドを実行する
  # docker exec -it redis redis-cli
  #
  #############################################################################
  # 単体 Redis サーバ
  redis:
    image: redis:5.0.5
    container_name: redis
    ports:
      - "6379:6379"

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

f:id:ksby:20190615005616p:plain

Redis の GUI クライアントとして Medis をインストールする

https://github.com/luin/medis から Medis on Windowsインストーラをダウンロードしてインストールします。

"ずん" あるいは "どこ" のメッセージを送信して5つ ずつ aggregate するサンプルを作成する

以下の動作を行うサンプルを作成します。

  • "ずん"、"どこ" をランダムに出力する MessageSource を作成する。”ずん" を 4/5、"どこ" を 1/5 の確立で返す。
  • MessageSource からは 15秒ごとに取得する(aggregator の動作を確認するために時間を遅めにする)。
  • aggregator は Redis を MessageStore として使用し、メッセージが5つ溜まったら集約して1つのメッセージにして次の処理に流す。タイムアウトは設定しない(5つ溜まるまでずっと待つ)。
  • 最後に集約されたメッセージが "ずん", "ずん", "ずん", "ずん", "どこ" だったら "きよし!" とログに出力する。

src/main/java/ksbysample/eipapp/aggregator の下に RandomZundokoFlowConfig.java を新規作成し、以下の内容を記述します。

package ksbysample.eipapp.aggregator;

import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.integration.core.MessageSource;
import org.springframework.integration.dsl.IntegrationFlow;
import org.springframework.integration.dsl.IntegrationFlows;
import org.springframework.integration.dsl.Pollers;
import org.springframework.integration.redis.store.RedisMessageStore;
import org.springframework.integration.support.json.JacksonJsonUtils;
import org.springframework.messaging.support.MessageBuilder;

import java.util.Arrays;
import java.util.List;
import java.util.Random;

@Slf4j
@Configuration
public class RandomZundokoFlowConfig {

    private static final Random r = new Random();
    private static final String[] ZUNDOKO = new String[]{"ずん", "ずん", "ずん", "ずん", "どこ"};

    private static final long POLLER_DELAY_PERIOD = 15000L;

    // Redis の設定はデフォルト値をそのまま使用するので、application.poperties には何も記載していない
    // https://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html#common-application-properties
    // の spring.redis.~ を参照。
    private final RedisConnectionFactory redisConnectionFactory;

    public RandomZundokoFlowConfig(RedisConnectionFactory redisConnectionFactory) {
        this.redisConnectionFactory = redisConnectionFactory;
    }

    /**
     * Redis を MessageStore として使用するための設定。
     * Redis に格納する message は JSON フォーマットにする。
     * https://docs.spring.io/spring-integration/docs/current/reference/html/#redis-message-store
     * 参照。
     *
     * @return {@link RedisMessageStore} object
     */
    @Bean
    public RedisMessageStore redisMessageStore() {
        RedisMessageStore store = new RedisMessageStore(redisConnectionFactory);
        ObjectMapper mapper = JacksonJsonUtils.messagingAwareMapper();
        RedisSerializer<Object> serializer = new GenericJackson2JsonRedisSerializer(mapper);
        store.setValueSerializer(serializer);
        return store;
    }

    @Bean
    public MessageSource<String> zundokoMessageSource() {
        return () -> MessageBuilder.withPayload(ZUNDOKO[r.nextInt(5)]).build();
    }

    @Bean
    public IntegrationFlow zundokoFlow() {
        return IntegrationFlows.from(zundokoMessageSource()
                , e -> e.poller(Pollers.fixedDelay(POLLER_DELAY_PERIOD)))
                // kiyoshiFlow にメッセージを送信する
                .channel(kiyoshiFlow().getInputChannel())
                .get();
    }

    @Bean
    public IntegrationFlow kiyoshiFlow() {
        return f -> f
                // メッセージが5つ溜まったら集約して次の処理に流す
                // * 流れてくる message の header に correlationId がないので
                //   correlationExpression("1") を記述して固定で "1" という扱いにする。
                //   これで全てのメッセージが1つの MESSAGE_QUEUE に蓄積される。
                // * expireGroupsUponCompletion(true) を記述すると5つ集約したメッセージを
                //   次の処理に流した後に再びメッセージが蓄積されるようになる。
                .aggregate(a -> a.correlationExpression("1")
                        .messageStore(redisMessageStore())
                        .releaseStrategy(g -> g.size() == 5)
                        .expireGroupsUponCompletion(true))
                // {"ずん", "ずん", "ずん", "ずん", "どこ"} と揃っていたら "きよし!" を出力する
                .log()
                .<List<String>>handle((p, h) -> {
                    if (p.size() == 5 && p.equals(Arrays.asList(ZUNDOKO))) {
                        log.error("きよし!");
                    }
                    return null;
                });
    }

}

実行して Redis 内に蓄積されている message の状態を見ると、"MESSAGE_GROUP_" + <correlationIdとして使用する文字列(今回は "1" 固定)> のレコードと個々の message のレコードがあることが確認できます。

f:id:ksby:20190615100441p:plain f:id:ksby:20190615100523p:plain

message が5つ蓄積されて aggregator により1つの message に集約されて次の処理に渡されると、蓄積されていた MESSAGE_GROUP と MESSAGE は削除されます。

f:id:ksby:20190615100821p:plain

コンソールには以下のように出力されます。payload=[ずん, ずん, ずん, ずん, どこ] になっていると "きよし!" の文字列が出力されています。またスレッド名を見るとバラバラでした。1つのスレッドで処理するわけではないようです。

f:id:ksby:20190615102346p:plain

.outputProcessor(...) を使うと次の処理に渡す時の palyload にセットする値を変更することができます。デフォルトでは List でまとめられますが、以下のように実装するとカンマで結合した String オブジェクトになります。

    @Bean
    public IntegrationFlow kiyoshiFlow() {
        return f -> f
                // メッセージが5つ溜まったら集約して次の処理に流す
                // * 流れてくる message の header に correlationId がないので
                //   correlationExpression("1") を記述して固定で "1" という扱いにする。
                //   これで全てのメッセージが1つの MESSAGE_QUEUE に蓄積される。
                // * expireGroupsUponCompletion(true) を記述すると5つ集約したメッセージを
                //   次の処理に流した後に再びメッセージが蓄積されるようになる。
                .aggregate(a -> a.correlationExpression("1")
                        .messageStore(redisMessageStore())
                        .releaseStrategy(g -> g.size() == 5)
                        .outputProcessor(g -> {
                            String msg = g.getMessages().stream()
                                    .map(m -> (String) m.getPayload())
                                    .collect(Collectors.joining(","));
                            if (msg.equals("ずん,ずん,ずん,ずん,どこ")) {
                                msg += ",きよし!";
                            }
                            return msg;
                        })
                        .expireGroupsUponCompletion(true))
                // {"ずん", "ずん", "ずん", "ずん", "どこ"} と揃っていたら "きよし!" を出力する
                .log()
                .nullChannel();
//                .<List<String>>handle((p, h) -> {
//                    if (p.size() == 5 && p.equals(Arrays.asList(ZUNDOKO))) {
//                        log.error("きよし!");
//                    }
//                    return null;
//                });
    }

f:id:ksby:20190615105148p:plain

Aggregator 用のクラスを定義して @CorrelationStrategy、@ReleaseStrategy、@Aggregator アノテーションを付与したメソッドを用意する書き方もあります。

    @Bean
    public IntegrationFlow kiyoshiFlow() {
        return f -> f
                // メッセージが5つ溜まったら集約して次の処理に流す
                // * 流れてくる message の header に correlationId がないので
                //   correlationExpression("1") を記述して固定で "1" という扱いにする。
                //   これで全てのメッセージが1つの MESSAGE_QUEUE に蓄積される。
                // * expireGroupsUponCompletion(true) を記述すると5つ集約したメッセージを
                //   次の処理に流した後に再びメッセージが蓄積されるようになる。
                .aggregate(a -> a.processor(new ZundokoAggregator())
                        .messageStore(redisMessageStore())
                        .expireGroupsUponCompletion(true))
                // {"ずん", "ずん", "ずん", "ずん", "どこ"} と揃っていたら "きよし!" を出力する
                .log()
                .<List<String>>handle((p, h) -> {
                    if (p.size() == 5 && p.equals(Arrays.asList(ZUNDOKO))) {
                        log.error("きよし!");
                    }
                    return null;
                });
    }

    static class ZundokoAggregator {

        @CorrelationStrategy
        public String correlationKey(Message<?> message) {
            return "1";
        }

        @ReleaseStrategy
        public boolean canRelease(List<Message<?>> messages) {
            return messages.size() == 5;
        }

        @Aggregator
        public List<String> aggregate(List<String> payloads) {
            // .outputProcessor(...) での加工処理を実装したい場合にはこのメソッド内に記述する
            return payloads;
        }

    }

f:id:ksby:20190615110538p:plain

"ずん"、"ずん"、"ずん"、"ずん"、"どこ" で1セットのメッセージを送信後分解してから再び1セットのメッセージに aggregate するサンプルを作成する

次に以下の動作を行うサンプルを作成します。

  • "ずん", "ずん", "ずん", "ずん", "どこ" の配列が palyload にセットされたメッセージを出力する MessageSource を作成する。
  • MessageSource からは 5秒ごとに取得する。
  • MessageSource からメッセージを取得したら Splitter で分解して、マルチスレッドで処理させるための channel に渡す。
  • マルチスレッド側ではランダムで決められた秒数(最大5秒)待機してから aggregator へ渡す。
  • aggregator では Splitter で分割されていたメッセージを全て集めたら1つのメッセージにして次の処理に流す。また3秒以内に分割していたメッセージが全て集まらなかった時には MESSAGE_GROUP を破棄する。
  • 集約されたメッセージが "ずん", "ずん", "ずん", "ずん", "どこ" だったら "きよし!" とログに出力する。

src/main/java/ksbysample/eipapp/aggregator/RandomZundokoFlowConfig.java を実行しないためにクラスに付与した @Configuration アノテーションコメントアウトした後、

@Slf4j
//@Configuration
public class RandomZundokoFlowConfig {

src/main/java/ksbysample/eipapp/aggregator の下に SplitZundokoFlowConfig.java を新規作成し、以下の内容を記述します。

package ksbysample.eipapp.aggregator;

import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.integration.core.MessageSource;
import org.springframework.integration.dsl.DelayerEndpointSpec;
import org.springframework.integration.dsl.IntegrationFlow;
import org.springframework.integration.dsl.IntegrationFlows;
import org.springframework.integration.dsl.Pollers;
import org.springframework.integration.handler.LoggingHandler;
import org.springframework.integration.redis.store.RedisMessageStore;
import org.springframework.integration.support.json.JacksonJsonUtils;
import org.springframework.messaging.support.MessageBuilder;

import java.util.Arrays;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadLocalRandom;

@Slf4j
@Configuration
public class SplitZundokoFlowConfig {

    private static final String[] ZUNDOKO = new String[]{"ずん", "ずん", "ずん", "ずん", "どこ"};

    private static final long POLLER_DELAY_PERIOD = 5000L;
    private static final int DELAY_MAX_VALUE = 5000;
    private static final long MESSAGE_GROUP_TIMEOUT = 3000L;

    // Redis の設定はデフォルト値をそのまま使用するので、application.poperties には何も記載していない
    // https://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html#common-application-properties
    // の spring.redis.~ を参照。
    private final RedisConnectionFactory redisConnectionFactory;

    public SplitZundokoFlowConfig(RedisConnectionFactory redisConnectionFactory) {
        this.redisConnectionFactory = redisConnectionFactory;
    }

    /**
     * Redis を MessageStore として使用するための設定。
     * Redis に格納する message は JSON フォーマットにする。
     * https://docs.spring.io/spring-integration/docs/current/reference/html/#redis-message-store
     * 参照。
     *
     * @return {@link RedisMessageStore} object
     */
    @Bean
    public RedisMessageStore redisMessageStore() {
        RedisMessageStore store = new RedisMessageStore(redisConnectionFactory);
        ObjectMapper mapper = JacksonJsonUtils.messagingAwareMapper();
        RedisSerializer<Object> serializer = new GenericJackson2JsonRedisSerializer(mapper);
        store.setValueSerializer(serializer);
        return store;
    }

    @Bean
    public MessageSource<String[]> zundokoMessageSource() {
        return () -> MessageBuilder.withPayload(ZUNDOKO).build();
    }

    @Bean
    public IntegrationFlow zundokoFlow() {
        return IntegrationFlows.from(zundokoMessageSource()
                , e -> e.poller(Pollers.fixedDelay(POLLER_DELAY_PERIOD)))
                // Message<String[]> --> Message<String> x 5 に分割する
                .split()
                // ここから下はマルチスレッドで処理する
                .channel(c -> c.executor(Executors.newFixedThreadPool(5)))
                // 5秒以内(ランダムで決める)の間 delay する
                // group timeout した時にどのメッセージが原因だったのかが分かるようにするために
                // delay header を追加して値をセットする
                .enrichHeaders(h -> h.headerFunction("delay",
                        m -> String.valueOf(ThreadLocalRandom.current().nextInt(DELAY_MAX_VALUE))))
                .delay("ZUNDOKO_DELAYER",
                        (DelayerEndpointSpec e) -> e.delayExpression("headers.delay"))
                // kiyoshiFlow にメッセージを送信する
                .channel(kiyoshiFlow().getInputChannel())
                .get();
    }

    @Bean
    public IntegrationFlow kiyoshiFlow() {
        return f -> f
                .aggregate(a -> a
                        .messageStore(redisMessageStore())
                        .releaseStrategy(g -> g.getMessages().size() == g.getSequenceSize())
                        .expireGroupsUponCompletion(true)
                        // .groupTimeout(...) で指定した時間内に aggregator で処理されなかったメッセージは
                        // .discardChannel(...) で指定した channel に送信される
                        // ※既に MESSAGE_GROUP に蓄積されていたメッセージも discardChannel に送信される
                        .groupTimeout(MESSAGE_GROUP_TIMEOUT)
                        .discardChannel(discardFlow().getInputChannel()))
                // {"ずん", "ずん", "ずん", "ずん", "どこ"} と揃っていたら "きよし!" を出力する
                .log()
                .<List<String>>handle((p, h) -> {
                    if (p.size() == 5 && p.equals(Arrays.asList(ZUNDOKO))) {
                        log.error("きよし!");
                    }
                    return null;
                });
    }

    /**
     * MESSAGE_GROUP_TIMEOUT で指定された時間(3秒)以内に aggregator で処理されなかった MESSAGE GROUP
     * のメッセージが送信されてくる Flow
     *
     * @return {@link IntegrationFlow} object
     */
    @Bean
    public IntegrationFlow discardFlow() {
        return f -> f
                .log(LoggingHandler.Level.WARN)
                .nullChannel();
    }

}

実行して Redis 内に蓄積されている message の状態を見ると、"MESSAGE_GROUP_" の後には correlationId としてセットされている UUID が表示されています。

f:id:ksby:20190615125003p:plain

コンソールを見ると "ずん", "ずん", "ずん", "ずん", "どこ" に戻った場合には "きよし!" のログが出力されており、また3秒以内にメッセージが全て集まらなかった場合には "Expiring MessageGroup with correlaionKey[...]" のログと既に蓄積されていたメッセージが出力されて、その後に後からきたメッセージが出力されていました(header の delay を見ると4秒以上の遅延時間がセットされていることが分かります)。

f:id:ksby:20190615125319p:plain

履歴

2019/06/15
初版発行。

IntelliJ IDEA を 2019.1.2 → 2019.1.3 へバージョンアップ

IntelliJ IDEA を 2019.1.2 → 2019.1.3 へバージョンアップする

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

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

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

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

    f:id:ksby:20190605084348p:plain

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

    f:id:ksby:20190605084441p:plain

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

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

    f:id:ksby:20190605085056p:plain

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

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

  8. 更新可能な Plugin があるというダイアログが画面右下に表示されたので、再度 IntelliJ IDEA のメインメニューから「Help」-「Check for Updates...」を選択します。

  9. IDE and Plugin Updates」ダイアログが表示されますので「Update」ボタンをクリックします。

    f:id:ksby:20190605085316p:plain

  10. Plugin がインストールされます。画面右下に Restart のダイアログが表示されますので、リンクをクリックして IntelliJ IDEA を再起動します。

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

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

    f:id:ksby:20190605090613p:plain

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

    f:id:ksby:20190605091450p:plain f:id:ksby:20190605092209p:plain

IntelliJ IDEA を 2019.1.1 → 2019.1.2 へバージョンアップ

IntelliJ IDEA を 2019.1.1 → 2019.1.2 へバージョンアップする

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

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

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

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

    f:id:ksby:20190515205305p:plain

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

    f:id:ksby:20190515205410p:plain

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

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

    f:id:ksby:20190515210152p:plain

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

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

  8. 更新可能な Plugin があるというダイアログが画面右下に表示されたので、再度 IntelliJ IDEA のメインメニューから「Help」-「Check for Updates...」を選択します。

  9. IDE and Plugin Updates」ダイアログが表示されますので「Update」ボタンをクリックします。

  10. Plugin がインストールされます。画面右下に Restart のダイアログが表示されますので、リンクをクリックして IntelliJ IDEA を再起動します。

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

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

    f:id:ksby:20190515211804p:plain

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

    f:id:ksby:20190515232215p:plain f:id:ksby:20190515232935p:plain

Gradle で Multi-project を作成する ( 感想 )

記事一覧はこちらです。

  • Multi-project は Gradle Guides の Creating Multi-project Builds を見ながらやればそれ程難しくはない印象です。単に1つのプロジェクトに複数のサブプロジェクトを入れるだけ(スタブやテスト用のアプリケーションを置くだけで設定の共通化をしない)ならば settings.gradle に include を書くだけでよいのは簡単で便利だなと思いました。

  • Spring Boot ベースのプロジェクトで実行可能 Jar ではなくライブラリの jar を作成するには build.gradle に bootJar { enabled = false } jar { enabled = true } を記述すればよいということも今回初めて知りました。

    Spring Boot もバージョン 2.x から bootJar タスクで実行可能 Jar を作成するようになりましたが、バージョン 1.x の頃の記事を見直してみると jar タスクを実行してから bootRepackage タスクを実行していました。これまでこの違いをきちんと認識出来ていませんでした。。。

  • 実行可能 Jar を起動するのに launcher subclass というものがあること、launcher subclass には JarLauncher, WarLauncher, PropertiesLauncher があること、PropertiesLauncher を使用すれば実行可能 Jar の外側に置いたライブラリ Jar を利用することができること、も今回始めて知りました。PropertiesLauncher を使用するかと聞かれると1つの実行可能 Jar にまとめてリリースした方がミスしなさそうなので自分は使用しない気がしますが、実際に本番でも利用されているものなのでしょうか?

  • Vue.js のプロジェクトで生成したファイルを Spring Boot の実行可能 Jar に含めようとするのは意外に大変でした。

    • まさかデフォルトでは build タスクが存在しないとは。。。 Base Plugin を使えばよいのですが、Gradle は Java のプロジェクトで使われることが多く Java のプロジェクトでは Java Plugin が使われるので Base Plugin の存在になかなか気づけませんでした。
    • build タスクを実行した時のサブプロジェクトの実行順がアルファベット順になるということも初めて知りました。今回 backend-app と frontend-app という名前にしたので、frontend-app → backend-app の順に build したいのにいろいろ設定しても backend-app → frontend-app の順が変わりませんでした。最終的には copyDistToStatic.dependsOn ":frontend-app:build" と記述することで解決しましたが、他にも解決策として書かれている記事をいくつか見つけて試したのですがなかなか変わらなかったんですよね。まだまだ Gradle を理解できていませんね。。。
  • 今更ながら draw.io のことを知りましたが、とても便利ですね。これで個人・商用問わず無料らしいとは驚きです。EIP の Shape も結構揃っているので Spring Integration のフローを記述するのにも使えそうです。

Gradle で Multi-project を作成する ( その15 )( vuejs+springboot編、frontend-app プロジェクトを作成する )

概要

記事一覧はこちらです。

Gradle で Multi-project を作成する ( その14 )( vuejs+springboot編、Multi-project のベースと backend-app プロジェクトを作成する ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • frontend を Vue.js で、backend を Spring Boot でアプリケーションを実装し、各アプリケーションをサブプロジェクトとする Gradle Multi-project のサンプルを作成します。
    • 今回は Vue.js のアプリケーション(frontend)を作成し、gradle build で実行可能 jar にまとめます。

参照したサイト・書籍

  1. Vue CLI
    https://cli.vuejs.org/

  2. Vuetify
    https://vuetifyjs.com/en/

  3. How to build a web app with Vue, Vuetify and Axios
    https://medium.com/javascript-in-plain-english/implement-movie-app-with-vue-vuetify-axios-open-movie-database-api-d12290318cf9

  4. axios、async/awaitを使ったHTTPリクエスト(Web APIを実行)
    https://qiita.com/shisama/items/61cdcc09dc69fd8d3127

  5. async関数においてtry/catchではなくawait/catchパターンを活用する
    https://qiita.com/akameco/items/cc73afcdb5ac5d0774bc

  6. Vue.jsでビューの変更がされないときに疑うこと+主な解決策方法
    https://cloudpack.media/41984

  7. Vue Test Utils
    https://vue-test-utils.vuejs.org/

  8. Vue.jsのテストでコンポーネントをいい感じにwrapする方法
    https://qiita.com/ykhirao/items/8e8a9547a693c677813c

  9. Vue CLI - Configuration Reference - devServer.proxy
    https://cli.vuejs.org/config/#devserver-proxy

  10. Gradle Docs - The Base Plugin
    https://docs.gradle.org/current/userguide/base_plugin.html

  11. node-gradle/gradle-node-plugin
    https://github.com/node-gradle/gradle-node-plugin

  12. node-gradle/gradle-node-plugin - Node Plugin
    https://github.com/node-gradle/gradle-node-plugin/blob/master/docs/node.md

目次

  1. Vue.js のアプリケーション(frontend)(frontend-app)を作成する
    1. Vue CLI をインストールする
    2. Vue CLI で frontend-app プロジェクトを作成する
    3. Vuetify をインストールする
    4. npm run buildnpm run lintnpm run test:unit 実行時に prettier で自動フォーマットするよう設定する
    5. WebAPI を呼び出して取得したデータを画面に表示する処理を実装する
    6. vue.config.js を作成して webpack-dev-server に proxy の設定を追加する
    7. 動作確認
  2. Gradle の build タスク実行時に npm run test:unitnpm run build が実行されるようにする
  3. Gradle の build タスク実行時に frontend-app の dist ディレクトリの下にあるファイルを backend-app の src/main/resources/static にコピーする
  4. settings.gradle に sample-cmdapp プロジェクトの include 文を追加する
  5. clean タスク実行 → Rebuild Project 実行 → build タスク実行を行う
  6. 実行可能 jar から Tomcat を起動して動作確認する
  7. 次回は。。。

手順

Vue.js のアプリケーション(frontend)(frontend-app)を作成する

Vue CLI をインストールする

以下のコマンドを実行して Vue CLI をインストールします。

  • npm install -g @vue/cli
  • npm install -g @vue/cli-service-global

f:id:ksby:20190506093206p:plain

以下の警告メッセージが出ていますが、TypeScript、GraphQL は使用しないので今回は何もしません。

  • ts-node@8.1.0 requires a peer of typescript@>=2.0 but none is installed.
  • apollo-tracing@0.5.2 requires a peer of graphql@0.10.x - 14.1.x but none is installed.

インストールされた Vue CLI のバージョンを vue --version コマンドで確認すると 3.7.0 でした。

f:id:ksby:20190506101848p:plain

Vue CLI で frontend-app プロジェクトを作成する

コマンドラインから vue create frontend-app を実行して frontend-app プロジェクトを作成します。

f:id:ksby:20190506093745p:plain ※「Please pick a preset:」では「Manually select features」を選択します。

f:id:ksby:20190506093932p:plain ※「Check the features needed for your project:」はデフォルトで「Babel」「Linter / Formatter」が選択済みで、「Unit Testing」を追加で選択します。

f:id:ksby:20190506094156p:plain ※「Pick a linter / formatter config:」では「ESLint + Prettier」を選択します。

f:id:ksby:20190506094643p:plain ※「Pick additional lint features:」はデフォルトで「Lint on save」が選択済みで、そのままにします。

f:id:ksby:20190506094804p:plain ※「Pick a unit testing solution:」では「Jest」を選択します。

f:id:ksby:20190506094922p:plain ※「Where do you prefer placing config for Babel, PostCSS, ESLint, etc.? (Use arrow keys)」では「In dedicated config files」を選択します。

f:id:ksby:20190506095230p:plain ※「Save this as a preset for future projects? (y/N)」は何も入力せずに Enter キーを押します。

インストールが実行されます。

f:id:ksby:20190506095625p:plain

インストール後 frontend-app ディレクトリに移動してから npm run serve コマンドを実行して、

f:id:ksby:20190506101444p:plain f:id:ksby:20190506101541p:plain

http://localhost:8080/ にアクセスすると画面が表示されました。

f:id:ksby:20190506101648p:plain

npm run build コマンドを実行すると、警告・エラーは出ずに終了します。

f:id:ksby:20190506103005p:plain

IntelliJ IDEA の Project Tool Window で frontend-app のディレクトリ構成を見てみると以下のようになっていました。尚、IntelliJ IDEA で最初に表示させる時には「Indexing…」のメッセージが表示されてしばらく時間がかかります(おそらく node_modules の下の大量のファイルを index するのに時間がかかっているためでしょう)。

f:id:ksby:20190506103059p:plain

Vuetify をインストールする

Quick start の記述に従い vue add vuetify コマンドを実行します。

f:id:ksby:20190506110432p:plain ※「Choose a preset: (Use arrow keys)」はデフォルトの「Default (recommended)」のままにします。

f:id:ksby:20190506110617p:plain

インストール直後の状態で npm run build コマンドを実行すると prettier のフォーマットと異なるために eslint が警告を大量に出すので、npm run build コマンド実行時に prettier で自動フォーマットして警告が出ないようにします。

npm run buildnpm run lintnpm run test:unit 実行時に prettier で自動フォーマットするよう設定する

npm install --save-dev npm-run-all コマンドを実行して npm-run-all をインストールします。

f:id:ksby:20190506133306p:plain

frontend-app/package.json"scripts": { ... } を以下のように変更します。

  "scripts": {
    "serve": "vue-cli-service serve",
    "build": "run-s prettier:format vue-cli-service:build",
    "lint": "run-s prettier:format vue-cli-service:lint",
    "test:unit": "run-s prettier:format vue-cli-service:test:unit",
    "vue-cli-service:build": "vue-cli-service build",
    "vue-cli-service:lint": "vue-cli-service lint",
    "vue-cli-service:test:unit": "vue-cli-service test:unit",
    "prettier:format": "prettier --write {src,tests}/**/*.{js,vue}"
  },
  • 以下の4行を追加します。今回初めて知りましたが、Paste した時に Paste した行や1つ上の行の末尾に "," がないと IntelliJ IDEA が自動で付けてくれますね。
    • "vue-cli-service:build": "vue-cli-service build"
    • "vue-cli-service:lint": "vue-cli-service lint"
    • "vue-cli-service:test:unit": "vue-cli-service test:unit"
    • "prettier:format": "prettier --write {src,tests}/**/*.{js,vue}"
  • "build": "vue-cli-service build""build": "run-s prettier:format vue-cli-service:build" に変更します。
  • "lint": "vue-cli-service lint""lint": "run-s prettier:format vue-cli-service:lint" に変更します。
  • "test:unit": "vue-cli-service test:unit""test:unit": "run-s prettier:format vue-cli-service:test:unit" に変更します。

以上で設定は完了です。再度 npm run build コマンドを実行してみると2件 warning が出ていますが Build complete. が表示されました。

f:id:ksby:20190506135038p:plain f:id:ksby:20190506135128p:plain

2件の warning はファイルサイズや lazy load に関するものでしたので、今回は解消せずに先に進みます。

また IntelliJ IDEA 上でも prettier でフォーマットできるように設定します。メインメニューから「File」-「Settings...」を選択して「Settings」ダイアログを表示させた後、画面右上に prettier と入力して Prettier Plugin の設定画面を開いてから、画面右側の「Prettier package」に frontend-app/node_modules/prettier の絶対パスを設定します。

f:id:ksby:20190506143006p:plain

WebAPI を呼び出して取得したデータを画面に表示する処理を実装する

WebAPI を呼び出すのに axios を使用するので npm install --save axios コマンドを実行してインストールします。

f:id:ksby:20190506142722p:plain

frontend-app/src/components/HelloWorld.vue のファイル名を CallSampleWebapi.Vue に変更します。

frontend-app/src/App.vue を以下のように変更します。

<template>
  <v-app>
    <v-content>
      <CallSampleWebapi />
    </v-content>
  </v-app>
</template>

<script>
import CallSampleWebapi from "./components/CallSampleWebapi";

export default {
  name: "App",
  components: {
    CallSampleWebapi: CallSampleWebapi
  },
  data() {
    return {
      //
    };
  }
};
</script>
  • <v-toolbar app>...</v-toolbar> を削除します。
  • HelloWorldCallSampleWebapi に一括置換します。

frontend-app/src/components/CallSampleWebapi.Vue を以下のように変更します。

<template>
  <v-container>
    <v-layout text-xs-center wrap>
      <v-flex xs12>
        <div>code: {{ code }}</div>
        <div>value: {{ value }}</div>
        <v-btn color="info" v-on:click="callSampleWebapi">クリック!</v-btn>
      </v-flex>
    </v-layout>
  </v-container>
</template>

<script>
import axios from "axios";

export default {
  data: function() {
    return {
      code: "(空)",
      value: "(空)"
    };
  },
  methods: {
    callSampleWebapi: async function() {
      try {
        const response = await axios.post("/webapi/sample");
        this.$set(this, "code", response.data.code);
        this.$set(this, "value", response.data.value);
      } catch (err) {
        alert(err);
      }
    }
  }
};
</script>

<style></style>

frontend-app/tests/unit/example.spec.js もファイル名を CallSampleWebapi.spec.js に変更した後、以下の内容に変更します。

import { mount } from "@vue/test-utils";
import Vue from "vue";
import Vuetify from "vuetify";
import CallSampleWebapi from "@/components/CallSampleWebapi.vue";

describe("CallSampleWebapi.vue test", () => {
  it("init render test", () => {
    Vue.use(Vuetify);
    const wrapper = mount(CallSampleWebapi, {});
    expect(wrapper.html()).toContain("<div>code: (空)</div>");
  });
});

vue.config.js を作成して webpack-dev-server に proxy の設定を追加する

npm run serve コマンドで起動した webpack-dev-server に http://localhost:8080/webapi/sample でアクセスしたら backend-app の http://localhost:8081/webapi/sample へリクエストを転送させるための設定を行います。

frontend-app の下に vue.config.js を新規作成した後、以下の内容を記述します。changeOrigin: true を記述すると backend-app 側で @RestController アノテーションを付与したクラスに @CrossOrigin アノテーションを付与して設定しなくてもアクセスできるようになります。

module.exports = {
  devServer: {
    proxy: {
      "^/webapi/sample": {
        target: "http://localhost:8081",
        changeOrigin: true
      }
    }
  }
};

動作確認

backend-app の Tomcat を develop profile で起動してから、

f:id:ksby:20190506202707p:plain

npm run serve コマンドを実行して frontend-app の webpack-dev-server を起動した後、

f:id:ksby:20190506203121p:plain

http://localhost:8080/ にアクセスすると以下の画面が表示されます。

f:id:ksby:20190506203230p:plain

「クリック!」ボタンをクリックすると backend-app の WebAPI を呼び出して取得した code, value の値が画面に表示されました。

f:id:ksby:20190506203339p:plain

Tomcat、webpack-dev-server を停止します。

npm run test:unit コマンドを実行するとテストも成功します。

f:id:ksby:20190506203722p:plain

npm run build コマンドも2件の warning は出たままですが、他の警告・エラーは出ずに Build complete. が表示されました。

f:id:ksby:20190506204058p:plain f:id:ksby:20190506204152p:plain

Gradle の build タスク実行時に npm run test:unitnpm run build が実行されるようにする

frontend-app ディレクトリの下に build.gradle を新規作成し、以下の内容を記述します。

plugins {
    id "base"
    id "com.github.node-gradle.node" version "1.3.0"
}

clean.delete "dist"
task npmTestUnit(type: NpmTask) {
    args = ["run", "test:unit", "2>&1"]
    execOverrides {
        it.standardOutput = new ByteArrayOutputStream()
    }
}
task npmBuild(type: NpmTask) {
    args = ["run", "build", "2>&1"]
    execOverrides {
        it.standardOutput = new ByteArrayOutputStream()
    }
}
npmBuild.dependsOn npmTestUnit
build.dependsOn npmBuild
  • id "base" を記述することで clean や build タスクが使えるようになります。
  • npm run ... コマンドの実行には gradle-node-plugin を使用します。
  • args に "2>&1" を指定して標準エラー出力を標準出力にリダイレクトし、execOverrides { it.standardOutput = new ByteArrayOutputStream() } を記述することで標準出力、標準エラー出力に何も出力されないようにします。

ちなみに標準出力、標準エラー出力の内容をファイルに出力したい場合には、以下のように記述すれば frontend-app/build/tests/stdout.txt に出力されます。

task npmTestUnit(type: NpmTask) {
    args = ["run", "test:unit", "2>&1"]

    def testsDir = "${projectDir}/build/tests"
    execOverrides {
//        it.standardOutput = new ByteArrayOutputStream()
        it.standardOutput = new FileOutputStream("${testsDir}/stdout.txt")
    }
    doFirst {
        mkdir testsDir
    }
}

Gradle の build タスク実行時に frontend-app の dist ディレクトリの下にあるファイルを backend-app の src/main/resources/static にコピーする

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

..........

dependencies {
    ..........
}

clean.delete fileTree("src/main/resources/static").include("**/*")
task copyDistToStatic(type: Copy) {
    from project(":frontend-app").file("dist")
    into "src/main/resources/static"
}
copyDistToStatic.dependsOn ":frontend-app:build"
processResources.dependsOn copyDistToStatic
  • clean.delete fileTree("src/main/resources/static").include("**/*")processResources.dependsOn copyDistToStatic の記述を追加します。

settings.gradle に frontend-app プロジェクトの include 文を追加する

gradle-multiprj-vuejs-springboot の settings.gradle に include 'frontend-app' を追加します。

rootProject.name = 'gradle-multiprj-vuejs-springboot'
include 'backend-app'
include 'frontend-app'

Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。Gradle Tool Window に frontend-app が表示されます。

f:id:ksby:20190507230434p:plain

clean タスク実行 → Rebuild Project 実行 → build タスク実行を行う

clean タスクを実行すると backend-app は src/main/resources/static の下がクリアされて、frontend-app は dist ディレクトリが削除されます。

f:id:ksby:20190507231149p:plain

build タスクを実行すると警告・エラーは出ずに BUILD SUCCESSFUL が表示されて、

f:id:ksby:20190507231420p:plain

frontend-app の dist ディレクトリの下のディレクトリ・ファイル一式が backend-app の src/main/resources/static の下にコピーされます。

f:id:ksby:20190507231706p:plain

実行可能 jar から Tomcat を起動して動作確認する

backend-app/build/libs の下に backend-app-1.0.0-RELEASE.jar が生成されていますので、

f:id:ksby:20190507235208p:plain

コマンドラインから java -Dspring.profiles.active=product -jar backend-app-1.0.0-RELEASE.jar を実行します。

f:id:ksby:20190507235401p:plain

http://localhost:8080/ にアクセスすると以下の画面が表示されて、

f:id:ksby:20190507235509p:plain

「クリック!」ボタンをクリックすると WebAPI を呼び出して取得したデータが画面に表示されました。

f:id:ksby:20190507235605p:plain

次回は。。。

frontend-app の下に作成した build.gradle のタスクを build タスク実行時に動かすための The Base Plugin を見つけるまでが意外に時間がかかりました。書いてある記事が少ないのか、なかなか見つけられなかったんですよね。。。

これでとりあえずやりたいことは全てやったので、最後に感想を書いて終わりにします。

履歴

2019/05/08
初版発行。

Gradle で Multi-project を作成する ( その14 )( vuejs+springboot編、Multi-project のベースと backend-app プロジェクトを作成する )

概要

記事一覧はこちらです。

Gradle で Multi-project を作成する ( その13 )( doma2lib+cmdapp+webapp編、PropertiesLauncher を利用して doma2-lib の jar ファイルを外部に出す ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • frontend を Vue.js で、backend を Spring Boot でアプリケーションを実装し、各アプリケーションをサブプロジェクトとする Gradle Multi-project のサンプルを作成します。
    • Vue CLI で作成した Vue.js のアプリケーションと Spring Boot のアプリケーションをそれぞれ別のディレクトリ(サブプロジェクト)で作成して、最後は必要なファイルを Spring Boot の実行可能 jar にまとめてリリース・実行する想定です。

参照したサイト・書籍

目次

  1. 方針
  2. gradle-multiprj-vuejs-springboot ディレクトリ作成+Gradle Wrapper コピー+gradlew init
  3. IntelliJ IDEA で gradle-multiprj-vuejs-springboot プロジェクトをオープンする
  4. Spring Boot ベースの Web アプリケーション(backend)(backend-app)を作成する
    1. IntelliJ IDEA で backend-app プロジェクトを作成する
    2. settings.gradle に backend-app プロジェクトの include 文を追加する
    3. backend-app プロジェクトの build.gradle を変更する
    4. Vue.js のアプリから呼び出す WebAPI を実装する
    5. 動作確認

手順

方針

gradle-multiprj-vuejs-springboot
├ backend-app    <-- Spring Boot ベースの Web アプリケーション(backend)の Project
└ frontend-app    <-- Vue.js のアプリケーション(frontend)の Project
  • Vue.js のアプリケーション(frontend)でボタンをクリックしたら Spring Boot ベースの Web アプリケーション(backend)の WebAPI を呼び出して、取得したデータを画面上に表示します。
  • Spring Boot ベースの Web アプリケーションの Project では develop と product の2つの Profile を作成・使用します。
  • Spring Boot ベースの Web アプリケーションの Project ではテスティングフレームワークは導入しません(今回はテストなし!)。
  • checkstyle, spotbugs, pmd, error-prone は今回は導入しません。
  • Spring Boot ベースの Web アプリケーション(backend)は develop profile では 8081番ポート、product profile では 8080番ポートを使用します。
  • Vue.js のアプリケーションは Vue CLI を使用してプロジェクトを作成します。
  • Vue.js のアプリケーションの画面は Vuetify を使用して作成します。
  • Vue.js のアプリケーション(frontend)では npm run serve 実行時には 8080番ポートを使用します。
  • Gradle の build タスクで以下の処理を行います。
    • Vue.js のアプリケーション(frontend)の npm run build を実行します。
    • Spring Boot ベースの Web アプリケーション(backend)の processResources タスクの前に src/main/resources/static の下に Vue.js のアプリケーション(frontend)のリリースファイル一式(/dist ディレクトリの下に出来るもの)をコピーします。
  • 開発環境及び本番環境どちらも http://localhost:8080/ でアクセスします。
  • 開発環境では npm run serve で起動した webpack-dev-server から http-proxy-middleware で backend-app の Tomcat へリクエストを転送します。
  • こんなイメージです。

f:id:ksby:20190505095922p:plain

gradle-multiprj-vuejs-springboot ディレクトリ作成+Gradle Wrapper コピー+gradlew init

ksby/ksbysample-boot-miscellaneous の repository を checkout している D:\project-springboot\ksbysample-boot-miscellaneous の下に gradle-multiprj-vuejs-springboot ディレクトリを作成します。

f:id:ksby:20190505100848p:plain

gradle-multiprj-doma2lib-cmdwebapp プロジェクトから Gradle Wrapper のファイルをコピーします。

f:id:ksby:20190505101245p:plain

gradlew init コマンドを実行します。選択肢は 1: basic1: groovy を選択し、Project name は何も入力せずに Enter キーを押します。

f:id:ksby:20190505101506p:plain f:id:ksby:20190505101558p:plain

IntelliJ IDEA で gradle-multiprj-vuejs-springboot プロジェクトをオープンする

gradle-multiprj-vuejs-springboot プロジェクトをオープンします。

f:id:ksby:20190505101832p:plain

f:id:ksby:20190505102147p:plainf:id:ksby:20190505102226p:plain

Spring Boot ベースの Web アプリケーション(backend)(backend-app)を作成する

IntelliJ IDEA で backend-app プロジェクトを作成する

IntelliJ IDEA から Spring Initializr を利用して backend-app プロジェクトを作成します。

f:id:ksby:20190505152830p:plainf:id:ksby:20190505153013p:plain
f:id:ksby:20190505153106p:plainf:id:ksby:20190505153139p:plain
f:id:ksby:20190505153232p:plainf:id:ksby:20190505153301p:plain

f:id:ksby:20190505153401p:plain

作成後 IntelliJ IDEA のウィンドウが開きますが、何もせずに閉じます。

src ディレクトリと build.gradle だけ残して、それ以外のディレクトリ・ファイルは全て削除します。

また backend-app/src/test/java/ksbysample/app/backendapp/BackendAppApplicationTests.java.java も削除し、backend-app/src/test/java/ の下はクリアします。

settings.gradle に backend-app プロジェクトの include 文を追加する

gradle-multiprj-vuejs-springboot の settings.gradle に include 'backend-app' を追加します。

rootProject.name = 'gradle-multiprj-vuejs-springboot'
include 'backend-app'

backend-app プロジェクトの build.gradle を変更する

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

plugins {
    id 'org.springframework.boot' version '2.1.4.RELEASE'
    id 'java'
    id "idea"
}

apply plugin: 'io.spring.dependency-management'

group = 'ksby.ksbysample-boot-miscellaneous.gradle-multiprj-vuejs-springboot'
version = '1.0.0-RELEASE'

sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11

idea {
    module {
        inheritOutputDirs = false
        outputDir = file("$buildDir/classes/main/")
    }
}

repositories {
    mavenCentral()
}

dependencyManagement {
    imports {
        mavenBom(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES)
    }
}

dependencies {
    def lombokVersion = "1.18.6"

    implementation 'org.springframework.boot:spring-boot-starter-web'
    runtimeOnly 'org.springframework.boot:spring-boot-devtools'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'

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

変更後、Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。Gradle Tool Window に backend-app が表示されます。

f:id:ksby:20190505155232p:plain

clean タスク実行 → Rebuild Project 実行 → build タスク実行を行い、警告・エラーが出ずに BUILD SUCCESSFUL が出力されることを確認します。

f:id:ksby:20190505155458p:plain

Vue.js のアプリから呼び出す WebAPI を実装する

POST メソッドで /webapi/sample の URL にアクセスしたら以下の JSON を返す WebAPI を実装します。

{
    "code": "123",
    "value": "sample data"
}

backend-app/src/main/java/ksbysample/app/backendapp の下に SampleResponse.java を新規作成し、以下の内容を記述します。

package ksbysample.app.backendapp;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class SampleResponse {

    private String code;

    private String value;

}

backend-app/src/main/java/ksbysample/app/backendapp の下に SampleController.java を新規作成し、以下の内容を記述します。今回は ResponseEntity を使ってみます。

package ksbysample.app.backendapp;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/webapi/sample")
public class SampleController {

    @PostMapping
    public ResponseEntity<SampleResponse> index() {
        SampleResponse sampleResponse =
                SampleResponse.builder()
                        .code("123")
                        .value("sample data")
                        .build();
        return new ResponseEntity<>(sampleResponse, HttpStatus.OK);
    }

}

src/main/resources の下に application-develop.properties を新規作成し、以下の内容を記述します。

server.port=8081

server.port のデフォルトが 8080 なので application-product.properties は作成しません。

動作確認

最初に clean タスク実行 → Rebuild Project 実行 → build タスク実行を行い、BUILD SUCCESSFUL が出力されることを確認します。

f:id:ksby:20190506084133p:plain

次に develop profile で Tomcat を起動した後、

f:id:ksby:20190506084339p:plain

コマンドラインから curl http://localhost:8081/webapi/sample -i -X POST を実行すると JSON データが返ってきました。

f:id:ksby:20190506084651p:plain

Tomcat を停止します。

今度は product profile で実行可能 jar で Tomcat を起動した後、

f:id:ksby:20190506084957p:plain

コマンドラインから curl http://localhost:8080/webapi/sample -i -X POST(ポート番号を 8081 → 8080 に変更しています) を実行するとこちらも問題なく JSON データが返ってきました。

f:id:ksby:20190506085134p:plain

履歴

2019/05/06
初版発行。

Gradle で Multi-project を作成する ( その13 )( doma2lib+cmdapp+webapp編、PropertiesLauncher を利用して doma2-lib の jar ファイルを外部に出す )

概要

記事一覧はこちらです。

Gradle で Multi-project を作成する ( その12 )( doma2lib+cmdapp+webapp編、sample-webapp プロジェクトを作成する ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • Doma 2 の Entity、Dao を提供するライブラリ+Spring Boot ベースのコマンドラインアプリケーション+Spring Boot ベースの Web アプリケーションの Multi-project を作成します。
    • doma2lib+cmdapp+webapp編のここまでの記事では実行可能 Jar ファイルを起動するのに JarLauncher を利用する方式でしたので sample-cmdapp-1.0.0-RELEASE.jar、sample-webapp-1.0.0-RELEASE.jar の中に doma2-lib-1.0.0-RELEASE.jar が入っていましたが、PropertiesLauncher を利用する方式に変更して doma2-lib-1.0.0-RELEASE.jar を外部に出すようにしてみます。

参照したサイト・書籍

  1. E.3 Launching Executable Jars
    https://docs.spring.io/spring-boot/docs/current/reference/html/executable-jar.html#executable-jar-launching

  2. Spring Boot 2 and external libs with the PropertiesLauncher
    https://medium.com/saas-startup-factory/spring-boot-2-and-external-libs-with-the-propertieslauncher-fc49d2d93636

  3. Gradle Docs 5.4.1 - Copy
    https://docs.gradle.org/current/dsl/org.gradle.api.tasks.Copy.html

目次

  1. gradle-multiprj-doma2lib-cmdwebapp の build.gradle を変更する
  2. sample-cmdapp、sample-webapp の build.gradle を変更する
  3. clean タスク実行 → Rebuild Project 実行 → build タスク実行を行う
  4. 動作確認
  5. sample-webapp を IntelliJ IDEA の Run Dashboard から起動するとどうなるのか?
  6. 次回は。。。

手順

gradle-multiprj-doma2lib-cmdwebapp の build.gradle を変更する

PropertiesLauncher を利用するのに必要な設定と、jar ファイルを gradle-multiprj-doma2lib-cmdwebapp プロジェクトのルートディレクトリ直下の libs ディレクトリ(新規に作成します)にコピーするための task を gradle-multiprj-doma2lib-cmdwebapp の build.gradle に記述します。

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

..........

subprojects {
    ..........

    // サブプロジェクトの build/libs の下に生成された jar ファイルを
    // プロジェクトのルートディレクトリ直下の libs ディレクトリにコピーする
    task copyJarToLibsDir(type: Copy) {
        from "build/libs"
        include "*.jar"
        into "${rootDir}/libs"
    }
    copyJarToLibsDir.dependsOn test
    check.dependsOn copyJarToLibsDir
}

configure(subprojects.findAll { it.name ==~ /^(doma2-lib|sample-cmdapp|sample-webapp)$/ }) {
    ..........
}

configure(subprojects.findAll { it.name ==~ /^(sample-cmdapp|sample-webapp)$/ }) {
    bootJar {
        manifest {
            attributes 'Main-Class': 'org.springframework.boot.loader.PropertiesLauncher'
        }
    }
}
  • subproject block 内に task copyJarToLibsDir(type: Copy) { ... } を追加します。また test タスクの後に実行するために copyJarToLibsDir.dependsOn testcheck.dependsOn copyJarToLibsDir の2行を追加します。
  • configure は複数記述できるようなので、sample-cmdapp、sample-webapp だけに適用する設定を記述する configure(subprojects.findAll { it.name ==~ /^(sample-cmdapp|sample-webapp)$/ }) { ... } を追加します。この中に PropertiesLauncher を利用するのに必要な設定を記述します。

またプロジェクトのルートディレクトリ直下に libs ディレクトリを作成しておきます。

sample-cmdapp、sample-webapp の build.gradle を変更する

sample-cmdapp、sample-webapp の build.gradle では doma2-lib への依存関係を implementation で記述しており、このままでは jar ファイル内に doma2-lib-1.0.0-RELEASE.jar が含まれてしまうので、implementation project(":doma2-lib")compileOnly project(":doma2-lib") に変更します。

■sample-cmdapp の build.gradle

dependencies {
    implementation("org.springframework.boot:spring-boot-starter")

    compileOnly project(":doma2-lib")
    implementation("com.univocity:univocity-parsers:2.8.1")
    implementation("args4j:args4j:2.33")
    implementation("com.github.rozidan:modelmapper-spring-boot-starter:1.0.0")
}

■sample-webapp の build.gradle

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-web")
    runtimeOnly("org.springframework.boot:spring-boot-devtools")

    compileOnly project(":doma2-lib")
}

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

clean タスク実行 → Rebuild Project 実行 → build タスク実行を行う

必要な設定は以上で完了です。clean タスク実行 → Rebuild Project 実行 → build タスク実行を行い、jar ファイルを生成し直して libs ディレクトリに集めます。

エラーメッセージは出ずに BUILD SUCCESSFUL が出力されています。各サブプロジェクトの test タスクの後では copyJarToLibsDir タスクが実行されて、

f:id:ksby:20190501224807p:plain f:id:ksby:20190501224943p:plain

libs ディレクトリの下に doma2-lib-1.0.0-RELEASE.jar、sample-cmdapp-1.0.0-RELEASE.jar、sample-webapp-1.0.0-RELEASE.jar がコピーされています。

f:id:ksby:20190501225129p:plain

sample-cmdapp-1.0.0-RELEASE.jar を表示させてみると lib ディレクトリの下には doma2-lib-1.0.0-RELEASE.jar は入っておらず、

f:id:ksby:20190501225355p:plain

sample-webapp-1.0.0-RELEASE.jar の方にも同様に入っていませんでした。

f:id:ksby:20190501225527p:plain

動作確認

employee テーブルのデータは以下の状況です。

f:id:ksby:20190501230241p:plain

PropertiesLauncher を利用する方式の場合、外部に配置した jar ファイルがある場所を loader.path で指定します。今回はコマンドラインからアプリを実行する時に libs ディレクトリに移動した後 -Dloader.path=. を追加したコマンドを使用するようにします。

コマンドラインから java -Dspring.profiles.active=product -Dloader.path=. -Dbatch.execute=EmployeeDataCsvToDbLoader -jar sample-cmdapp-1.0.0-RELEASE.jar -csvfile=D:\project-springboot\ksbysample-boot-miscellaneous\gradle-multiprj-doma2lib-cmdwebapp\sample-cmdapp\src\test\resources\employee.csv コマンドを実行するとエラーは出ずに終了し、

f:id:ksby:20190501230514p:plain f:id:ksby:20190501230635p:plain

employee テーブルにもデータが登録されていました。

f:id:ksby:20190501230750p:plain

java -Dspring.profiles.active=product -Dloader.path=. -jar sample-webapp-1.0.0-RELEASE.jar で sample-webapp を起動した後、

f:id:ksby:20190501231034p:plain f:id:ksby:20190501231141p:plain

http://localhost:8080/sample にアクセスすると employee テーブルに登録されているデータが表示されました。

f:id:ksby:20190501231218p:plain

動作は問題なさそうです。

また libs ディレクトリから doma2-lib-1.0.0-RELEASE.jar を削除した後、sample-cmdapp を実行しようとすると java.lang.NoClassDefFoundError: ksbysample/lib/doma2lib/dao/EmployeeDao が発生してエラーで終了しました。

f:id:ksby:20190501231805p:plain

sample-webapp を起動しようとすると java.lang.ClassNotFoundException: ksbysample.lib.doma2lib.entity.Employee が発生して、こちらもエラーで終了しました。

f:id:ksby:20190501232323p:plain (.....途中省略.....) f:id:ksby:20190501232420p:plain

sample-webapp を IntelliJ IDEA の Run Dashboard から起動するとどうなるのか?

JarLauncher か PropertiesLauncher かは実行可能 Jar で起動する時の話ですので、Run Dashboard から起動する時には関係がありません。

Configuration でも Main class には org.springframework.boot.loader.PropertiesLauncher ではなく ksbysample.app.samplewebapp.SampleWebappApplication を指定しているので、

f:id:ksby:20190501233224p:plain

設定を変更しなくても起動します。

f:id:ksby:20190501233500p:plain

次回は。。。

Doma 2 の Dao インターフェース、Entity クラスを別の jar ファイルにしようとすると意外に大変ですね。今回は知らないことがいろいろありました。

Gradle で Multi-project を作成する ( その1 )( 概要 ) に記載していたパターンは一通り作成しましたが、サブプロジェクトの1つが Javascript のプロジェクト(Vue.js かな?)+1つは Spring Boot ベースのプロジェクトのパターンを作成してみたいと思ったので、もう1パターンだけ作成してみます。

履歴

2019/05/01
初版発行。