かんがるーさんの日記

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

Spring Boot 1.5.x の Web アプリを 2.0.x へバージョンアップする ( その5 )( checkstyle を 7.8.1 → 8.12 に、PMD を 5.8.1 → 6.7.0 にバージョンアップする )

概要

記事一覧はこちらです。

Spring Boot 1.5.x の Web アプリを 2.0.x へバージョンアップする ( その4 )( AbstractJsonpResponseBodyAdvice を削除し、失敗しているテストを成功させる ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • checkstyle を 7.8.1 → 8.12 にバージョンアップします。
    • PMD を 5.8.1 → 6.7.0 にバージョンアップします。

参照したサイト・書籍

目次

  1. checkstyle を 7.8.1 → 8.12 にバージョンアップする
  2. PMD を 5.8.1 → 6.7.0 にバージョンアップする
    1. build.gradle を変更して build してみる
    2. The constant name 'springProfiles' doesn't match '[A-Z][A-Z_0-9]*'
    3. Do not use hard coded encryption keys
    4. Comment is too large: Too many lines
    5. Avoid throwing raw exception types.
    6. The constant name 'logger' doesn't match '[A-Z][A-Z_0-9]*'
    7. StringBuffer (or StringBuilder).append is called consecutively without reusing the target variable.
    8. This class has too many methods, consider refactoring it.
    9. Avoid unnecessary constructors - the compiler will generate these for you
    10. It is a good practice to call super() in a constructor
    11. A method/constructor should not explicitly throw java.lang.Exception
    12. The constant name 'serialVersionUID' doesn't match '[A-Z][A-Z_0-9]*'
    13. Useless parentheses.
    14. Document empty constructor
    15. There is log block not surrounded by if
    16. Avoid short class names like ...
    17. Avoid using Literals in Conditional Statements
    18. Avoid instantiating new objects inside loops
    19. Prefer StringBuilder (non-synchronized) or StringBuffer (synchronized) over += for concatenating strings
    20. Assigning an Object to null is a code smell. Consider refactoring.
    21. Avoid using redundant field initializer for 'errcode'
    22. Avoid catching generic exceptions such as NullPointerException, RuntimeException, Exception in try-catch block
    23. 最後に

手順

checkstyle を 7.8.1 → 8.12 にバージョンアップする

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

checkstyle {
    configFile = file("${rootProject.projectDir}/config/checkstyle/google_checks.xml")
    toolVersion = "8.12"
    sourceSets = [project.sourceSets.main]
}
  • checkstyle タスクで toolVersion = "7.8.1"toolVersion = "8.12" に変更します。

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

clean タスク実行 → Rebuild Project 実行 → build タスクを実行すると、checkstyleMain タスクで失敗しました。

f:id:ksby:20180917093557p:plain

コマンドラインから gradlew --stacktrace --debug build > gradle-debug.log 2>&1 コマンドを実行してログをファイルに出力した後、gradle-debug.log を IntelliJ IDEA のメインメニューの「Tools」-「Tail File in Console...」で開きます。

f:id:ksby:20180917094603p:plain

Property 'maxLineLength' in module LeftCurly does not exist, please check the documentation というエラーが出ていました。Spring Boot + npm + Geb で入力フォームを作ってテストする ( その28 )( Spring Boot を 1.5.4 → 1.5.7 へ、error-prone を 2.0.15 → 2.1.1 へバージョンアップする ) で書きましたが、LeftCurly module から maxLineLength プロパティがなくなったので削除します。

config/checkstyle/google_checks.xml を以下のように変更します。

        <module name="NeedBraces"/>
        <module name="LeftCurly"/>
        <module name="RightCurly">
            <property name="id" value="RightCurlySame"/>
            <property name="tokens" value="LITERAL_TRY, LITERAL_CATCH, LITERAL_FINALLY, LITERAL_IF, LITERAL_ELSE, LITERAL_DO"/>
        </module>
  • LeftCurly module に記述していた <property name="maxLineLength" value="100"/> を削除して、<module name="LeftCurly">...</module><module name="LeftCurly"/> に変更します。

再度 clean タスク実行 → Rebuild Project 実行 → build タスクを実行すると、今度は BUILD SUCCESSFUL が表示されました。

f:id:ksby:20180917095641p:plain

Checkstyle plugin の設定も 8.12 に変更しておきます。

f:id:ksby:20180917095842p:plain

PMD を 5.8.1 → 6.7.0 にバージョンアップする

以下の記事をベースに進めます。

設定ファイルも以下のファイルをコピーします。

build.gradle を変更して build してみる

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

pmd {
    toolVersion = "5.8.1"
    sourceSets = [project.sourceSets.main]
    ignoreFailures = true
    consoleOutput = true
    ruleSetFiles = rootProject.files("/config/pmd/pmd-project-rulesets.xml")
    ruleSets = []
}
  • toolVersion = "5.8.1"toolVersion = "6.7.0" に変更します。

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

pmd-project-rulesets.xml をダウンロードして、config/pmd/pmd-project-rulesets.xml にコピーします。

clean タスク実行 → Rebuild Project 実行 → build タスクを実行しますが、警告が大量に出力されました。1つずつ解消していきます。

f:id:ksby:20180917133714p:plain

The constant name 'springProfiles' doesn't match '[A-Z][A-Z_0-9]*'

定数名を 英大文字+スネークケースにしていなかったので、警告が出ていました。警告に従って springProfilesSPRING_PROFILES に変更します。

Do not use hard coded encryption keys

SecretKeySpec クラスのコンストラクタの第1引数に渡すキー文字列をクラス内に定数として定義していたので、外部に定義するよう警告が出ていました。このサンプルではこのままにしますので、クラスに @SuppressWarnings({"PMD.HardCodedCryptoKey"}) を付けて警告が出ないようにします。

Comment is too large: Too many lines

コメントの行数が多いという警告なのですが、警告が出たところを見ると以下の内容でした。

f:id:ksby:20180917141407p:plain

この警告は不要なので削除します。config/pmd/pmd-project-rulesets.xml では一旦 exclude した後、<rule ref="category/java/documentation.xml/CommentSize">...</rule> で定義し直していたのですが、<rule ref="category/java/documentation.xml/CommentSize">...</rule> を削除して exclude するだけにします。

Avoid throwing raw exception types.

適切な Exception クラスを定義せずに throw new RuntimeException(e); と RuntimeException を throw していたので警告が出ていました。このサンプルでは config/pmd/pmd-project-rulesets.xml を以下のように変更して、警告が出ないようにします。

    <rule ref="category/java/design.xml">
        <exclude name="AvoidThrowingRawExceptionTypes"/>
        <exclude name="CyclomaticComplexity"/>
        <exclude name="DataClass"/>
        <exclude name="LawOfDemeter"/>
        <exclude name="LoosePackageCoupling"/>
        <exclude name="NcssCount"/>
        <exclude name="UseObjectForClearerAPI"/>
        <exclude name="UseUtilityClass"/>
    </rule>
  • <exclude name="AvoidThrowingRawExceptionTypes"/> を追加します。

The constant name 'logger' doesn't match '[A-Z][A-Z_0-9]*'

logger を private static final Logger logger = LoggerFactory.getLogger(ControllerAndEventNameLogger.class); と定義していたので、定数なのに英大文字+スネークケースでないと警告が出ていました。でも、変数は logger のままにしたいので static を削除すると、今度は The Logger variable declaration does not contain the static and final modifiers という警告が出ます。

今回は static を削除して private static final Logger logger = ...private final Logger logger = ... に変更し、config/pmd/pmd-project-rulesets.xml を以下のように変更します。

    <rule ref="category/java/errorprone.xml">
        <exclude name="BeanMembersShouldSerialize"/>
        <exclude name="DataflowAnomalyAnalysis"/>
        <exclude name="LoggerIsNotStaticFinal"/>
        <exclude name="MissingStaticMethodInNonInstantiatableClass"/>
    </rule>
  • <exclude name="LoggerIsNotStaticFinal"/> を追加します。

StringBuffer (or StringBuilder).append is called consecutively without reusing the target variable.

1行ずつ .append(...) を呼び出していたので警告が出ていました。

f:id:ksby:20180917230017p:plain

.append(...) を連続で呼び出すようにします。

This class has too many methods, consider refactoring it.

メソッド数が多いので警告が出ていました。この警告は不要なので、config/pmd/pmd-project-rulesets.xml を以下のように変更します。

    <rule ref="category/java/design.xml">
        <exclude name="AvoidThrowingRawExceptionTypes"/>
        <exclude name="CyclomaticComplexity"/>
        <exclude name="DataClass"/>
        <exclude name="LawOfDemeter"/>
        <exclude name="LoosePackageCoupling"/>
        <exclude name="NcssCount"/>
        <exclude name="TooManyMethods"/>
        <exclude name="UseObjectForClearerAPI"/>
        <exclude name="UseUtilityClass"/>
    </rule>
  • <exclude name="TooManyMethods"/> を追加します。

Avoid unnecessary constructors - the compiler will generate these for you

以下のように空の public コンストラクタを定義していたら不要との警告でした。コンストラクタの定義を削除します。

f:id:ksby:20180917233413p:plain

It is a good practice to call super() in a constructor

継承クラスのコンストラクタで super(); を呼び出していないという警告でした。super(); の呼び出しを追加します。

A method/constructor should not explicitly throw java.lang.Exception

メソッドに throws Exception を付けているとこの警告が出ていました。メソッドに付けている throws Exception を削除し、もしメソッド内から呼び出しているメソッドに throws Exception が付いていて削除できない場合には、メソッドに @SuppressWarnings("PMD.SignatureDeclareThrowsException") を付けて警告が出ないようにします。

The constant name 'serialVersionUID' doesn't match '[A-Z][A-Z_0-9]*'

private static final long serialVersionUID = ...static final が付いているので定数と判断されたが、英大文字/数字+スネークケースでないので警告が出ていました。serialVersionUID はこういう宣言だと思うので警告出さなくてもいいと思うのですが。。。 @SuppressWarnings("PMD.FieldNamingConventions") を付けて警告が出ないようにします。

Useless parentheses.

不要なカッコが書かれているという警告でした。この警告は不要なので、config/pmd/pmd-project-rulesets.xml を以下のように変更します。

    <rule ref="category/java/codestyle.xml">
        <exclude name="AtLeastOneConstructor"/>
        <exclude name="ClassNamingConventions"/>
        <exclude name="CommentDefaultAccessModifier"/>
        <exclude name="DefaultPackage"/>
        <exclude name="LongVariable"/>
        <exclude name="LocalVariableCouldBeFinal"/>
        <exclude name="MethodArgumentCouldBeFinal"/>
        <exclude name="OnlyOneReturn"/>
        <exclude name="ShortVariable"/>
        <exclude name="UnnecessaryAnnotationValueElement"/>
        <exclude name="UselessParentheses"/>
        <exclude name="VariableNamingConventions"/>
    </rule>
  • <exclude name="UselessParentheses"/> を追加します。

Document empty constructor

コンストラクタをオーバーロードしている時に、中身が空でコメントも書かれていないものがあると出る警告でした。// This constructor is intentionally empty. Nothing special is needed here. というコメントを記述して警告が出ないようにします。

There is log block not surrounded by if

logger.info("★★★ リトライ回数 = " + context.getRetryCount()); のようにlogger で変数を出力する時に {} を使わずに + で結合しているために警告が出ていました。logger.info("★★★ リトライ回数 = {}", context.getRetryCount()); という書き方に変更します。

Avoid short class names like ...

クラス名が短い(デフォルトでは5文字以内)と出る警告でした。この警告は不要なので、config/pmd/pmd-project-rulesets.xml を以下のように変更します。

    <rule ref="category/java/codestyle.xml">
        <exclude name="AtLeastOneConstructor"/>
        <exclude name="ClassNamingConventions"/>
        <exclude name="CommentDefaultAccessModifier"/>
        <exclude name="DefaultPackage"/>
        <exclude name="LongVariable"/>
        <exclude name="LocalVariableCouldBeFinal"/>
        <exclude name="MethodArgumentCouldBeFinal"/>
        <exclude name="OnlyOneReturn"/>
        <exclude name="ShortClassName"/>
        <exclude name="ShortVariable"/>
        <exclude name="UnnecessaryAnnotationValueElement"/>
        <exclude name="UselessParentheses"/>
        <exclude name="VariableNamingConventions"/>
    </rule>
  • <exclude name="ShortClassName"/> を追加します。

Avoid using Literals in Conditional Statements

メソッドの処理内に数値リテラルを直接記述していたので警告が出ていました。定数を定義して、数値リテラルと置き換えます。

Avoid instantiating new objects inside loops

ループ処理内で new でオブジェクトを生成していたので警告が出ていました。@SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") を付けるか、ループの外で生成したオブジェクトをループ内で使い回すように変更します。

Prefer StringBuilder (non-synchronized) or StringBuffer (synchronized) over += for concatenating strings

+= を使用しているところを StringBuilder か StringBuffer に置き換えるように出た警告でした。StringBuilder を使用するよう変更します。

Assigning an Object to null is a code smell. Consider refactoring.

変数宣言時以外の場所で変数に null をセットしていると出る警告でした。直接 null をセットしないよう実装を変えるようにします。

Avoid using redundant field initializer for 'errcode'

int の変数を宣言した時に 0 をセットしていたのですが、default の初期値なので警告が出ていました。セットしないようにします。

Avoid catching generic exceptions such as NullPointerException, RuntimeException, Exception in try-catch block

catch (Exception e) { ... } を記述していると出る警告でした。使用している外部ライブラリが Exception を throw するため変更も削除もできないので、`` を付けて警告が出ないようにします。

最後に

これで全ての警告に対応しました。clean タスク実行 → Rebuild Project 実行 → build タスクを実行すると BUILD SUCCESSFUL の文字が出力されます。

f:id:ksby:20180921004029p:plain

config/pmd/pmd-project-rulesets.xml は以下のようになりました。

<?xml version="1.0" encoding="UTF-8"?>
<ruleset name="mybraces"
         xmlns="http://pmd.sourceforge.net/ruleset/2.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://pmd.sourceforge.net/ruleset/2.0.0 http://pmd.sourceforge.net/ruleset_2_0_0.xsd">
    <description>project rulesets</description>

    <!--
        rulesets の種類・説明は 以下の URL 参照
        https://github.com/pmd/pmd/tree/master/pmd-java/src/main/resources/category/java
        https://github.com/pmd/pmd/tree/master/pmd-java/src/main/resources/rulesets/java
        https://pmd.github.io/pmd-6.7.0/pmd_rules_java.html
        ※"pmd-6.7.0" の部分は適用しているバージョンに変更すること。
    -->
    <rule ref="category/java/bestpractices.xml"/>
    <rule ref="category/java/codestyle.xml">
        <exclude name="AtLeastOneConstructor"/>
        <exclude name="ClassNamingConventions"/>
        <exclude name="CommentDefaultAccessModifier"/>
        <exclude name="DefaultPackage"/>
        <exclude name="LocalVariableCouldBeFinal"/>
        <exclude name="LongVariable"/>
        <exclude name="MethodArgumentCouldBeFinal"/>
        <exclude name="OnlyOneReturn"/>
        <exclude name="ShortClassName"/>
        <exclude name="ShortVariable"/>
        <exclude name="UnnecessaryAnnotationValueElement"/>
        <exclude name="UselessParentheses"/>
        <exclude name="VariableNamingConventions"/>
    </rule>
    <rule ref="category/java/design.xml">
        <exclude name="AvoidThrowingRawExceptionTypes"/>
        <exclude name="CyclomaticComplexity"/>
        <exclude name="DataClass"/>
        <exclude name="LawOfDemeter"/>
        <exclude name="LoosePackageCoupling"/>
        <exclude name="NcssCount"/>
        <exclude name="TooManyMethods"/>
        <exclude name="UseObjectForClearerAPI"/>
        <exclude name="UseUtilityClass"/>
    </rule>
    <rule ref="category/java/documentation.xml">
        <!-- CommentRequired はここでは exclude し、下で別途定義する -->
        <exclude name="CommentRequired"/>
        <exclude name="CommentSize"/>
        <exclude name="UncommentedEmptyMethodBody"/>
    </rule>
    <rule ref="category/java/documentation.xml/CommentRequired">
        <properties>
            <property name="fieldCommentRequirement" value="Ignored"/>
            <property name="enumCommentRequirement" value="Ignored"/>
        </properties>
    </rule>
    <rule ref="category/java/errorprone.xml">
        <exclude name="BeanMembersShouldSerialize"/>
        <exclude name="DataflowAnomalyAnalysis"/>
        <exclude name="LoggerIsNotStaticFinal"/>
        <exclude name="MissingStaticMethodInNonInstantiatableClass"/>
    </rule>
    <rule ref="category/java/multithreading.xml">
        <exclude name="UseConcurrentHashMap"/>
    </rule>
    <rule ref="category/java/performance.xml"/>
    <rule ref="category/java/security.xml"/>
</ruleset>

履歴

2018/09/21
初版発行。

Spring Boot + Spring Integration でいろいろ試してみる ( その28 )( Spring Cloud for AWS で S3 へファイルをアップロード・ダウンロードする )

概要

記事一覧 はこちらです。
今回作成したソースの GitHub レポジトリ はこちらです。

Spring Boot + Spring Integration でいろいろ試してみる ( その27 )( Spring Integration Extension for AWS で S3 へファイルをアップロード・ダウンロードする ) からの続きです。

今回は Spring Cloud for Amazon Web Services のサンプルを作成します。仕様や、S3 Bucket とアップロード・ダウンロード用の IAM ユーザは前回と同じにして、実装だけ変えます。

参照したサイト・書籍

目次

  1. ksbysample-eipapp-cloudaws プロジェクトを作成する
  2. upload ディレクトリ → S3 へアップロードする処理を実装する
  3. S3 → download ディレクトリへダウンロード → upload ディレクトリへ移動する処理を実装する
  4. 動作確認
  5. S3 Bucket とアップロード・ダウンロード用の IAM ユーザを削除する
  6. Spring Integration Extension for AWS と Spring Cloud for AWS を使用してみた感想

手順

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

Spring Initializr でプロジェクトの雛形を作成します。

f:id:ksby:20180912011527p:plain f:id:ksby:20180912011619p:plain f:id:ksby:20180912011849p:plain f:id:ksby:20180912011947p:plain

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

buildscript {
    ext {
        springBootVersion = '2.0.4.RELEASE'
    }
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
    }
}

apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'

group = 'ksbysample'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = 1.8

repositories {
    mavenCentral()
}

dependencyManagement {
    imports {
        mavenBom org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES
        mavenBom 'org.springframework.cloud:spring-cloud-aws:2.0.0.RELEASE'
    }
}

dependencies {
    def lombokVersion = "1.18.2"

    implementation('org.springframework.boot:spring-boot-starter-integration')
    implementation('org.springframework.cloud:spring-cloud-aws-context')
    implementation('org.springframework.integration:spring-integration-file')
    testImplementation('org.springframework.boot:spring-boot-starter-test')

    // for lombok
    annotationProcessor("org.projectlombok:lombok:${lombokVersion}")
    compileOnly("org.projectlombok:lombok:${lombokVersion}")
}
  • dependencyManagement block を追加します。
  • dependencies block に以下の2行を追加します。
    • implementation('org.springframework.cloud:spring-cloud-aws-context')
    • implementation('org.springframework.integration:spring-integration-file')
  • lombok@Slf4j アノテーションを使いたいので、dependencies block に以下の3行を追加します。
    • def lombokVersion = "1.18.2"
    • annotationProcessor("org.projectlombok:lombok:${lombokVersion}")
    • compileOnly("org.projectlombok:lombok:${lombokVersion}")

メインクラス名を KsbysampleEipappCloudawsApplication → Application に変更した後、clean タスク実行 → Rebuild Project 実行 → build タスクを実行して "BUILD SUCCESSFUL" のメッセージが出力されることを確認します。

※最初 build.gradle の dependencies block には Spring Cloud for Amazon Web Services に記載されている implementation('org.springframework.cloud:spring-cloud-starter-aws') を記載したのですが、テストを実行した時に java.lang.IllegalStateException: There is no EC2 meta data available, because the application is not running in the EC2 environment. Region detection is only possible if the application is running on a EC2 instance というエラーが出たので implementation('org.springframework.cloud:spring-cloud-aws-context') に変更しました。

upload ディレクトリ → S3 へアップロードする処理を実装する

src/main/java/ksbysample/eipapp/cloudaws の下に flow パッケージを作成した後、その下に FlowConfig.java を新規作成して、以下の内容を記述します。

@Slf4j
@Configuration
public class FlowConfig {

    private static final String EIPAPP_ROOT_DIR_PATH = "D:/eipapp/ksbysample-eipapp-cloudaws";
    private static final String UPLOAD_DIR_PATH = EIPAPP_ROOT_DIR_PATH + "/upload";
    private static final String UPLOADING_DIR_PATH = EIPAPP_ROOT_DIR_PATH + "/uploading";
    private static final String S3_BUCKET = "s3bucket-integration-test-ksby";

    // リージョンは環境変数 AWS_REGION に(東京リージョンなら ap-northeast-1)、
    // AccessKeyId, SecretAccessKey はそれぞれ環境変数 AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY にセットする
    @Bean
    public TransferManager transferManager() {
        return TransferManagerBuilder.standard().build();
    }

    /********************************************************
     * upload ディレクトリ --> S3 ファイルアップロード処理        *
     ********************************************************/

    @Bean
    public FileReadingMessageSource uploadFileMessageSource() {
        FileReadingMessageSource source = new FileReadingMessageSource();
        source.setDirectory(new File(UPLOAD_DIR_PATH));
        source.setFilter(new AcceptAllFileListFilter<>());
        return source;
    }

    @Bean
    public IntegrationFlow uploadToS3Flow() {
        return IntegrationFlows.from(
                // 200ミリ秒毎に upload ディレクトリを監視し、ファイルがあれば処理を進める
                uploadFileMessageSource(), c -> c.poller(Pollers.fixedDelay(200)))
                // ファイルを uploading ディレクトリへ移動する
                .<File>handle((p, h) -> {
                    try {
                        Path movedFilePath = Files.move(p.toPath(), Paths.get(UPLOADING_DIR_PATH, p.getName())
                                , StandardCopyOption.REPLACE_EXISTING);
                        return new GenericMessage<>(movedFilePath.toFile(), h);
                    } catch (IOException e) {
                        throw new RuntimeException(e);
                    }
                })
                // ここから下はマルチスレッドで並列処理する
                .channel(c -> c.executor(Executors.newFixedThreadPool(2)))
                // 処理開始のログを出力し、S3 へアップロードする
                .<File>handle((p, h) -> {
                    log.warn(String.format("☆☆☆ %s を S3 にアップロードします", p.getName()));
                    try {
                        // .waitForUploadResult() も呼び出してアップロード完了を待たないとファイルはアップロードされない
                        transferManager()
                                .upload(S3_BUCKET, p.getName(), p)
                                .waitForUploadResult();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    return new GenericMessage<>(p, h);
                })
                // アップロードしたファイルを削除し、処理終了のログを出力する
                .<File>handle((p, h) -> {
                    p.delete();
                    log.warn(String.format("★★★ %s を S3 にアップロードしました", p.getName()));
                    return null;
                })
                .get();
    }

}

upload ディレクトリに5ファイルだけ配置して動作確認してみます。最初に IntelliJ IDEA のメインメニューから「Run」-「Edit Configurations...」を選択して「Run/Debug Configurations」ダイアログを表示後、AWS_REGION、AWS_ACCESS_KEY_ID、AWS_SECRET_ACCESS_KEY を設定します。

f:id:ksby:20180912070220p:plain

s3bucket-integration-test-ksby バケットが空の状態であることを確認します。

f:id:ksby:20180912070414p:plain

アプリケーションを起動し upload ディレクトリに5ファイル配置すると、ファイルがアップロードされます。

f:id:ksby:20180912070651p:plain

再び s3bucket-integration-test-ksby バケットを見ると5ファイルアップロードされていました。ダウンロードして元の画像と比較すると同じファイルで問題ありませんでした。

S3 → download ディレクトリへダウンロード → upload ディレクトリへ移動する処理を実装する

src/main/java/ksbysample/eipapp/cloudaws/flow/FlowConfig.java に以下の処理を追加します。

@Slf4j
@Configuration
public class FlowConfig {

    ..........
    private static final String DOWNLOAD_DIR_PATH = EIPAPP_ROOT_DIR_PATH + "/download";
    ..........

    /********************************************************
     * S3 --> download ディレクトリ ファイルダウンロード処理      *
     ********************************************************/

    @Bean
    public MessageSource<File> downloadFileFromS3MessageSource() {
        return () -> {
            try {
                File downloadFile = null;
                String key = null;
                ObjectListing objectListing = amazonS3().listObjects(S3_BUCKET);
                if (objectListing.getObjectSummaries().size() > 0) {
                    S3ObjectSummary summary = objectListing.getObjectSummaries().iterator().next();
                    key = summary.getKey();
                    downloadFile = Paths.get(DOWNLOAD_DIR_PATH, key).toFile();
                    transferManager().download(S3_BUCKET, key, downloadFile)
                            .waitForCompletion();
                }
                return downloadFile != null
                        ? MessageBuilder.withPayload(downloadFile).setHeader("s3Path", key).build()
                        : null;
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        };
    }

    @Bean
    public IntegrationFlow downloadFromS3Flow() {
        return IntegrationFlows.from(
                // 200ミリ秒毎に S3 Bucket を監視し、ファイルがあれば処理を進める
                downloadFileFromS3MessageSource(), c -> c.poller(Pollers
                        .fixedDelay(200)))
                // download ディレクトリに保存されたファイルを upload ディレクトリに移動する
                // ちなみに download ディレクトリからファイルを移動か削除しないと s3InboundFileSynchronizingMessageSource()
                // から Message が延々と送信され続けるので、必ず移動か削除する必要がある
                .<File>handle((p, h) -> {
                    try {
                        Files.move(p.toPath(), Paths.get(UPLOAD_DIR_PATH, p.getName())
                                , StandardCopyOption.REPLACE_EXISTING);
                        log.error(String.format("◎◎◎ %s をダウンロードしました", p.getName()));
                        return new GenericMessage<>(p, h);
                    } catch (IOException e) {
                        throw new RuntimeException(e);
                    }
                })
                .<File>handle((p, h) -> {
                    amazonS3().deleteObject(S3_BUCKET, (String) h.get("s3Path"));
                    return null;
                })
                .get();
    }

}

アップロードの処理をコメントアウトして、アップロードしたファイルをダウンロードしてみます。

アプリケーションを起動すると S3 Bucket にあるファイルが download ディレクトリにダウンロード → upload ディレクトリへ移動されて、

f:id:ksby:20180916154326p:plain

S3 Bucket の中は空になっていました。

f:id:ksby:20180916154455p:plain

ダウンロードされたファイルは元々アップロードしたファイルと全く同じで問題ありませんでした。

動作確認

アプリケーションを起動後、最初は upload ディレクトリにファイルを1つだけ配置してみます。

f:id:ksby:20180916160000p:plain

ファイルのアップロードとダウンロードを繰り返し、エラーも発生しませんでした。

今度は 30ファイル配置してみます。

f:id:ksby:20180916160507p:plain f:id:ksby:20180916160609p:plain f:id:ksby:20180916160738p:plain f:id:ksby:20180916160909p:plain

ダウンロード・アップロードが実行されました。全然制御していないので、同じファイルばかりダウンロードされていますが。。。

S3 Bucket とアップロード・ダウンロード用の IAM ユーザを削除する

以下のコマンドを実行して、作成した S3 Bucket とアップロード・ダウンロード用の IAM ユーザを削除します。

f:id:ksby:20180916170109p:plain

Spring Integration Extension for AWS と Spring Cloud for AWS を使用してみた感想

  • Spring Integration Extension for AWS を使用すると Spring Integration らしく実装できて、かつ便利になったという感じがします。
  • Spring Cloud for AWSAWS SDK for Java の知識がないとやりたいことが実装できなくて、S3 を使うだけの場合、あまりメリットを感じられませんでした。build.gradle に implementation('org.springframework.cloud:spring-cloud-aws-context') を記述すれば依存性解決をしてくれることくらいのような気がします。
  • implementation('org.springframework.cloud:spring-cloud-starter-aws') を記述すると、裏で何かいろいろ設定されるのか、PC 上で起動できなくなる(起動するためにいろいろ調べないといけない)ので、やり過ぎの感じがします。EC2 インスタンス上で起動すれば便利に感じるのかもしれませんが、今回は PC 上で実行していたので分かりません。implementation('org.springframework.integration:spring-integration-aws:2.0.0.RELEASE') はそんなことはなかったので、Spring Integration Extension for AWS の方で不要な自動設定を無効にしてくれているのでしょうか。。。
  • Spring Cloud for AWS のダウンロード処理を実装しようとしていて思ったのが、Spring Integration の MessageSource の作成方法の理解が全然足りないということでした。Spring Integration Extension for AWS だと Spring Integration に既にある AbstractInboundFileSynchronizer や AbstractInboundFileSynchronizingMessageSource を利用して実装されているのですが、それらのクラスは今回初めて知りました。Spring Integration、まだまだ奥が深いです。

履歴

2018/09/16
初版発行。

Spring Boot + Spring Integration でいろいろ試してみる ( その27 )( Spring Integration Extension for AWS で S3 へファイルをアップロード・ダウンロードする )

概要

記事一覧 はこちらです。
今回作成したソースの GitHub レポジトリ はこちらです。

Spring Integration のアプリケーションから AWS の S3 へファイルをアップロード・ダウンロードするのに使えそうな Spring のライブラリとして以下の2種類がありますが、

それぞれのサンプルを作成してみます。まずは Spring Integration Extension for Amazon Web Services (AWS) から。

ちなみにこの2つのライブラリの違いですが、Spring Integration Extension for Amazon Web Services (AWS)Note the Spring Integration AWS Extension is based on the Spring Cloud AWS project. と記載されていました。Spring Integration Extension for AWS は Spring Cloud for AWS をベースに Spring Integration の Adapter 等を作成したもののようです。

参照したサイト・書籍

目次

  1. 仕様を決める
  2. ksbysample-eipapp-integrationaws プロジェクトを作成する
  3. S3 Bucket とアップロード・ダウンロード用の IAM ユーザを作成する
  4. upload ディレクトリ → S3 へアップロードする処理を実装する
  5. S3 → download ディレクトリへダウンロード → upload ディレクトリへ移動する処理を実装する
  6. 動作確認

手順

仕様を決める

  • S3 Bucket を1つ作成します。
  • 作成した S3 Bucket へアップロード・ダウンロードするための IAM ユーザを1つ作成します。
  • アプリケーションは AWS 側ではなく PC 上で実行します。
  • 作成したアプリケーションを実行するディレクトリ構成を以下のようにします。
D:\eipapp\ksbysample-eipapp-integrationaws
├ download
├ upload
├ uploading
└ ksbysample-eipapp-integrationaws-0.0.1-SNAPSHOT.jar
  • 最初に upload ディレクトリの下に画像ファイルを 30ファイル配置します。1ファイル 3~4MB 程度のファイルです。
  • 一定時間毎に upload ディレクトリを監視し、ファイルがあれば S3 Buket へアップロードします。アップロードするファイルは upload ディレクトリから uploading ディレクトリへ移動してから S3 へアップロードします(ディレクトリ監視に引っかからないようにするため)。
  • S3 へのアップロードはマルチスレッドで並列処理します。
  • 一定時間毎に S3 Bucket を監視し、ファイルがあれば PC にダウンロードします。こちらはシングルスレッドで処理します。
  • upload ディレクトリ →(アップロードFlow)→ S3 Bucket
    →(ダウンロードFlow)→ download ディレクトリ → upload ディレクト
    →(続く)
    という流れになります。

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

Spring Initializr でプロジェクトの雛形を作成します。

f:id:ksby:20180909112018p:plain f:id:ksby:20180909112113p:plain f:id:ksby:20180909112211p:plain f:id:ksby:20180909112341p:plain

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

buildscript {
    ext {
        springBootVersion = '2.0.4.RELEASE'
    }
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
    }
}

apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'

group = 'ksbysample'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = 1.8

repositories {
    mavenCentral()
}

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

dependencies {
    def lombokVersion = "1.18.2"

    implementation('org.springframework.boot:spring-boot-starter-integration')
    implementation('org.springframework.integration:spring-integration-aws:2.0.0.RELEASE')
    implementation('org.springframework.integration:spring-integration-file')
    testImplementation('org.springframework.boot:spring-boot-starter-test')

    // for lombok
    annotationProcessor("org.projectlombok:lombok:${lombokVersion}")
    compileOnly("org.projectlombok:lombok:${lombokVersion}")
}
  • dependencyManagement block を追加します。
  • dependencies block に以下の2行を追加します。
    • implementation('org.springframework.integration:spring-integration-aws:2.0.0.RELEASE')
    • implementation('org.springframework.integration:spring-integration-file')
  • lombok@Slf4j アノテーションを使いたいので、dependencies block に以下の3行を追加します。
    • def lombokVersion = "1.18.2"
    • annotationProcessor("org.projectlombok:lombok:${lombokVersion}")
    • compileOnly("org.projectlombok:lombok:${lombokVersion}")

メインクラス名を KsbysampleEipappIntegrationawsApplication → Application に変更した後、clean タスク実行 → Rebuild Project 実行 → build タスクを実行して "BUILD SUCCESSFUL" のメッセージが出力されることを確認します。

S3 Bucket とアップロード・ダウンロード用の IAM ユーザを作成する

AWS アカウントがすでに作成されていること、AWS CLI をインストール済であること、AWS CloudFormation を実行可能な環境を作成済であること、が前提で以下の手順を記載しています。

プロジェクトのルート直下に create-s3bucket.yaml を新規作成し、以下の内容を記述します。

AWSTemplateFormatVersion: 2010-09-09
Description: Spring Integration Test Stack

Resources:
  #########################
  # S3 Bucket
  #########################
  IntegrationTestS3Bucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: "s3bucket-integration-test-ksby"

  #########################
  # IAM User & IAM Policy
  #########################
  IntegrationTestUser:
    Type: AWS::IAM::User
    Properties:
      UserName: "iam-user-integration-test"
  IntegrationTestUserAccessKey:
    Type: AWS::IAM::AccessKey
    Properties:
      UserName: !Ref IntegrationTestUser
  IntegrationTestPolicy:
    Type: AWS::IAM::Policy
    Properties:
      PolicyName: "iam-policy-integration-test"
      PolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Action:
              - s3:ListBucket
              - s3:GetBucketLocation
            Resource: !Join [ "", [ "arn:aws:s3:::", !Ref IntegrationTestS3Bucket ] ]
          - Effect: Allow
            Action:
              - s3:PutObject
              - s3:PutObjectAcl
              - s3:GetObject
              - s3:GetObjectAcl
              - s3:DeleteObject
            Resource: !Join [ "", [ "arn:aws:s3:::", !Ref IntegrationTestS3Bucket, "/*" ] ]
      Users:
        - !Ref IntegrationTestUser

Outputs:
  AccessKeyId:
    Value: !Ref IntegrationTestUserAccessKey
  SecretAccessKey:
    Value: !GetAtt IntegrationTestUserAccessKey.SecretAccessKey

git-cmd.exe を起動した後、以下のコマンドを実行してテストで使用する S3 Bucket とアップロード・ダウンロード用の IAM ユーザを作成します。

f:id:ksby:20180909165121p:plain f:id:ksby:20180909165328p:plain

upload ディレクトリ → S3 へアップロードする処理を実装する

src/main/java/ksbysample/eipapp/integrationaws の下に flow パッケージを作成した後、その下に FlowConfig.java を新規作成して、以下の内容を記述します。

@Slf4j
@Configuration
public class FlowConfig {

    private static final String EIPAPP_ROOT_DIR_PATH = "D:/eipapp/ksbysample-eipapp-integrationaws";
    private static final String UPLOAD_DIR_PATH = EIPAPP_ROOT_DIR_PATH + "/upload";
    private static final String UPLOADING_DIR_PATH = EIPAPP_ROOT_DIR_PATH + "/uploading";
    private static final String S3_BUCKET = "s3bucket-integration-test-ksby";

    // リージョンは環境変数 AWS_REGION に(東京リージョンなら ap-northeast-1)、
    // AccessKeyId, SecretAccessKey はそれぞれ環境変数 AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY にセットする
    @Bean
    public AmazonS3 amazonS3() {
        return AmazonS3ClientBuilder.standard().build();
    }

    /********************************************************
     * upload ディレクトリ --> S3 ファイルアップロード処理        *
     ********************************************************/

    @Bean
    public FileReadingMessageSource uploadFileMessageSource() {
        FileReadingMessageSource source = new FileReadingMessageSource();
        source.setDirectory(new File(UPLOAD_DIR_PATH));
        source.setFilter(new AcceptAllFileListFilter<>());
        return source;
    }

    @Bean
    public MessageHandler uploadToS3MessageHandler() {
        return new S3MessageHandler(amazonS3(), S3_BUCKET);
    }

    @Bean
    public IntegrationFlow uploadToS3Flow() {
        return IntegrationFlows.from(
                // 200ミリ秒毎に upload ディレクトリを監視し、ファイルがあれば処理を進める
                uploadFileMessageSource(), c -> c.poller(Pollers.fixedDelay(200)))
                // ファイルを uploading ディレクトリへ移動する
                .<File>handle((p, h) -> {
                    try {
                        Path movedFilePath = Files.move(p.toPath(), Paths.get(UPLOADING_DIR_PATH, p.getName())
                                , StandardCopyOption.REPLACE_EXISTING);
                        return new GenericMessage<>(movedFilePath.toFile(), h);
                    } catch (IOException e) {
                        throw new RuntimeException(e);
                    }
                })
                // ここから下はマルチスレッドで並列処理する
                .channel(c -> c.executor(Executors.newFixedThreadPool(5)))
                // 処理開始のログを出力する
                // 上の .channel(...) の直後に .log(...) を書くと並列処理されないため、.handle(...) を書いてその中でログに出力する
                .<File>handle((p, h) -> {
                    log.warn(String.format("☆☆☆ %s を S3 にアップロードします", p.getName()));
                    return new GenericMessage<>(p, h);
                })
                // S3 へアップロードする
                // S3MessageHandler は Outbound Channel Adapter で .handle(...) メソッドに渡しただけでは次の処理に行かないので、
                // .wireTap(...) で呼び出す
                .wireTap(sf -> sf
                        .handle(uploadToS3MessageHandler()))
                // アップロードしたファイルを削除し、処理終了のログを出力する
                .<File>handle((p, h) -> {
                    p.delete();
                    log.warn(String.format("★★★ %s を S3 にアップロードしました", p.getName()));
                    return null;
                })
                .get();
    }

}

upload ディレクトリに5ファイルだけ配置して動作確認してみます。最初に IntelliJ IDEA のメインメニューから「Run」-「Edit Configurations...」を選択して「Run/Debug Configurations」ダイアログを表示後、AWS_REGION、AWS_ACCESS_KEY_ID、AWS_SECRET_ACCESS_KEY を設定します。

f:id:ksby:20180909195452p:plain

s3bucket-integration-test-ksby バケットが空の状態であることを確認します。

f:id:ksby:20180909194942p:plain

アプリケーションを起動し upload ディレクトリに5ファイル配置すると、ファイルがアップロードされます。

f:id:ksby:20180909195733p:plain

再び s3bucket-integration-test-ksby バケットを見ると5ファイルアップロードされていました。ダウンロードして元の画像と比較すると同じファイルで問題ありませんでした。

f:id:ksby:20180909195903p:plain

S3 → download ディレクトリへダウンロード → upload ディレクトリへ移動する処理を実装する

src/main/java/ksbysample/eipapp/integrationaws/flow/FlowConfig.java に以下の処理を追加します。

@Slf4j
@Configuration
public class FlowConfig {

    ..........
    private static final String DOWNLOAD_DIR_PATH = EIPAPP_ROOT_DIR_PATH + "/download";
    ..........

    /********************************************************
     * S3 --> download ディレクトリ ファイルダウンロード処理      *
     ********************************************************/

    @Bean
    public S3InboundFileSynchronizer s3InboundFileSynchronizer() {
        S3InboundFileSynchronizer synchronizer = new S3InboundFileSynchronizer(amazonS3());
        synchronizer.setDeleteRemoteFiles(true);
        synchronizer.setPreserveTimestamp(true);
        synchronizer.setRemoteDirectory(S3_BUCKET);
        return synchronizer;
    }

    @Bean
    public S3InboundFileSynchronizingMessageSource s3InboundFileSynchronizingMessageSource() {
        S3InboundFileSynchronizingMessageSource messageSource =
                new S3InboundFileSynchronizingMessageSource(s3InboundFileSynchronizer());
        messageSource.setLocalDirectory(new File(DOWNLOAD_DIR_PATH));
        messageSource.setLocalFilter(new AcceptAllFileListFilter<>());
        return messageSource;
    }

    @Bean
    public IntegrationFlow downloadFromS3Flow() {
        return IntegrationFlows.from(
                // 1秒毎に S3 Bucket を監視し、ファイルがあれば処理を進める
                s3InboundFileSynchronizingMessageSource(), c -> c.poller(Pollers
                        .fixedDelay(1000)
                        // 1度に最大100ファイルダウンロードする
                        // .maxMessagesPerPoll(...) を書かないと 1ファイルずつダウンロードされる
                        .maxMessagesPerPoll(100)))
                // download ディレクトリに保存されたファイルを upload ディレクトリに移動する
                // ちなみに download ディレクトリからファイルを移動か削除しないと s3InboundFileSynchronizingMessageSource()
                // から Message が延々と送信され続けるので、必ず移動か削除する必要がある
                .<File>handle((p, h) -> {
                    try {
                        Files.move(p.toPath(), Paths.get(UPLOAD_DIR_PATH, p.getName())
                                , StandardCopyOption.REPLACE_EXISTING);
                        log.error(String.format("◎◎◎ %s をダウンロードしました", p.getName()));
                        return null;
                    } catch (IOException e) {
                        throw new RuntimeException(e);
                    }
                })
                .get();
    }

}

アップロードの処理をコメントアウトして、先程アップロードしたファイルをダウンロードしてみます。

アプリケーションを起動すると S3 Bucket にあるファイルが download ディレクトリにダウンロード → upload ディレクトリへ移動されて、

f:id:ksby:20180909211846p:plain

S3 Bucket の中は空になっていました。

f:id:ksby:20180909212049p:plain

ダウンロードされたファイルは元々アップロードしたファイルと全く同じで問題ありませんでした。

動作確認

アプリケーションを起動後、最初は upload ディレクトリにファイルを1つだけ配置してみます。

f:id:ksby:20180909232800p:plain

ファイルのアップロードとダウンロードを繰り返し、エラーも発生しませんでした。

今度は 30ファイル配置してみます。

f:id:ksby:20180909233613p:plain f:id:ksby:20180909233710p:plain f:id:ksby:20180909233828p:plain f:id:ksby:20180909233931p:plain f:id:ksby:20180909234027p:plain

なんか想定と違う動きをしています。。。

  • アップロード処理がマルチスレッドなので当然処理は多くて当たり前なのですが、ダウンロード処理がほとんど動きません。
  • org.springframework.messaging.MessagingException: Problem occurred while synchronizing remote to local directory; nested exception is org.springframework.messaging.MessagingException: Failure occurred while copying 's3bucket-integration-test-ksby/P7250021.JPG' from the remote to the local directory; nested exception is java.net.SocketTimeoutException: Read timed out というエラーも出ています。
  • エラーの後に一気にダウンロードが実行されています。

1CPU2Core の PC で動かしているのですが、おそらく 5スレッドだと重いのと、ダウロード処理がどうも S3 Bucket 内に見つけたファイルを全て PC にダウンロードしてから処理を進めているようなので(ダウンロードのログが出ていないのに download ディレクトリにファイルが出来ていました)、以下の点を変更します。

    @Bean
    public IntegrationFlow uploadToS3Flow() {
                ..........
                // ここから下はマルチスレッドで並列処理する
                .channel(c -> c.executor(Executors.newFixedThreadPool(2)))
                ..........
    }

    ..........

    @Bean
    public S3InboundFileSynchronizingMessageSource s3InboundFileSynchronizingMessageSource() {
        S3InboundFileSynchronizingMessageSource messageSource =
                new S3InboundFileSynchronizingMessageSource(s3InboundFileSynchronizer());
        messageSource.setLocalDirectory(new File(DOWNLOAD_DIR_PATH));
        messageSource.setLocalFilter(new AcceptAllFileListFilter<>());
        messageSource.setMaxFetchSize(1);
        return messageSource;
    }

    @Bean
    public IntegrationFlow downloadFromS3Flow() {
        return IntegrationFlows.from(
                // 1秒毎に S3 Bucket を監視し、ファイルがあれば処理を進める
                s3InboundFileSynchronizingMessageSource(), c -> c.poller(Pollers
                        .fixedDelay(200)))
                ..........
    }

}
  • uploadToS3Flow 内のマルチスレッド数を 5 → 2 に変更します。
  • s3InboundFileSynchronizingMessageSource 内に messageSource.setMaxFetchSize(1); を追加し、S3 Bucket に見つけたファイルを全てダウンロードするのではなく 1ファイルだけダウンロードするようにします。
  • downloadFromS3Flow のポーリング間隔を uploadToS3Flow と同じ 200ミリ秒にし、.maxMessagesPerPoll(100) を削除します。

変更後に再度実行すると、最初はアップロードばかりが動きますが、途中からアップロードとダウンロードがバランスよく動くようになりました。

f:id:ksby:20180910001644p:plain

jar ファイルを作成してコマンドラインから実行してみます。まずは jar ファイルを生成して D:\eipapp\ksbysample-eipapp-integrationaws の下にコピーします。

コピー後、環境変数 AWS_REGION、AWS_ACCESS_KEY_ID、AWS_SECRET_ACCESS_KEY を設定してからアプリケーションを実行します。

f:id:ksby:20180911043628p:plain

ksbysample-eipapp-integrationaws.log を IntelliJ IDEA のメインメニューの「Tools」-「Tail File in Console...」で開いておきます。

f:id:ksby:20180911043731p:plain

upload ディレクトリに 30ファイル配置すると、ダウンロード・アップロードが実行されました。

f:id:ksby:20180911044230p:plain f:id:ksby:20180911044329p:plain f:id:ksby:20180911044431p:plain f:id:ksby:20180911044553p:plain

Ctrl+C でアプリケーションを終了させた後、S3 にアップロードされたファイルを削除しておきます。

履歴

2018/09/11
初版発行。

IntelliJ IDEA を 2018.2.2 → 2018.2.3 へバージョンアップ

IntelliJ IDEA を 2018.2.2 → 2018.2.3 へバージョンアップする

IntelliJ IDEA の 2018.2.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:20180908224851p:plain

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

    f:id:ksby:20180908224940p:plain

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

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

    f:id:ksby:20180908225350p:plain

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

  7. Gradle Tool Window のツリーを見ると「Tasks」の下に「other」しかない状態になっているので、左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。

    f:id:ksby:20180908225554p:plain

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

    f:id:ksby:20180908230247p:plain

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

    f:id:ksby:20180908230829p:plain

Spring Boot 1.5.x の Web アプリを 2.0.x へバージョンアップする ( その4 )( AbstractJsonpResponseBodyAdvice を削除し、失敗しているテストを成功させる )

概要

記事一覧はこちらです。

Spring Boot 1.5.x の Web アプリを 2.0.x へバージョンアップする ( その3 )( build.gradle を変更する ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • AbstractJsonpResponseBodyAdvice が非推奨になったので、使用しないようにします。
    • 失敗しているテストを解消します。

参照したサイト・書籍

目次

  1. AbstractJsonpResponseBodyAdvice を削除する
  2. 失敗しているテストを成功させる
    1. ksbysample.webapp.lending.listener.rabbitmq.InquiringStatusOfBookQueueListenerTest > testReceiveMessage FAILED
    2. ksbysample.webapp.lending.web.lendingapproval.LendingapprovalControllerTest > ksbysample.webapp.lending.web.lendingapproval.LendingapprovalControllerTest$貸出承認画面の正常処理時のテスト.確定ボタンをクリックした場合_却下と却下理由 FAILED
    3. ksbysample.webapp.lending.web.LoginControllerTest > ksbysample.webapp.lending.web.LoginControllerTest$ログインエラーのテスト.存在するユーザ名でもパスワードが正しくなければログインはエラーになる FAILED
    4. ksbysample.webapp.lending.web.LoginControllerTest > ksbysample.webapp.lending.web.LoginControllerTest$ログインエラーのテスト.ログインを5回失敗すればアカウントはロックされる FAILED
  3. 次回は。。。

手順

AbstractJsonpResponseBodyAdvice を削除する

JavascriptJSONP で WebAPI を呼び出しているところはなかったので、src/main/java/ksbysample/webapp/lending/config/JsonpAdvice.java を削除します。

失敗しているテストを成功させる

失敗していた4件のテストを1つずつ見ていきます。

ksbysample.webapp.lending.listener.rabbitmq.InquiringStatusOfBookQueueListenerTest > testReceiveMessage FAILED

f:id:ksby:20180908171519p:plain f:id:ksby:20180908171703p:plain

テストが失敗した原因は、org.apache.commons.lang3.builder.ToStringBuilder#reflectionToString で java.lang.NullPointerException が発生していたためでした。

f:id:ksby:20180908174827p:plain

テストを Debug 実行して調べると、this.correlationId が null のため this.correlationId.hashCode()NullPointerException が発生していました。

もう少し調べた結果、以下の原因であることが分かりました。

  • テストのために、本来は RabbitMQ から受信するはずの Message オブジェクトをテストメソッド内で生成していました。下の画像の赤枠の部分です。 f:id:ksby:20180908185542p:plain
  • この時 messageConverter.toMessage(...) の第2引数に MessageProperties オブジェクトが必要なので new MessageProperties() と書いていたのですが、これだと correlationId に値をセットしていません。
  • messageConverter.toMessage(...) の後に listener.receiveMessage(message); を呼び出しているのですが、listener.receiveMessage(...); が呼び出されると MethodLogger#logginBeginMethod によりメソッドの情報をログに出力しようとします。この時 MessageProperties#hashCode が呼び出されるのですが、this.correlationId が null なので NullPointerException が発生していました。

NullPointerException が発生しないようにするには correlationId をセットすればよさそうです。MessageProperties を生成するための MessagePropertiesBuilder というクラスを見つけたので、new MessageProperties()MessagePropertiesBuilder.newInstance().setCorrelationId("test").build() に変更します。

f:id:ksby:20180908190713p:plain

変更後にテストを実行すると成功しました。

f:id:ksby:20180908191019p:plain

ksbysample.webapp.lending.web.lendingapproval.LendingapprovalControllerTest > ksbysample.webapp.lending.web.lendingapproval.LendingapprovalControllerTest$貸出承認画面の正常処理時のテスト.確定ボタンをクリックした場合_却下と却下理由 FAILED

f:id:ksby:20180908195737p:plain f:id:ksby:20180908195952p:plain

テストが失敗した原因は、メールの中身が "承認" ではなく "却下" になっていたためでした。

テストメソッドを見たら public void 確定ボタンをクリックした場合_却下と却下理由() throws Exception { ... } と書いていて、どうも却下のテストをしているので "却下" になっているのが正しいのでは?と思って調べたら、単に前回の修正ミスでした。メソッド内の messageTxt001Resource.getFile()messageTxt002Resource.getFile() に修正します。

修正後にテストを実行すると成功しました。

f:id:ksby:20180908201021p:plain

ksbysample.webapp.lending.web.LoginControllerTest > ksbysample.webapp.lending.web.LoginControllerTest$ログインエラーのテスト.存在するユーザ名でもパスワードが正しくなければログインはエラーになる FAILED

f:id:ksby:20180908201623p:plain

java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null" というエラーメッセージが出ていました。

stackoverflow を検索すると以下のページが見つかりました。

Spring Security 5.0.0.RC1 Released の「Password Storage Format」にパスワードの文字列のフォーマットが {id}encodedPassword に変更されたと書かれています。テストで使用している DB に保存されているパスワードには {id} の部分を付けていないので java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null" のエラーが出たようです。DB に保存するパスワードは bcrypt で暗号化しているので、先頭に {bcrypt} の文字列を追加するようにします。

以下のファイルを変更します。

  • src/test/resources/testdata/assertdata/user_info.csv
  • src/test/resources/testdata/base/user_info.csv

f:id:ksby:20180908210436p:plain

変更後にテストを実行すると成功しました。

f:id:ksby:20180908210824p:plain

ksbysample.webapp.lending.web.LoginControllerTest > ksbysample.webapp.lending.web.LoginControllerTest$ログインエラーのテスト.ログインを5回失敗すればアカウントはロックされる FAILED

このエラーも出ていたのですが、上の "{bcrypt}" を入れる対応をしたら出なくなりました。

次回は。。。

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

f:id:ksby:20180908212230p:plain

次回は checkstyle、PMD をバージョンアップします。

履歴

2018/09/08
初版発行。

Spring Boot 1.5.x の Web アプリを 2.0.x へバージョンアップする ( その3 )( build.gradle を変更する )

概要

記事一覧はこちらです。

Spring Boot 1.5.x の Web アプリを 2.0.x へバージョンアップする ( その2 )( Gradle を 3.5 → 4.10 にバージョンアップする ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • build.gradle を変更します。
      • Spring Initializr で 2.0.4 のプロジェクトを作成して、変更した方がよい点があれば反映します。
      • Spring Boot のバージョンを 1.5.4 → 2.0.4 に変更します。
      • BOM を Spring IO Platform のものから Spring Boot のものに変更します。
      • ライブラリは最新バージョンにアップデートします。

参照したサイト・書籍

目次

  1. Spring Initializr で 2.0.4 のプロジェクトを作成する
  2. build.gradle を変更する
  3. Rebuild Project でエラーの出るクラスを修正する
    1. src/main/java/ksbysample/webapp/lending/config/ApplicationConfig.java
    2. src/main/java/ksbysample/webapp/lending/config/WebMvcConfig.java
    3. src/main/java/ksbysample/webapp/lending/config/WebSecurityConfig.java
    4. src/main/java/ksbysample/webapp/lending/security/LendingUserDetailsService.java
    5. src/main/java/ksbysample/webapp/lending/web/WebappErrorController.java
    6. org.hibernate.validator.constraintsのorg.hibernate.validator.constraints.NotBlankは非推奨になりました
    7. org.springframework.web.servlet.mvc.method.annotationのorg.springframework.web.servlet.mvc.method.annotation.AbstractJsonpResponseBodyAdviceは非推奨になりました
  4. build タスク実行時に出るエラーを修正する
    1. An unhandled exception was thrown by the Error Prone static analysis plugin. @Data
    2. 警告:[deprecation] org.mockitoのMatchersは推奨されません
    3. 警告:[deprecation] FilesのtoString(File,Charset)は推奨されません
    4. 警告:[deprecation] MockMvcRequestBuildersのfileUpload(String,Object...)は推奨されません
  5. 次回は。。。

手順

Spring Initializr で 2.0.4 のプロジェクトを作成する

Spring Initializr で 2.0.4 のプロジェクトを作成して、生成された build.gradle を見て反映した方が良い点があるか確認します。

f:id:ksby:20180902103149p:plain f:id:ksby:20180902103249p:plain f:id:ksby:20180902103400p:plain f:id:ksby:20180902103451p:plain f:id:ksby:20180902103825p:plain f:id:ksby:20180902103926p:plain f:id:ksby:20180902104018p:plain f:id:ksby:20180902104258p:plain f:id:ksby:20180902104339p:plain

以下の build.gradle が作成されました。

buildscript {
    ext {
        springBootVersion = '2.0.4.RELEASE'
    }
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
    }
}

apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = 1.8

repositories {
    mavenCentral()
}


dependencies {
    compile('org.springframework.boot:spring-boot-starter-data-redis')
    compile('org.springframework.boot:spring-boot-starter-freemarker')
    compile('org.springframework.boot:spring-boot-starter-mail')
    compile('org.springframework.boot:spring-boot-starter-security')
    compile('org.springframework.boot:spring-boot-starter-thymeleaf')
    compile('org.springframework.boot:spring-boot-starter-web')
    compile('org.springframework.session:spring-session-data-redis')
    runtime('org.springframework.boot:spring-boot-devtools')
    compileOnly('org.projectlombok:lombok')
    testCompile('org.springframework.boot:spring-boot-starter-test')
    testCompile('org.springframework.security:spring-security-test')
}

反映した方がよいのは Spring Boot 2.0 Migration Guide に書いてある apply plugin: 'io.spring.dependency-management' だけでした。これは Spring Boot 2.0 Migration Guide に従って変更するので、Spring Initializr で生成された build.gradle からは何も反映しません。

build.gradle を変更する

以下の方針で変更します。

buildscript {
    ext {
        group "ksbysample"
        version "2.0.4-RELEASE"
    }
    repositories {
        mavenCentral()
        maven { url "https://repo.spring.io/release/" }
        maven { url "https://plugins.gradle.org/m2/" }
    }
}

plugins {
    id "java"
    id "eclipse"
    id "idea"
    id "org.springframework.boot" version "2.0.4.RELEASE"
    id "io.spring.dependency-management" version "1.0.6.RELEASE"
    id "groovy"
    id "checkstyle"
    id "findbugs"
    id "pmd"
    id "net.ltgt.errorprone" version "0.0.16"
    id "de.undercouch.download" version "3.4.3"
}

sourceCompatibility = 1.8
targetCompatibility = 1.8

wrapper {
    gradleVersion = "4.10"
    distributionType = Wrapper.DistributionType.ALL
}

[compileJava, compileTestGroovy, compileTestJava]*.options*.encoding = "UTF-8"
[compileJava, compileTestGroovy, compileTestJava]*.options*.compilerArgs = ["-Xlint:all,-options,-processing,-path"]
compileJava.options.compilerArgs += [
        "-Xep:RemoveUnusedImports:WARN"
        , "-Xep:InsecureCryptoUsage:OFF"
]

// for Doma 2
// JavaクラスとSQLファイルの出力先ディレクトリを同じにする
processResources.destinationDir = compileJava.destinationDir
// コンパイルより前にSQLファイルを出力先ディレクトリにコピーするために依存関係を逆転する
compileJava.dependsOn processResources

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

springBoot {
    buildInfo()
}

configurations {
    // for Doma 2
    domaGenRuntime
}

checkstyle {
    configFile = file("${rootProject.projectDir}/config/checkstyle/google_checks.xml")
    toolVersion = "7.8.1"
    sourceSets = [project.sourceSets.main]
}

findbugs {
    toolVersion = "3.0.1"
    sourceSets = [project.sourceSets.main]
    ignoreFailures = true
    effort = "max"
    excludeFilter = file("${rootProject.projectDir}/config/findbugs/findbugs-exclude.xml")
}

tasks.withType(FindBugs) {
    reports {
        xml.enabled = false
        html.enabled = true
    }
}

pmd {
    toolVersion = "5.8.1"
    sourceSets = [project.sourceSets.main]
    ignoreFailures = true
    consoleOutput = true
    ruleSetFiles = rootProject.files("/config/pmd/pmd-project-rulesets.xml")
    ruleSets = []
}

repositories {
    mavenCentral()
}

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

dependencies {
    def jdbcDriver = "org.postgresql:postgresql:42.2.4"
    def spockVersion = "1.1-groovy-2.4"
    def domaVersion = "2.19.3"
    def lombokVersion = "1.18.2"
    def errorproneVersion = "2.3.1"
    def powermockVersion = "2.0.0-beta.5"

    // dependency-management-plugin によりバージョン番号が自動で設定されるもの
    // Appendix A. Dependency versions ( http://docs.spring.io/platform/docs/current/reference/htmlsingle/#appendix-dependency-versions ) 参照
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("org.springframework.boot:spring-boot-starter-thymeleaf") {
        exclude group: "org.codehaus.groovy", module: "groovy"
    }
    implementation("org.thymeleaf.extras:thymeleaf-extras-springsecurity4")
    implementation("org.thymeleaf.extras:thymeleaf-extras-java8time")
    implementation("org.springframework.boot:spring-boot-starter-data-jpa")
    implementation("org.springframework.boot:spring-boot-starter-freemarker")
    implementation("org.springframework.boot:spring-boot-starter-mail")
    implementation("org.springframework.boot:spring-boot-starter-security")
    implementation("org.springframework.boot:spring-boot-starter-data-redis")
    implementation("org.springframework.boot:spring-boot-starter-amqp")
    implementation("org.springframework.boot:spring-boot-devtools")
    implementation("org.springframework.session:spring-session-core")
    implementation("org.springframework.session:spring-session-data-redis")
    implementation("org.springframework.retry:spring-retry")
    implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310")
    implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-xml")
    implementation("org.apache.commons:commons-lang3")
    implementation("org.codehaus.janino:janino")
    implementation("org.apache.tomcat:tomcat-jdbc")
    testImplementation("org.springframework.boot:spring-boot-starter-test")
    testImplementation("org.springframework.security:spring-security-test")
    testImplementation("org.yaml:snakeyaml")
    testImplementation("org.mockito:mockito-core")
    runtimeOnly("org.springframework.boot:spring-boot-properties-migrator")

    // dependency-management-plugin によりバージョン番号が自動で設定されないもの、あるいは最新バージョンを指定したいもの
    runtimeOnly("${jdbcDriver}")
    implementation("com.integralblue:log4jdbc-spring-boot-starter:1.0.2")
    implementation("org.simpleframework:simple-xml:2.7.1")
    implementation("com.univocity:univocity-parsers:2.7.5")
    implementation("com.google.guava:guava:26.0-jre")
    testImplementation("org.dbunit:dbunit:2.5.4")
    testImplementation("com.icegreen:greenmail:1.5.8")
    testImplementation("org.assertj:assertj-core:3.11.0")
    testImplementation("com.jayway.jsonpath:json-path:2.4.0")
    testImplementation("org.jsoup:jsoup:1.11.3")
    testImplementation("cglib:cglib-nodep:3.2.7")
    testImplementation("org.spockframework:spock-core:${spockVersion}")
    testImplementation("org.spockframework:spock-spring:${spockVersion}")
    testImplementation("com.google.code.findbugs:jsr305:3.0.2")

    // for lombok
    annotationProcessor("org.projectlombok:lombok:${lombokVersion}")
    compileOnly("org.projectlombok:lombok:${lombokVersion}")
    testCompileOnly("org.projectlombok:lombok:${lombokVersion}")

    // for Doma
    annotationProcessor("org.seasar.doma:doma:${domaVersion}")
    implementation("org.seasar.doma:doma:${domaVersion}")
    domaGenRuntime("org.seasar.doma:doma-gen:${domaVersion}")
    domaGenRuntime("${jdbcDriver}")

    // for Error Prone ( http://errorprone.info/ )
    errorprone("com.google.errorprone:error_prone_core:${errorproneVersion}")
    compileOnly("com.google.errorprone:error_prone_annotations:${errorproneVersion}")

    // PowerMock
    testImplementation("org.powermock:powermock-module-junit4:${powermockVersion}")
    testImplementation("org.powermock:powermock-api-mockito2:${powermockVersion}")
}

..........
  • 先頭に記述していた以下の2行は buildscript block の中の ext block に移動します。また version は 2.0.4-RELEASE に変更します。これにより buildscript block が一番上になります。
    • group 'ksbysample'
    • version '1.5.4-RELEASE'version '2.0.4-RELEASE'
  • buildscript block の以下の点を変更します。
    • ext block から springBootVersion = '1.5.4.RELEASE' を削除します。Spring Boot のバージョンは plugins block 内の org.springframework.boot プラグインで指定します。
    • repositories block 内で jcenter()mavenCentral() に変更します。
    • plugins block を書くように変更すると dependencies block は不要になるので削除します。
  • プラグインの書き方を apply plugin: ... から plugins block に変更します。ここは変更内容が大きいので詳細は上を見てください。Gradle plugin のバージョンは https://plugins.gradle.org/ で検索して調べます。
  • springBoot { buildInfo() } を追加します。
  • repositories block で jcenter()mavenCentral() に変更します。
  • dependencyManagement block の以下の点を変更します。
    • mavenBom("io.spring.platform:platform-bom:Brussels-SR3")mavenBom(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES) に変更します。
    • Spring Boot の BOM では guava は記述されていないので、bomProperty 'guava.version', '21.0' を削除します。
    • Thymeleaf 関連のライブラリは Spring Boot の BOM に最新バージョン(3.0.9.RELEASE等)が記述されているので、以下の5行を削除します。
      • bomProperty 'thymeleaf.version', '3.0.6.RELEASE'
      • bomProperty 'thymeleaf-extras-springsecurity4.version', '3.0.2.RELEASE'
      • bomProperty 'thymeleaf-layout-dialect.version', '2.2.2'
      • bomProperty 'thymeleaf-extras-data-attribute.version', '2.0.1'
      • bomProperty 'thymeleaf-extras-java8time.version', '3.0.0.RELEASE'
  • bootRepackage タスクは書かなくても動作するので削除します。
  • dependencies block の以下の点を変更します。
  • ライブラリのバージョンを最新にします。ライブラリの最新バージョンは主に https://mvnrepository.com/ で調べます(一部ライブラリの GitHub ページ等を見る必要あり)。
    • def jdbcDriver = "org.postgresql:postgresql:42.1.1"def jdbcDriver = "org.postgresql:postgresql:42.2.4"
    • def domaVersion = "2.16.1"def domaVersion = "2.19.3"
    • def lombokVersion = "1.16.16"def lombokVersion = "1.18.2"
    • def errorproneVersion = "2.0.15"def errorproneVersion = "2.3.1"
    • def powermockVersion = "1.6.6"def powermockVersion = "2.0.0-beta.5"
    • implementation("com.integralblue:log4jdbc-spring-boot-starter:1.0.1")implementation("com.integralblue:log4jdbc-spring-boot-starter:1.0.2")
    • implementation("com.univocity:univocity-parsers:2.4.1")implementation("com.univocity:univocity-parsers:2.7.5")
    • testImplementation("org.dbunit:dbunit:2.5.3")testImplementation("org.dbunit:dbunit:2.5.4")
    • testImplementation("com.icegreen:greenmail:1.5.5")testImplementation("com.icegreen:greenmail:1.5.8")
    • testImplementation("org.assertj:assertj-core:3.8.0")testImplementation("org.assertj:assertj-core:3.11.0")
    • testImplementation("com.jayway.jsonpath:json-path:2.2.0")testImplementation("com.jayway.jsonpath:json-path:2.4.0")
    • testImplementation("org.jsoup:jsoup:1.10.3")testImplementation("org.jsoup:jsoup:1.11.3")
    • testImplementation("cglib:cglib-nodep:3.2.5")testImplementation("cglib:cglib-nodep:3.2.7")
  • guava はバージョン番号を指定する必要があるので、以下のように変更します。
    • implementation("com.google.guava:guava")implementation("com.google.guava:guava:26.0-jre")
  • Spring Boot + npm + Geb で入力フォームを作ってテストする ( その73 )( Spring Boot を 1.5.14 → 2.0.4 へバージョンアップする ) の記事で実施した以下の点も反映します。
    • implementation("org.springframework.session:spring-session")implementation("org.springframework.session:spring-session-core") に変更します。
    • implementation("org.springframework.session:spring-session-data-redis") を追加します。
    • implementation("org.apache.tomcat:tomcat-jdbc") を追加します。
    • testImplementation("org.powermock:powermock-api-mockito:${powermockVersion}")testImplementation("org.powermock:powermock-api-mockito2:${powermockVersion}") に変更します。
    • runtimeOnly("org.springframework.boot:spring-boot-properties-migrator") を追加します。

変更後、Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新すると、特にエラーは出ずに更新されました。

Rebuild Project でエラーの出るクラスを修正する

clean タスク実行 → Rebuild Project 実行 をするとエラーが出るので修正します。

src/main/java/ksbysample/webapp/lending/config/ApplicationConfig.java

f:id:ksby:20180902213318p:plain

  • DataSourceBuilder クラスが import できずエラーが出ていました。import 文を削除した後、Alt+Enter を押して import し直します。

src/main/java/ksbysample/webapp/lending/config/WebMvcConfig.java

f:id:ksby:20180902212655p:plain

  • WebMvcConfigurerAdapter が非推奨になりました。WebMvcConfigurerAdapter クラスの JavaDoc の説明を読むと WebMvcConfigurer インターフェースに Java 8 のデフォルトメソッドで実装が追加されたので WebMvcConfigurer インターフェースを使用するよう記述されていました。extends WebMvcConfigurerAdapterimplements WebMvcConfigurer に変更します。
  • SpringTemplateEngine クラスが import できずエラーが出ていました。import 文を削除した後、Alt+Enter を押して import class し直します。

src/main/java/ksbysample/webapp/lending/config/WebSecurityConfig.java

f:id:ksby:20180902222047p:plain

  • 28. Security から @Order(SecurityProperties.ACCESS_OVERRIDE_ORDER) の記述が削除されていたので、おそらく無くなったものと思われます。@Order(SecurityProperties.ACCESS_OVERRIDE_ORDER) を削除します。
  • コンストラクタの引数 userDetailsService で赤波線が出ています。IntelliJ IDEA で原因を表示させると Autowired 可能な Bean が2種類以上あるので、その警告でした。 f:id:ksby:20180902223756p:plain コンストラクタの引数を UserDetailsService userDetailsService@Qualifier("lendingUserDetailsService") UserDetailsService userDetailsService に変更します。

src/main/java/ksbysample/webapp/lending/security/LendingUserDetailsService.java

エラーは出ていませんが、クラス名が "~Service" なのに @Component アノテーションなのは違和感があったので、@Component@Service アノテーションに変更します。

src/main/java/ksbysample/webapp/lending/web/WebappErrorController.java

f:id:ksby:20180902230106p:plain

  • ErrorController クラスが import できずエラーが出ていました。import 文を削除した後、Alt+Enter を押して import class し直します。

org.hibernate.validator.constraintsのorg.hibernate.validator.constraints.NotBlankは非推奨になりました

f:id:ksby:20180903022520p:plain

  • org.hibernate.validator.constraints.NotBlank は非推奨になったので javax.validation.constraints.NotBlank に変更します。import 文を削除した後、Alt+Enter で import し直します。
  • 修正したのは以下のソースです。
    • src/main/java/ksbysample/webapp/lending/web/springmvcmemo/EditFormChecker.java
    • src/main/java/ksbysample/webapp/lending/web/springmvcmemo/SendmailFormChecker.java

org.springframework.web.servlet.mvc.method.annotationのorg.springframework.web.servlet.mvc.method.annotation.AbstractJsonpResponseBodyAdviceは非推奨になりました

f:id:ksby:20180903023004p:plain

  • org.springframework.web.servlet.mvc.method.annotation.AbstractJsonpResponseBodyAdvice が非推奨になっていました。AbstractJsonpResponseBodyAdvice のソースを見ると、Spring Framework 5.1 で削除する予定なので CORS を使用するよう書かれています。JavascriptJSONP で呼んでいるところも修正する必要があるので、今は一旦無視します。

    f:id:ksby:20180903023158p:plain

build タスク実行時に出るエラーを修正する

An unhandled exception was thrown by the Error Prone static analysis plugin. @Data

f:id:ksby:20180904002622p:plain

error-prone により An unhandled exception was thrown by the Error Prone static analysis plugin. というメッセージが出て @Data アノテーションで引っかかりました。もう少しエラーが発生している詳細な原因が知りたいので、コマンドプロンプトから gradlew --stacktrace build コマンドを実行します。

f:id:ksby:20180904003108p:plain

引っかかっている BugPattern がすごく見やすくなっていました。いつの間に。。。 今回は ParameterName で引っかかっているようなので、これを無効にします。build.gradle を以下のように変更します。

[compileJava, compileTestGroovy, compileTestJava]*.options*.encoding = "UTF-8"
[compileJava, compileTestGroovy, compileTestJava]*.options*.compilerArgs = [
        '-Xlint:all,-options,-processing,-path'
        , '-Xep:RemoveUnusedImports:WARN'
        , '-Xep:InsecureCryptoUsage:OFF'
        , '-Xep:ParameterName:OFF'
]
  • [compileJava, compileTestGroovy, compileTestJava]*.options*.compilerArgs = [ ... ]compileJava.options.compilerArgs += [ ... ] に分けていたのを前者だけにまとめます。
  • '-Xep:ParameterName:OFF' を追加します。

警告:[deprecation] org.mockitoのMatchersは推奨されません

  • import static org.mockito.Matchers.any;Matchers が非推奨になっていたので、import 文を削除した後 Alt+Enter を押して import し直します。

警告:[deprecation] FilesのtoString(File,Charset)は推奨されません

f:id:ksby:20180905010116p:plain

Guava の Files#toString が非推奨になっていました。最近 Guava は使用しなくなる傾向にあるので(Spring Boot の BOM にも入っていません)、Spring Framework に入っている FileCopyUtils#copyToString に置き換えることにします。

例えば Files.toString(new File("src/test/resources/ksbysample/webapp/lending/helper/mail/assertdata/001/message.txt"), Charsets.UTF_8) は以下のように変更します。

  • フィールドに @Value("ksbysample/webapp/lending/helper/mail/assertdata/001/message.txt") ClassPathResource messageTxtResource; を追加する。
  • FileCopyUtils.copyToString(Files.newReader(messageTxtResource.getFile(), StandardCharsets.UTF_8))); に変更する。

f:id:ksby:20180905065522p:plain

修正したのは以下のソースです。

  • src/test/java/ksbysample/webapp/lending/helper/mail/Mail001HelperTest.java
  • src/test/java/ksbysample/webapp/lending/helper/mail/Mail002HelperTest.java
  • src/test/java/ksbysample/webapp/lending/helper/mail/Mail003HelperTest.java
  • src/test/java/ksbysample/webapp/lending/listener/rabbitmq/InquiringStatusOfBookQueueListenerTest.java
  • src/test/java/ksbysample/webapp/lending/web/confirmresult/ConfirmresultControllerTest.java
  • src/test/java/ksbysample/webapp/lending/web/lendingapp/LendingappControllerTest.java
  • src/test/java/ksbysample/webapp/lending/web/lendingapproval/LendingapprovalControllerTest.java

警告:[deprecation] MockMvcRequestBuildersのfileUpload(String,Object...)は推奨されません

f:id:ksby:20180905211554p:plain

MockMvcRequestBuilders#fileUpload が非推奨になっていたので、MockMvcRequestBuilders#multipart に置き換えます。修正したのは以下のソースです。

  • src/test/java/ksbysample/webapp/lending/web/booklist/BooklistControllerTest.java

次回は。。。

ここまでの変更ではまだ BUILD SUCCESSFUL は出ておらず、以下の問題が残っています。

  • 警告:[deprecation] org.springframework.web.servlet.mvc.method.annotationのAbstractJsonpResponseBodyAdviceは推奨されません が出ている。
  • FindBugs rule violations were found. See the report at: file:///D:/project-springboot/ksbysample-webapp-lending/build/reports/findbugs/main.html が出ている。
  • テストが4件失敗している。

f:id:ksby:20180905213339p:plain f:id:ksby:20180905213506p:plain

次回は AbstractJsonpResponseBodyAdvice の警告と、失敗しているテストを解消します。

履歴

2018/09/05
初版発行。
2019/02/10
* org.hibernate.validator.constraintsのorg.hibernate.validator.constraints.NotBlankは非推奨になりました の中の記述で org.hibernate.validator.constraints.NotEmpty → org.hibernate.validator.constraints.NotBlank、javax.validation.constraints.NotEmpty → javax.validation.constraints.NotBlank に修正しました。

Spring Boot + Spring Integration でいろいろ試してみる ( その26 )( MessageChannel の capacity を超えたメッセージを送信しようとするとどうなるのか? )

概要

記事一覧はこちらです。

MessageChannel には capacity (MessageChannel に格納できる Message の最大数)を設定できますが、Inbound Channel Adapter から取得した Message 数より capacity の数値が低い場合にどのような動作になるのか気になったので確認します。

参照したサイト・書籍

目次

  1. ksbysample-eipapp-channelcapacity プロジェクトを作成する
  2. 仕様を決める
  3. Application クラスを変更する
  4. Flyway 用の SQL ファイルを作成する
  5. application.properties に DB 接続用の設定を記述する
  6. ApplicationConfig クラスを作成する
  7. QueueSourceDto クラスを作成する
  8. FlowConfig クラスを作成する
  9. 動作確認
  10. 【検証】.transactional(this.transactionManager) が有効か確認する

手順

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

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

buildscript {
    ext {
        springBootVersion = '2.0.4.RELEASE'
    }
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
    }
}

apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'

group = 'ksbysample'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = 1.8

repositories {
    mavenCentral()
}

dependencies {
    def lombokVersion = "1.18.2"

    compile('org.springframework.boot:spring-boot-starter-integration')
    implementation('org.springframework.boot:spring-boot-starter-jdbc')
    implementation('org.springframework.integration:spring-integration-jdbc')
    testCompile('org.springframework.boot:spring-boot-starter-test')

    implementation("com.h2database:h2:1.4.192")
    implementation("org.flywaydb:flyway-core:5.1.4")

    // for lombok
    annotationProcessor("org.projectlombok:lombok:${lombokVersion}")
    compileOnly("org.projectlombok:lombok:${lombokVersion}")
}
  • Spring Integration の 19. JDBC Support の Inbound Channel Adapter の機能を使うので、以下の行を追加します。
    • implementation('org.springframework.integration:spring-integration-jdbc')
  • DataSource や PlatformTransactionManager も利用するので以下の行を追加します。
    • implementation('org.springframework.boot:spring-boot-starter-jdbc')
  • JDBC Inbound Channel Adapter のデータ取得元のテーブルを自動生成するために Flyway を入れます。Flyway は Appendix F. Dependency versions に記載があるのでバージョン番号を指定しなくても良いのですが、最新バージョンを使用したいので指定します。DB には H2 を使用します。
    • implementation("com.h2database:h2:1.4.192")
    • implementation("org.flywaydb:flyway-core:5.1.4")
  • DTO クラスを作成する時に @Data アノテーションを使いたいんで lombok も入れます。

仕様を決める

今回作成するサンプルは以下の仕様にします。

  • JDBC Inbound Channel Adapter から取得する Message の数を最大 5 にし、取得した後に送信する MessageChannel の capacity の数を 3 にする。
  • JDBC Inbound Channel Adapter からは 1秒毎に Message を取得する。
  • 送信先の MessageChannel からは 3秒毎に Message を取得する。取得する時の Message の最大数は 1 とする。
  • JDBC Inbound Channel Adapter の取得元のテーブル名を QUEUE_SOURCE とし、テーブルには 10レコード入れておく。

これで取得した5つの Message 全てを次の MessageChannel に送信できないという状況になります。

Application クラスを変更する

src/main/java/ksbysample/eipapp/channelcapacity/Application.java の以下の点を変更します。

package ksbysample.eipapp.channelcapacity;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.data.jpa.JpaRepositoriesAutoConfiguration;
import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration;
import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication(exclude = {JpaRepositoriesAutoConfiguration.class, HibernateJpaAutoConfiguration.class})
@EnableScheduling
public class Application {

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

}
  • MessageChannel に格納された Message の数を定期的にログに出力するのにスケジューリングの機能を利用したいので、@EnableScheduling アノテーションを付与します。
  • @SpringBootApplication@SpringBootApplication(exclude = {JpaRepositoriesAutoConfiguration.class, HibernateJpaAutoConfiguration.class}) に変更します。

Flyway 用の SQL ファイルを作成する

src/main/resources の下に db/migration ディレクトリを作成します。

src/main/resources/db/migration の下に V1__init.sql を新規作成し、以下の内容を記述します。

CREATE TABLE QUEUE_SOURCE
(
  seq INT NOT NULL,
  status INT NOT NULL,
  value VARCHAR(64) NOT NULL,
  CONSTRAINT PK_QUEUE_SOURCE PRIMARY KEY (seq)
);

INSERT INTO QUEUE_SOURCE VALUES (1, 0, 'その1');
INSERT INTO QUEUE_SOURCE VALUES (2, 0, 'その2');
INSERT INTO QUEUE_SOURCE VALUES (3, 0, 'その3');
INSERT INTO QUEUE_SOURCE VALUES (4, 0, 'その4');
INSERT INTO QUEUE_SOURCE VALUES (5, 0, 'その5');
INSERT INTO QUEUE_SOURCE VALUES (6, 0, 'その6');
INSERT INTO QUEUE_SOURCE VALUES (7, 0, 'その7');
INSERT INTO QUEUE_SOURCE VALUES (8, 0, 'その8');
INSERT INTO QUEUE_SOURCE VALUES (9, 0, 'その9');
INSERT INTO QUEUE_SOURCE VALUES (10, 0, 'その10');

application.properties に DB 接続用の設定を記述する

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

spring.datasource.hikari.jdbc-url=jdbc:h2:mem:channelcapacitydb
spring.datasource.hikari.username=sa
spring.datasource.hikari.password=
spring.datasource.hikari.driver-class-name=org.h2.Driver

ここでアプリケーションを1度起動してエラーが出ないことを確認しておきます。

f:id:ksby:20180901000732p:plain

Flyway.setCallbacks(FlywayCallback) has been deprecated and will be removed in Flyway 6.0. Use Flyway.setCallbacks(Callback) instead. というメッセージの WARN ログが出ていますが、stackoverflow に Flyway deprecation message logged when using Spring Boot 2 という記事がありました。FlywayAutoConfiguration のソースで Flyway.setCallbacks(FlywayCallback) が使用されていることが原因のようです。

ApplicationConfig クラスを作成する

src/main/java/ksbysampleveipapp/channelcapacity の下に ApplicationConfig.java を新規作成し、以下の内容を記述します。

package ksbysample.eipapp.channelcapacity;

import com.zaxxer.hikari.HikariDataSource;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableMBeanExport;
import org.springframework.jmx.support.RegistrationPolicy;

import javax.sql.DataSource;

@Configuration
@EnableMBeanExport(registration = RegistrationPolicy.IGNORE_EXISTING)
public class ApplicationConfig {

    @Bean
    @ConfigurationProperties("spring.datasource.hikari")
    public DataSource dataSource() {
        return DataSourceBuilder.create()
                .type(HikariDataSource.class)
                .build();
    }

}

QueueSourceDto クラスを作成する

src/main/java/ksbysampleveipapp/channelcapacity の下に QueueSourceDto.java を新規作成し、以下の内容を記述します。

package ksbysample.eipapp.channelcapacity;

import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
public class QueueSourceDto {

    private int seq;

    private int status;

    private String value;

}

FlowConfig クラスを作成する

src/main/java/ksbysampleveipapp/channelcapacity の下に FlowConfig.java を新規作成し、以下の内容を記述します。

jdbcMessageSource → selectDbFlow → dstChannel → getDstChannelFlow の順で Message が流れます。

package ksbysample.eipapp.channelcapacity;

import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.integration.channel.NullChannel;
import org.springframework.integration.channel.QueueChannel;
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.dsl.channel.MessageChannels;
import org.springframework.integration.handler.LoggingHandler;
import org.springframework.integration.jdbc.JdbcPollingChannelAdapter;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.transaction.PlatformTransactionManager;

import javax.sql.DataSource;

@Slf4j
@Configuration
public class FlowConfig {

    private final DataSource dataSource;

    private final PlatformTransactionManager transactionManager;

    private final NullChannel nullChannel;

    public FlowConfig(DataSource dataSource
            , PlatformTransactionManager transactionManager
            , NullChannel nullChannel) {
        this.dataSource = dataSource;
        this.transactionManager = transactionManager;
        this.nullChannel = nullChannel;
    }

    @Bean
    public MessageSource<Object> jdbcMessageSource() {
        JdbcPollingChannelAdapter adapter
                = new JdbcPollingChannelAdapter(this.dataSource
                , "select * from QUEUE_SOURCE where status = 0");
        adapter.setRowMapper(new BeanPropertyRowMapper<>(QueueSourceDto.class));
        adapter.setUpdateSql("update QUEUE_SOURCE set status = 1 where seq in (:seq)");
        return adapter;
    }

    @Bean
    public IntegrationFlow selectDbFlow() {
        return IntegrationFlows.from(jdbcMessageSource()
                , e -> e.poller(Pollers
                        .fixedDelay(1000)
                        .maxMessagesPerPoll(5)
                        .transactional(this.transactionManager)))
                // 取得したデータは List 形式で全件 payload にセットされているので、split で1payload1データに分割する
                .split()
                .<QueueSourceDto>log(LoggingHandler.Level.WARN, m -> "☆☆☆ " + m.getPayload().getSeq())
                .channel(dstChannel())
                .get();
    }

    @Bean
    public QueueChannel dstChannel() {
        return MessageChannels.queue(3).get();
    }

    @Bean
    public IntegrationFlow getDstChannelFlow() {
        return IntegrationFlows.from(dstChannel())
                .bridge(e -> e.poller(Pollers
                        .fixedDelay(3000)
                        .maxMessagesPerPoll(1)))
                .<QueueSourceDto>log(LoggingHandler.Level.ERROR, m -> "★★★ " + m.getPayload().getSeq())
                .channel(nullChannel)
                .get();
    }

    @Scheduled(initialDelay = 1000, fixedDelay = 1000)
    public void checkDstChannel() {
        log.info("dstChannel().getQueueSize() = " + dstChannel().getQueueSize());
    }

}

動作確認

アプリケーションを実行してみます。

f:id:ksby:20180901055429p:plain

取得したログを見ると以下のように処理されるようです。

  • dstChannel に capacity 分の Message が溜まっていると送信する部分の処理(.channel(dstChannel()))で止まり、JDBC Inbound Channel Adapter からデータを取りません。
  • JDBC Inbound Channel Adapter から最初は .maxMessagesPerPoll(5) で指定した5件取得しますが、その後は5件から減った分の1件しか取得しません。常に5件ずつ取得する訳ではありませんでした。

【検証】.transactional(this.transactionManager) が有効か確認する

※ここから先のコードはコミットしません。

JDBC Inbound Channel Adapter からデータを取得する時に e -> e.poller(Pollers.fixedDelay(1000).maxMessagesPerPoll(5).transactional(this.transactionManager)).transactional(this.transactionManager) を付けてトランザクションを有効にしていますが、本当に効いているのか確認してみます。

dstChannel に Message を送信する時にタイムアウト時間を設定して、タイムアウトした時には RuntimeException を throw するようにします。src/main/java/ksbysampleveipapp/channelcapacity/FlowConfig.java を以下のように変更します。

    @Bean
    public IntegrationFlow selectDbFlow() {
        return IntegrationFlows.from(jdbcMessageSource()
                , e -> e.poller(Pollers
                        .fixedDelay(1000)
                        .maxMessagesPerPoll(5)
                        .transactional(this.transactionManager)))
                // 取得したデータは List 形式で全件 payload にセットされているので、split で1payload1データに分割する
                .split()
                .<QueueSourceDto>log(LoggingHandler.Level.WARN, m -> "☆☆☆ " + m.getPayload().getSeq())
                // .channel(dstChannel())
                .handle((p, h) -> {
                    dstChannel().send(new GenericMessage<>(p, h), 2000);
                    return null;
                })
                .get();
    }

    static class DstChannelInterceptor implements ChannelInterceptor {
        @Override
        public void postSend(Message<?> message, MessageChannel channel, boolean sent) {
            // postSend メソッドは MessageChannel に Message を送信した後に呼び出される
            // 引数の sent は Message の送信に成功すると true が、失敗すると false がセットされるので、
            // false の時に RuntimeException を throw させる
            if (!sent) {
                throw new RuntimeException("Can't send message.");
            }
        }
    }

    @Bean
    public QueueChannel dstChannel() {
        QueueChannel queueChannel = MessageChannels.queue(3).get();
        queueChannel.addInterceptor(new DstChannelInterceptor());
        return queueChannel;
    }
  • ChannelInterceptor インターフェースの実装クラス DstChannelInterceptor を追加します。
  • dstChannel Bean に queueChannel.addInterceptor(new DstChannelInterceptor()); を追加します。
  • selectDbFlow Bean の dstChannel へのメッセージを送信処理を .channel(dstChannel()).handle((p, h) -> { dstChannel().send(new GenericMessage<>(p, h), 2000); return null; }) に変更して、取得される時間(3秒)より短い時間(2秒)でタイムアウトさせます。

アプリケーションを実行してみます。

f:id:ksby:20180901092115p:plain f:id:ksby:20180901092236p:plain

最低でも dstChannel で1件目が処理されていますが、RuntimeException が発生すれると1件目から取り直されていました。adapter.setUpdateSql("update QUEUE_SOURCE set status = 1 where seq in (:seq)"); で update されたのが commit されていないようです。どのような SQL が実行されているか確認してみます。

log4jdbc を入れるので、build.gradle に implementation("com.integralblue:log4jdbc-spring-boot-starter:1.0.2") を追加します。

dependencies {
    def lombokVersion = "1.18.2"

    compile('org.springframework.boot:spring-boot-starter-integration')
    implementation('org.springframework.boot:spring-boot-starter-jdbc')
    implementation('org.springframework.integration:spring-integration-jdbc')
    testCompile('org.springframework.boot:spring-boot-starter-test')

    implementation("com.h2database:h2:1.4.192")
    implementation("org.flywaydb:flyway-core:5.1.4")
    implementation("com.integralblue:log4jdbc-spring-boot-starter:1.0.2")

    // for lombok
    annotationProcessor("org.projectlombok:lombok:${lombokVersion}")
    compileOnly("org.projectlombok:lombok:${lombokVersion}")
}

src/main/properties/application.properties に log4jdbc の設定を追加します。

spring.datasource.hikari.jdbc-url=jdbc:h2:mem:channelcapacitydb
spring.datasource.hikari.username=sa
spring.datasource.hikari.password=
spring.datasource.hikari.driver-class-name=org.h2.Driver
spring.datasource.hikari.register-mbeans=true

# log4jdbc-log4j2
logging.level.jdbc.sqlonly=DEBUG
logging.level.jdbc.sqltiming=INFO
logging.level.jdbc.audit=INFO
logging.level.jdbc.resultset=ERROR
logging.level.jdbc.resultsettable=ERROR
logging.level.jdbc.connection=DEBUG

アプリケーションを実行してみます。

f:id:ksby:20180901094620p:plain

.maxMessagesPerPoll(5) を指定しているので5件しか update していないと思っていましたが、テーブル内にある10件全て取得して update されていました。なるほどそんな動作をするのか。。。

また RuntimeException が発生しているので rollback もされていました。.transactional(this.transactionManager) は有効でした。

dstChannel で処理した分は commit して処理し直されないようにしてみます。src/main/java/ksbysampleveipapp/channelcapacity/FlowConfig.java を以下のように変更します。

    @Bean
    public MessageSource<Object> jdbcMessageSource() {
        JdbcPollingChannelAdapter adapter
                = new JdbcPollingChannelAdapter(this.dataSource
                , "select * from QUEUE_SOURCE where status = 0");
        adapter.setRowMapper(new BeanPropertyRowMapper<>(QueueSourceDto.class));
        adapter.setUpdateSql("update QUEUE_SOURCE set status = 1 where seq in (:seq)");
        adapter.setMaxRowsPerPoll(1);
        return adapter;
    }

    @Bean
    public IntegrationFlow selectDbFlow() {
        return IntegrationFlows.from(jdbcMessageSource()
                , e -> e.poller(Pollers
                        .fixedDelay(1000)
                        .maxMessagesPerPoll(1)
                        .transactional(this.transactionManager)))
                // 取得したデータは List 形式で全件 payload にセットされているので、split で1payload1データに分割する
                .split()
                .<QueueSourceDto>log(LoggingHandler.Level.WARN, m -> "☆☆☆ " + m.getPayload().getSeq())
                // .channel(dstChannel())
                .handle((p, h) -> {
                    dstChannel().send(new GenericMessage<>(p, h), 2000);
                    return null;
                })
                .get();
    }
  • jdbcMessageSource Bean の処理に adapter.setMaxRowsPerPoll(1); を追加します。
  • selectDbFlow Bean の処理で、.maxMessagesPerPoll(5).maxMessagesPerPoll(1) に変更します。

これで1件毎に commit されるはずです。アプリケーションを実行してみます。

f:id:ksby:20180901100612p:plain

1件目は dstChannel に Message を送信したら commit されていました。

f:id:ksby:20180901100840p:plain f:id:ksby:20180901101157p:plain

7件目でタイムアウトが発生しましたが、次に取り直されたのは7件目でした。

結論としては、以下のような考慮が必要になるものと思われます。

  • JdbcPollingChannelAdapter のデータ取得元のテーブルに件数がある場合には、JdbcPollingChannelAdapter#setMaxRowsPerPoll による取得件数の設定は必須。
  • 取得した後の処理ですぐに split しない方が多分良さそうです。
  • rollback が発生した時のことを考慮して(どこから処理をやり直してよいのか?)、MessageSource から取得するデータ件数、送信先の MessageChannel の capacity や MessageChannel から取得する件数等を考える必要があります。

履歴

2018/09/01
初版発行。