かんがるーさんの日記

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

Spring Boot 1.3.x の Web アプリを 1.4.x へバージョンアップする ( その4 )( build.gradle 修正後の Rebuild で出た Warning を解消する )

概要

記事一覧はこちらです。

Spring Boot 1.3.x の Web アプリを 1.4.x へバージョンアップする ( その3 )( build.gradle の修正 ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • build.gradle 修正後の Rebuild Project 実行時に出た Warning の解消
    • Velocity は Thymeleaf 3 か FreeMarker に変更しようと思っています。他の Warning を解消してから次回以降に変更します。今回は何もしません。

参照したサイト・書籍

目次

  1. java: com.univocity.parsers.common.CommonParserSettingsのsetRowProcessor(com.univocity.parsers.common.processor.RowProcessor)は非推奨になりました
  2. java: org.springframework.boot.testのorg.springframework.boot.test.SpringApplicationConfigurationは非推奨になりました
  3. 次回は。。。

手順

java: com.univocity.parsers.common.CommonParserSettingsのsetRowProcessor(com.univocity.parsers.common.processor.RowProcessor)は非推奨になりました

uniVocity-parsers の JavaDoc で CsvParserSettings#setRowProcessor の説明を見ると “Use the setProcessor(Processor) method …” と記述されていました。

Tutorials はまだ CsvParserSettings#setRowProcessor が使用されたままでしたが、GitHub のテストコードを見ると univocity-parsers/src/test/java/com/univocity/parsers/common/processor/AnnotatedBeanProcessorTest.java で CsvParserSettings#setProcessor が使用されていました。CsvParserSettings#setRowProcessor → CsvParserSettings#setProcessor に変更すればよいだけのようです。

以下のソース内の .setRowProcessor.setProcessor に変更します。

  • src/main/java/ksbysample/webapp/lending/service/file/BooklistCsvFileService.java

java: org.springframework.boot.testのorg.springframework.boot.test.SpringApplicationConfigurationは非推奨になりました

Spring Boot 1.4 Release Notes を見ると “From @SpringApplicationConfiguration(classes=MyConfig.class) to @SpringBootTest(classes=MyConfig.class)” と記述されていますので、テストコード内の @SpringApplicationConfiguration@SpringBootTest へ変更します。

Ctrl+Shift+R を押して「Replace in Path」ダイアログを表示して置換します。

f:id:ksby:20170211084737p:plain

以下のダイアログが表示されたら「All Files」ボタンをクリックします。

f:id:ksby:20170211085717p:plain

これだけでは import 文が修正されませんので、「Replace in Path」ダイアログで import org.springframework.boot.test.SpringApplicationConfiguration;import org.springframework.boot.test.context.SpringBootTest; へ変更します。

f:id:ksby:20170211085933p:plain

最後に Project Tool Window で src/test を選択した後、Ctrl+Alt+O を押して「Optimize Imports」ダイアログを表示して「Run」ボタンを押して import 文を最適化します。最適化した結果を見て気づきましたが、不要な import 文を消していないソースが結構ありました。。。

clean タスク実行 → Rebuild Project を実行して Velocity 以外の Warning が消えていることを確認します。

f:id:ksby:20170211091141p:plain

次回は。。。

java: org.springframework.ui.velocityのorg.springframework.ui.velocity.VelocityEngineUtilsは非推奨になりました の対応として、Velocity を Thymeleaf 3 か FreeMarker に変更する予定です。

履歴

2017/02/11
初版発行。  

Spring Boot 1.3.x の Web アプリを 1.4.x へバージョンアップする ( その3 )( build.gradle の修正 )

概要

記事一覧はこちらです。

Spring Boot 1.3.x の Web アプリを 1.4.x へバージョンアップする ( その2 )( IntelliJ IDEA の Gradle Tool Window の「Refresh all Gradle projects」を押してもエラーが出ないようにする ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • build.gradle の修正

参照したサイト・書籍

  1. gradle 2.12 で対応された compileOnly を試す
    http://qiita.com/or_die/items/709b5c37ff34ee2839b3

  2. gradleのcompileOnlyで指定したライブラリはtestCompileに引き継がれない
    http://qiita.com/gillax/items/1cbcff003384087f2db1

  3. 3.Maven 入門 (2)
    http://www.techscore.com/tech/Java/ApacheJakarta/Maven/3/

    • maven の provided scope を調べた時に参照しました。

目次

  1. Spring Initializr で 1.4.4 のプロジェクトを作成する
  2. build.gradle を修正して build してみる
  3. 次回は。。。

手順

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

  1. 「Welcome to IntelliJ IDEA」ダイアログで「Create New Project」をクリックします。

    f:id:ksby:20170210002217p:plain

  2. 「New Project」ダイアログが表示されます。画面左側のリストから「Spring Initializr」を選択した後、「Next」ボタンをクリックします。

    f:id:ksby:20170210002435p:plain

  3. 次の画面が表示されます。「Type」で「Gradle Project」を選択した後、「Next」ボタンをクリックします。

    f:id:ksby:20170210002709p:plain

  4. 次の画面が表示されます。画面中央上の「Spring Boot」で「1.4.4」を選択してから ksbysample-webapp-lending プロジェクトで使用している以下の項目をチェックした後、「Next」ボタンをクリックします。

    f:id:ksby:20170210003303p:plain f:id:ksby:20170210003427p:plain f:id:ksby:20170210003552p:plain f:id:ksby:20170210003727p:plain f:id:ksby:20170210003928p:plain

  5. 次の画面が表示されます。「Project location」を “C:\project-springboot\demo” に変更した後、「Finish」ボタンをクリックします。

    f:id:ksby:20170210004339p:plain

  6. 「Import Module from Gradle」ダイアログが表示されます。「Create directories for empty content roots automatically」をチェックした後、「OK」ボタンをクリックします。

    f:id:ksby:20170210004636p:plain

これでプロジェクトが作成されて以下の build.gradle が作成されました。

buildscript {
    ext {
        springBootVersion = '1.4.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'

jar {
    baseName = 'demo'
    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-mail')
    compile('org.springframework.boot:spring-boot-starter-security')
    compile('org.springframework.session:spring-session')
    compile('org.springframework.boot:spring-boot-starter-thymeleaf')
    compile('org.springframework.boot:spring-boot-starter-web')
    runtime('org.springframework.boot:spring-boot-devtools')
    compileOnly('org.projectlombok:lombok')
    testCompile('org.springframework.boot:spring-boot-starter-test')
}

以下の点が 1.3.5 で生成した時とは違っていました。これらは反映したいと思います。

  • 以前は apply plugin: 'spring-boot' でしたが apply plugin: 'org.springframework.boot' に変わっています。
  • 以前は入っていた eclipse { ... } の記述がなくなっています。
  • lombok は compile(...) ではなく compileOnly(...) になっています。maven の provided scope に対応したものだそうです。

build.gradle を修正して build してみる

  1. build.gradle を リンク先のその1の内容 に変更します。

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

    Gradle DSL method not found: 'compileOnly()' のエラーが表示されました。

    f:id:ksby:20170210022345p:plain

  3. 先程 Spring Initializr で作成したプロジェクトと ksbysample-webapp-lending プロジェクトの gradle のバージョンを gradle/wrapper/gradle-wrapper.properties を見て比較すると、前者が gradle-2.13-bin.zip、後者が gradle-2.2-bin.zip でした。

    compileOnly が追加されたのが 2.12 からなので、gradle のバージョンが低いことが原因のようです。

  4. gradle をバージョンアップします。最初に build.gradle を リンク先のその2の内容 に変更します。

  5. gradlew wrapper コマンドを実行します。が、Spring Boot plugin's support for Gradle 2.2 is deprecated. Please upgrade to Gradle 2.9 or later. と出力されてエラーになりました。

    f:id:ksby:20170210024110p:plain

    これは一旦 build.gradle を元に戻して gradle を先にバージョンアップした方がよさそうですね。。。

  6. 現在の build.gradle の内容を別のテキストファイルに退避し、Git でソースの変更を破棄します。

  7. 変更前の build.gradle に対して リンク先のその2の内容 の変更を反映した後、gradlew wrapper コマンドを実行します。

    今度は成功しました。gradle/wrapper/gradle-wrapper.properties を見ると gradle-2.13-bin.zip になっていました。

    f:id:ksby:20170210030459p:plain

  8. 再び build.gradle を リンク先のその1の内容 に変更します。リンク先のその2の内容 も反映します。

  9. Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。今度はエラー等は出ずに終了しました。

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

    Error が 0個、Warning が 37 個出ました。Warning は以下の3種類でした。

    • java: org.springframework.ui.velocityのorg.springframework.ui.velocity.VelocityEngineUtilsは非推奨になりました
    • java: com.univocity.parsers.common.CommonParserSettingsのsetRowProcessor(com.univocity.parsers.common.processor.RowProcessor)は非推奨になりました
    • java: org.springframework.boot.testのorg.springframework.boot.test.SpringApplicationConfigurationは非推奨になりました

    Velocity は Spring Boot 1.5 からサポートされなくなると聞いていましたが、1.4 から非推奨ですか。。。 あとは uniVocity-parsers で RowProcessor が非推奨になったのと、1.4 からテスト用のアノテーションが大きく変更されているようなのでその影響による Warning ですね。

  11. build タスクを実行します。

    テストの結果は 178 tests completed, 165 failed, 8 skipped で、BUILD FAILED が表示されました。Caused by: java.lang.ClassNotFoundException が大量に出力されているのが気になります。

    f:id:ksby:20170211010128p:plain

  12. Project Tool Window の src/test から「Run ‘All Tests’ with Coverage」も実行してみます。

    build タスクの結果から分かっていましたが、ほぼ全滅でした。。。 また build タスクで大量に出ていた ClassNotFoundException ですが、java.lang.NoClassDefFoundError: org/thymeleaf/dialect/IExpressionObjectDialect が原因のようです。

    f:id:ksby:20170211010819p:plain

次回は。。。

1.2.x → 1.3.x へバージョンアップした時と同様に、Rebuild Project 実行時の Warning の解消 → Run ‘All Tests’ with Coverage のエラーの解消 → build タスクの再実行 ( エラーが出れば解消します ) の順で進める予定です。

Velocity の非推奨の対応が悩みどころです。Thymeleaf の 3 から TEXT モードがあるので、これでしょうか。Web で検索したら migration guide がヒットしたのでちょっと読んでみたいと思います。Velocity の対応は後回しにして他の部分だけ進めるかもしれません。

ソースコード

build.gradle

■その1

buildscript {
    ext {
        springBootVersion = '1.4.4.RELEASE'
    }
    repositories {
        jcenter()
        maven { url "http://repo.spring.io/repo/" }
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
        classpath("io.spring.gradle:dependency-management-plugin:0.6.1.RELEASE")
        // for Grgit
        classpath("org.ajoberstar:grgit:1.8.0")
        // Gradle Download Task
        classpath("de.undercouch:gradle-download-task:3.2.0")
    }
}

apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'idea'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'
apply plugin: 'de.undercouch.download'
apply plugin: 'groovy'

sourceCompatibility = 1.8
targetCompatibility = 1.8

[compileJava, compileTestGroovy, compileTestJava]*.options*.compilerArgs = ['-Xlint:all,-options,-processing']

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

jar {
    baseName = 'ksbysample-webapp-lending'
    version = '1.1.0-RELEASE'
}

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

configurations {
    domaGenRuntime
}

repositories {
    jcenter()
}

dependencyManagement {
    imports {
        mavenBom 'io.spring.platform:platform-bom:Athens-SR3'
    }
}

bootRepackage {
    mainClass = 'ksbysample.webapp.lending.Application'
    excludeDevtools = true
}

dependencies {
    def jdbcDriver = "org.postgresql:postgresql:9.4.1212"

    // dependency-management-plugin によりバージョン番号が自動で設定されるもの
    // Appendix A. Dependency versions ( http://docs.spring.io/platform/docs/current/reference/htmlsingle/#appendix-dependency-versions ) 参照
    compile("org.springframework.boot:spring-boot-starter-web")
    compile("org.springframework.boot:spring-boot-starter-thymeleaf")
    compile("org.thymeleaf.extras:thymeleaf-extras-springsecurity4")
    compile("org.springframework.boot:spring-boot-starter-data-jpa")
    compile("org.springframework.boot:spring-boot-starter-velocity")
    compile("org.springframework.boot:spring-boot-starter-mail")
    compile("org.springframework.boot:spring-boot-starter-security")
    compile("org.springframework.boot:spring-boot-starter-redis")
    compile("org.springframework.boot:spring-boot-starter-amqp")
    compile("org.springframework.boot:spring-boot-devtools")
    compile("org.springframework.session:spring-session")
    compile("org.codehaus.janino:janino")
    compile("com.fasterxml.jackson.datatype:jackson-datatype-jsr310")
    compile("com.fasterxml.jackson.dataformat:jackson-dataformat-xml")
    testCompile("org.springframework.boot:spring-boot-starter-test")
    testCompile("org.springframework.security:spring-security-test")
    testCompile("org.yaml:snakeyaml")
    testCompile("org.spockframework:spock-core") {
        exclude module: "groovy-all"
    }
    testCompile("org.spockframework:spock-spring") {
        exclude module: "groovy-all"
    }

    // dependency-management-plugin によりバージョン番号が自動で設定されないもの、あるいは最新バージョンを指定したいもの
    runtime("${jdbcDriver}")
    compile("org.seasar.doma:doma:2.15.0")
    compile("org.bgee.log4jdbc-log4j2:log4jdbc-log4j2-jdbc4.1:1.16")
    compile("org.apache.commons:commons-lang3:3.5")
    compile("com.google.guava:guava:21.0")
    compile("org.simpleframework:simple-xml:2.7.1")
    compile("com.univocity:univocity-parsers:2.3.1")
    compile("org.thymeleaf.extras:thymeleaf-extras-java8time:3.0.0.RELEASE")
    testCompile("org.dbunit:dbunit:2.5.3")
    testCompile("com.icegreen:greenmail:1.5.3")
    testCompile("org.assertj:assertj-core:3.6.2")
    testCompile("com.jayway.jsonpath:json-path:2.2.0")
    testCompile("org.jmockit:jmockit:1.30")

    // for lombok
    compileOnly("org.projectlombok:lombok:1.16.12")
    testCompileOnly("org.projectlombok:lombok:1.16.12")

    // for Doma-Gen
    domaGenRuntime("org.seasar.doma:doma-gen:2.15.0")
    domaGenRuntime("${jdbcDriver}")
}

bootRun {
    jvmArgs = ['-Dspring.profiles.active=develop']
}

test {
    jvmArgs = ['-Dspring.profiles.active=unittest']
}

// for Doma-Gen
task domaGen << {
    // まず変更が必要なもの
    def rootPackageName  = 'ksbysample.webapp.lending'
    def daoPackagePath   = 'src/main/java/ksbysample/webapp/lending/dao'
    def dbUrl            = 'jdbc:postgresql://localhost/ksbylending'
    def dbUser           = 'ksbylending_user'
    def dbPassword       = 'xxxxxxxx'
    def tableNamePattern = '.*'
    // おそらく変更不要なもの
    def importOfComponentAndAutowiredDomaConfig = "${rootPackageName}.util.doma.ComponentAndAutowiredDomaConfig"
    def workDirPath      = 'work'
    def workDaoDirPath   = "${workDirPath}/dao"

    // 作業用ディレクトリを削除する
    clearDir("${workDirPath}")

    // 現在の Dao インターフェースのバックアップを取得する
    copy() {
        from "${daoPackagePath}"
        into "${workDaoDirPath}/org"
    }

    // Dao インターフェース、Entity クラスを生成する
    ant.taskdef(resource: 'domagentask.properties',
            classpath: configurations.domaGenRuntime.asPath)
    ant.gen(url: "${dbUrl}", user: "${dbUser}", password: "${dbPassword}", tableNamePattern: "${tableNamePattern}") {
        entityConfig(packageName: "${rootPackageName}.entity", useListener: false)
        daoConfig(packageName: "${rootPackageName}.dao")
        sqlConfig()
    }

    // 生成された Dao インターフェースを作業用ディレクトリにコピーし、
    // @ComponentAndAutowiredDomaConfig アノテーションを付加する
    copy() {
        from "${daoPackagePath}"
        into "${workDaoDirPath}/replace"
        filter {
            line -> line.replaceAll('import org.seasar.doma.Dao;', "import ${importOfComponentAndAutowiredDomaConfig};\nimport org.seasar.doma.Dao;")
                    .replaceAll('@Dao', '@Dao\n@ComponentAndAutowiredDomaConfig')
        }
    }

    // @ComponentAndAutowiredDomaConfig アノテーションを付加した Dao インターフェースを
    // dao パッケージへ戻す
    copy() {
        from "${workDaoDirPath}/replace"
        into "${daoPackagePath}"
    }

    // 元々 dao パッケージ内にあったファイルを元に戻す
    copy() {
        from "${workDaoDirPath}/org"
        into "${daoPackagePath}"
    }

    // 作業用ディレクトリを削除する
    clearDir("${workDirPath}")

    // 自動生成したファイルを git add する
    addGit()
}

task downloadCssFontsJs << {
    def staticDirPath   = 'src/main/resources/static'
    def workDirPath     = 'work'
    def adminLTEVersion     = '2.2.0'
    def jQueryVersion       = '2.1.4'
    def fontAwesomeVersion  = '4.3.0'
    def ioniconsVersion     = '2.0.1'
    def html5shivJsVersion  = '3.7.2'
    def respondMinJsVersion = '1.4.2'

    // 作業用ディレクトリを削除する
    clearDir("${workDirPath}")

    // Bootstrap & AdminLTE Dashboard & Control Panel Template
    downloadAdminLTE("${adminLTEVersion}", "${jQueryVersion}", "${workDirPath}", "${staticDirPath}")

    // Font Awesome Icons
    downloadFontAwesome("${fontAwesomeVersion}", "${workDirPath}", "${staticDirPath}")

    // Ionicons
    downloadIonicons("${ioniconsVersion}", "${workDirPath}", "${staticDirPath}")

    // html5shiv.js
    downloadHtml5shivJs("${html5shivJsVersion}", "${workDirPath}", "${staticDirPath}")

    // respond.min.js
    downloadRespondMinJs("${respondMinJsVersion}", "${workDirPath}", "${staticDirPath}")

    // fileinput.min.js ( v4.2.7 )
    downloadBootstrapFileInputMinJs("${workDirPath}", "${staticDirPath}")

    // 作業用ディレクトリを削除する
    clearDir("${workDirPath}")

    // 追加したファイルを git add する
    addGit()
}

task printClassWhatNotMakeTest << {
    def srcDir = new File("src/main/java");
    def excludePaths = [
            "src/main/java/ksbysample/webapp/lending/Application.java"
            , "src/main/java/ksbysample/webapp/lending/config"
            , "src/main/java/ksbysample/webapp/lending/cookie"
            , "src/main/java/ksbysample/webapp/lending/dao"
            , "src/main/java/ksbysample/webapp/lending/entity"
            , "src/main/java/ksbysample/webapp/lending/exception"
            , "src/main/java/ksbysample/webapp/lending/helper/download/booklistcsv"
            , "src/main/java/ksbysample/webapp/lending/helper/download/DataDownloadHelper.java"
            , "src/main/java/ksbysample/webapp/lending/helper/page/PagenationHelper.java"
            , "src/main/java/ksbysample/webapp/lending/security/LendingUser.java"
            , "src/main/java/ksbysample/webapp/lending/security/RoleAwareAuthenticationSuccessHandler.java"
            , "src/main/java/ksbysample/webapp/lending/service/calilapi/response"
            , "src/main/java/ksbysample/webapp/lending/service/file/BooklistCSVRecord.java"
            , "src/main/java/ksbysample/webapp/lending/service/openweathermapapi"
            , "src/main/java/ksbysample/webapp/lending/service/queue/InquiringStatusOfBookQueueMessage.java"
            , "src/main/java/ksbysample/webapp/lending/util/doma"
            , "src/main/java/ksbysample/webapp/lending/util/velocity/VelocityUtils.java"
            , "src/main/java/ksbysample/webapp/lending/values/validation/ValuesEnum.java"
            , "src/main/java/ksbysample/webapp/lending/view/BookListCsvView.java"
            , "src/main/java/ksbysample/webapp/lending/web/.+/.+Service.java"
            , "src/main/java/ksbysample/webapp/lending/webapi/common/CommonWebApiResponse.java"
            , "src/main/java/ksbysample/webapp/lending/webapi/weather"
    ];
    def excludeFileNamePatterns = [
            ".*EventListener.java"
            , ".*Dto.java"
            , ".*Form.java"
            , ".*Values.java"
    ];

    compareSrcAndTestDir(srcDir, excludePaths, excludeFileNamePatterns);
}

/* -----------------------------------------------------------------------------
 * メソッド定義部
 ---------------------------------------------------------------------------- */
void clearDir(String dirPath) {
    delete dirPath
}

void addGit() {
    def grgit = org.ajoberstar.grgit.Grgit.open(dir: project.projectDir)
    grgit.add(patterns: ['.'])
}

void downloadAdminLTE(String adminLTEVersion, String jQueryVersion, String workDirPath, String staticDirPath) {
    download {
        src "https://codeload.github.com/almasaeed2010/AdminLTE/zip/v${adminLTEVersion}"
        dest new File("${workDirPath}/download/AdminLTE-${adminLTEVersion}.zip")
    }
    copy {
        from zipTree("${workDirPath}/download/AdminLTE-${adminLTEVersion}.zip")
        into "${workDirPath}/unzip"
    }
    copy {
        from "${workDirPath}/unzip/AdminLTE-${adminLTEVersion}/bootstrap/css"
        into "${staticDirPath}/css"
    }
    copy {
        from "${workDirPath}/unzip/AdminLTE-${adminLTEVersion}/bootstrap/fonts"
        into "${staticDirPath}/fonts"
    }
    copy {
        from "${workDirPath}/unzip/AdminLTE-${adminLTEVersion}/bootstrap/js"
        into "${staticDirPath}/js"
    }
    copy {
        from "${workDirPath}/unzip/AdminLTE-${adminLTEVersion}/dist/css"
        into "${staticDirPath}/css"
    }
    copy {
        from "${workDirPath}/unzip/AdminLTE-${adminLTEVersion}/dist/js"
        into "${staticDirPath}/js"
    }
    copy {
        from "${workDirPath}/unzip/AdminLTE-${adminLTEVersion}/plugins/jQuery/jQuery-${jQueryVersion}.min.js"
        into "${staticDirPath}/js"
    }
    delete "${staticDirPath}/js/pages"
    delete "${staticDirPath}/js/demo.js"
}

void downloadFontAwesome(String fontAwesomeVersion, String workDirPath, String staticDirPath) {
    download {
        src "http://fortawesome.github.io/Font-Awesome/assets/font-awesome-${fontAwesomeVersion}.zip"
        dest new File("${workDirPath}/download/font-awesome-${fontAwesomeVersion}.zip")
    }
    copy {
        from zipTree("${workDirPath}/download/font-awesome-${fontAwesomeVersion}.zip")
        into "${workDirPath}/unzip"
    }
    copy {
        from "${workDirPath}/unzip/font-awesome-${fontAwesomeVersion}/css/font-awesome.min.css"
        into "${staticDirPath}/css"
    }
    copy {
        from "${workDirPath}/unzip/font-awesome-${fontAwesomeVersion}/fonts"
        into "${staticDirPath}/fonts"
    }
}

void downloadIonicons(String ioniconsVersion, String workDirPath, String staticDirPath) {
    download {
        src "https://codeload.github.com/driftyco/ionicons/zip/v${ioniconsVersion}"
        dest new File("${workDirPath}/download/ionicons-${ioniconsVersion}.zip")
    }
    copy {
        from zipTree("${workDirPath}/download/ionicons-${ioniconsVersion}.zip")
        into "${workDirPath}/unzip"
    }
    copy {
        from "${workDirPath}/unzip/ionicons-${ioniconsVersion}/css/ionicons.min.css"
        into "${staticDirPath}/css"
    }
    copy {
        from "${workDirPath}/unzip/ionicons-${ioniconsVersion}/fonts"
        into "${staticDirPath}/fonts"
    }
}

void downloadHtml5shivJs(String html5shivJsVersion, String workDirPath, String staticDirPath) {
    download {
        src "https://oss.maxcdn.com/html5shiv/${html5shivJsVersion}/html5shiv.min.js"
        dest new File("${workDirPath}/download/html5shiv.min.js")
    }
    copy {
        from "${workDirPath}/download/html5shiv.min.js"
        into "${staticDirPath}/js"
    }
}

void downloadRespondMinJs(String respondMinJsVersion, String workDirPath, String staticDirPath) {
    download {
        src "https://oss.maxcdn.com/respond/${respondMinJsVersion}/respond.min.js"
        dest new File("${workDirPath}/download/respond.min.js")
    }
    copy {
        from "${workDirPath}/download/respond.min.js"
        into "${staticDirPath}/js"
    }
}

void downloadBootstrapFileInputMinJs(String workDirPath, String staticDirPath) {
    download {
        src "https://github.com/kartik-v/bootstrap-fileinput/zipball/master"
        dest new File("${workDirPath}/download/kartik-v-bootstrap-fileinput.zip")
    }
    copy {
        from zipTree("${workDirPath}/download/kartik-v-bootstrap-fileinput.zip")
        into "${workDirPath}/unzip"
    }
    copy {
        from "${workDirPath}/unzip/kartik-v-bootstrap-fileinput-883d8b6/js/fileinput.min.js"
        into "${staticDirPath}/js"
    }
    copy {
        from "${workDirPath}/unzip/kartik-v-bootstrap-fileinput-883d8b6/js/fileinput_locale_ja.js"
        into "${staticDirPath}/js"
    }
    copy {
        from "${workDirPath}/unzip/kartik-v-bootstrap-fileinput-883d8b6/css/fileinput.min.css"
        into "${staticDirPath}/css"
    }
}

def compareSrcAndTestDir(srcDir, excludePaths, excludeFileNamePatterns) {
    def existFlg
    def testDirAndTestClassNameList = [
            ["src/test/java", "Test.java"]
            , ["src/test/groovy", "Test.groovy"]
    ]

    for (srcFile in srcDir.listFiles()) {
        String srcFilePath = (srcFile.toPath() as String).replaceAll("\\\\", "/")
        existFlg = false

        for (exclude in excludePaths) {
            if (srcFilePath =~ /^${exclude as String}/) {
                existFlg = true
                break
            }
        }
        if (existFlg == true) continue

        for (exclude in excludeFileNamePatterns) {
            if (srcFilePath =~ /${exclude as String}/) {
                existFlg = true
                break
            }
        }
        if (existFlg == true) continue

        if (srcFile.isDirectory()) {
            compareSrcAndTestDir(srcFile, excludePaths, excludeFileNamePatterns)
        } else {
            def nextFlg = false
            for (testDirAndTestClassName in testDirAndTestClassNameList) {
                def testDir = testDirAndTestClassName[0]
                def testClassName = testDirAndTestClassName[1]

                String testFilePath = srcFilePath.replaceFirst(/^src\/main\/java/, testDir).replaceFirst(/\.java$/, testClassName)
                def testFile = new File(testFilePath)
                if (testFile.exists()) {
                    nextFlg = true
                    break
                }
            }
            if (nextFlg) continue

            println(srcFilePath);
        }
    }
}
  • Spring Boot のバージョンアップ対応として以下の点を変更します。
    • buildscript の以下の点を変更します。
      • springBootVersion = '1.3.5.RELEASE'springBootVersion = '1.4.4.RELEASE' に変更します。
      • classpath("io.spring.gradle:dependency-management-plugin:0.5.6.RELEASE")classpath("io.spring.gradle:dependency-management-plugin:0.6.1.RELEASE") に変更します。dependency-management-plugin は 1.0.0.RELEASE が出ていますが、これと spring-boot-gradle-plugin を一緒に入れると動かないので今回は 0.6.1.RELEASE を使用します。
    • apply plugin: 'spring-boot'apply plugin: 'org.springframework.boot' に変更します。
    • dependencyManagement の以下の点を変更します。
      • mavenBom 'io.spring.platform:platform-bom:2.0.5.RELEASE'mavenBom 'io.spring.platform:platform-bom:Athens-SR3' に変更します。
  • ライブラリを最新バージョンにアップデートするために以下の点を変更します。
    • buildscript の以下の点を変更します。
      • classpath("org.ajoberstar:grgit:1.7.0")classpath("org.ajoberstar:grgit:1.8.0") に変更します。
      • classpath("de.undercouch:gradle-download-task:3.0.0")classpath("de.undercouch:gradle-download-task:3.2.0") に変更します。
    • dependencies の以下の点を変更します。
      • def jdbcDriver = "org.postgresql:postgresql:9.4.1208"def jdbcDriver = "org.postgresql:postgresql:9.4.1212" に変更します。
      • compile("org.seasar.doma:doma:2.8.0")compile("org.seasar.doma:doma:2.15.0") に変更します。
      • compile("org.apache.commons:commons-lang3:3.4")compile("org.apache.commons:commons-lang3:3.5") に変更します。
      • compile("org.projectlombok:lombok:1.16.8")compileOnly("org.projectlombok:lombok:1.16.12") に変更します。
      • compile("com.google.guava:guava:19.0")compile("com.google.guava:guava:21.0") に変更します。
      • compile("com.univocity:univocity-parsers:2.1.1")compile("com.univocity:univocity-parsers:2.3.1") に変更します。
      • compile("org.thymeleaf.extras:thymeleaf-extras-java8time:2.1.0.RELEASE")compile("org.thymeleaf.extras:thymeleaf-extras-java8time:3.0.0.RELEASE") に変更します。
      • testCompile("org.dbunit:dbunit:2.5.1")testCompile("org.dbunit:dbunit:2.5.3") に変更します。
      • testCompile("com.icegreen:greenmail:1.5.0")testCompile("com.icegreen:greenmail:1.5.3") に変更します。
      • testCompile("org.assertj:assertj-core:3.4.1")testCompile("org.assertj:assertj-core:3.6.2") に変更します。
      • testCompile("org.jmockit:jmockit:1.23")testCompile("org.jmockit:jmockit:1.30") に変更します。
      • domaGenRuntime("org.seasar.doma:doma-gen:2.8.0")domaGenRuntime("org.seasar.doma:doma-gen:2.15.0") に変更します。
  • それ以外に以下の点を変更します。
    • compileJava.options.compilerArgs, compileTestGroovy.options.compilerArgs, compileTestJava.options.compilerArgs の3行を1行にして、-options,-processing を追加し、[compileJava, compileTestGroovy, compileTestJava]*.options*.compilerArgs = ['-Xlint:all,-options,-processing'] に変更します。
    • eclipse { ... } を削除します。
    • dependencies の以下の点を変更します。
      • testCompileOnly("org.projectlombok:lombok:1.16.12") を追加します。

■その2

sourceCompatibility = 1.8
targetCompatibility = 1.8

task wrapper(type: Wrapper) {
    gradleVersion = '2.13'
}
  • task wrapper(type: Wrapper) { gradleVersion = '2.13' } を追加します。

履歴

2017/02/11
初版発行。

Spring Boot 1.3.x の Web アプリを 1.4.x へバージョンアップする ( その2 )( IntelliJ IDEA の Gradle Tool Window の「Refresh all Gradle projects」を押してもエラーが出ないようにする )

概要

記事一覧はこちらです。

Spring Boot 1.3.x の Web アプリを 1.4.x へバージョンアップする ( その1 )( 概要 ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • IntelliJ IDEA の Gradle Tool Window の左上の「Refresh all Gradle projects」を押すとエラーが出たので、それを解消します。
    • 使用している IntelliJ IDEA のバージョンは 2016.3.4 です。

参照したサイト・書籍

目次

  1. 1.4.x ブランチの作成
  2. どんなエラーが出たのか?
  3. プロジェクトの JDK を設定し直す
  4. Gradle Tool Window で「Refresh all Gradle projects」ボタンを押して更新する

手順

1.4.x ブランチの作成

  1. master から 1.4.x ブランチを、1.4.x から feature/128-issue ブランチを作成します。

どんなエラーが出たのか?

IntelliJ IDEA をアップデート後1度も ksbysample-webapp-lending プロジェクトを開いていませんでした。開くと Gradle Tool Window に other のツリーしか表示されていない状態でした。

f:id:ksby:20170208224535p:plain

左上の「Refresh all Gradle projects」ボタンを押して更新すると以下のエラーが表示されます。

f:id:ksby:20170208224826p:plain

エラーメッセージの中に “Project JDK is not specified.” と出力されていました。そう言えば JDK をバージョンアップすると古い JDK を削除していましたね。。。 JDK を設定し直せば解消しそうです。

プロジェクトの JDK を設定し直す

  1. IntelliJ IDEA のメインメニューから「File」-「Project Structure…」を選択します。

  2. 「Project Structure」ダイアログが表示されます。「Project SDK」が赤字で表示されており、「Project language level」も「1.3 - Plain old Java」になっていました。

    f:id:ksby:20170208225702p:plain

    「Project SDK」で「1.8.0_121」を選択し、「Project language level」で「SDK default」を選択した後、「OK」ボタンを押してダイアログを閉じます。

    f:id:ksby:20170208225818p:plain

Gradle Tool Window で「Refresh all Gradle projects」ボタンを押して更新する

  1. Gradle Tool Window で「Refresh all Gradle projects」ボタンを押して更新してみます。

    今度は無事更新されて、application や build のツリーが表示されました。

    f:id:ksby:20170208232010p:plain

  2. clean タスク実行 → Rebuild Project 実行をした後、build タスクを実行して “BUILD SUCCESSFUL” のメッセージが表示されることを確認します。

    groovy 関連のライブラリも結構ダウンロードされますね。

    f:id:ksby:20170208232917p:plain f:id:ksby:20170208233115p:plain

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

    f:id:ksby:20170208233950p:plain

正常に動作することが確認できましたので、次回は build.gradle を修正します。

履歴

2017/02/08
初版発行。

Spring Boot 1.3.x の Web アプリを 1.4.x へバージョンアップする ( その1 )( 概要 )

概要

記事一覧はこちらです。

  • 「Spring Boot で書籍の貸出状況確認・貸出申請する Web アプリケーションを作る」で作成した Web アプリケーション ( ksbysample-webapp-lending ) の Spring Boot のバージョンを 1.3.5 → 1.4.4 へバージョンアップします。
  • 進め方は以下の方針とします。
    • Git のブランチは 1.4.x を作成して、そちらで作業します。Spring Boot のバージョンと合わせます。
    • 最初に IntelliJ IDEA をバージョンアップしているためか Gradle Tool Window の左上の「Refresh all Gradle projects」を押すとエラーが出たので、それを解消します。
    • 次に build.gradle を修正します。
      • Spring Boot のバージョン番号を 1.4.4 に、Spring IO Platform の BOM を Athens-SR3 にします。
      • Spring Initializr で 1.4.4 のプロジェクトを作成して、修正した方がよさそうな点があれば反映します。
      • ライブラリは最新バージョンにアップデートします。
    • プロジェクトを build し直してエラーが出る点があれば修正し、まずはここまでで動くようにします。
    • その後で 1.4 系ではこう書くべきという点があるか確認し、変更した方がよいところを修正します。

 
1.4 のリリースノートはこちらです。

Spring Boot 1.4 Release Notes
https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-1.4-Release-Notes

すでに 1.5.1 がリリースされていますので、手短に 1.4 系へのバージョンアップをまとめられるといいな。。。

履歴

2017/02/08
初版発行。

Spring Boot 1.3.x の Web アプリを 1.4.x へバージョンアップする ( 大目次 )

今回から大目次を先に書きます。

  1. その1 ( 概要 )
  2. その2 ( IntelliJ IDEA の Gradle Tool Window の「Refresh all Gradle projects」を押してもエラーが出ないようにする )
  3. その3 ( build.gradle の修正 )
  4. その4 ( build.gradle 修正後の Rebuild で出た Warning を解消する )
  5. その5 ( メールのテンプレートに使用していた Velocity を FreeMarker に変更する )
  6. その6 ( 「Run ‘All Tests’ with Coverage」実行時のエラーを解消する+build タスク実行時の警告を解消する )
  7. その7 ( Google の Java コンパイル時バグチェックツール? Error Prone を試してみる )
  8. その8 ( build.gradle への checkstyle, findbugs の導入+CheckStyle-IDEA, FindBugs-IDEA Plugin の導入 )
  9. その9 ( 1.3系 → 1.4系で実装方法が変更された点を修正する )
  10. その10 ( インジェクションの方法を @Autowired によるフィールドインジェクション → コンストラクタインジェクションへ変更する )
  11. その11 ( Error Prone を 2.0.15 → 2.0.18 へバージョンアップ。。。できませんでした )
  12. その12 ( RestTemplateBuilder を使用するように変更したらテストが失敗するようになった理由とは? )
  13. その13 ( RestTemplate で WebAPI を呼び出している処理に spring-retry でリトライ処理を入れる )
  14. 番外編 ( IntelliJ IDEA に Request mapper Plugin をインストールする )
  15. その14 ( spring-boot-gradle-plugin は dependency-management-plugin を自動的に適用するので build.gradle に記述する必要がありませんでした )
  16. その15 ( テストクラスのアノテーションを 1.4 のものに変更する )
  17. その16 ( テストクラスのモックを @MockBean + Mockito で作り直す )
  18. その17 ( テストクラスのモックを @MockBean + Mockito で作り直す2 )
  19. その18 ( Gradle のバージョンを 2.13 → 3.x へバージョンアップ。。。しようと思いましたが止めました )
  20. 番外編 ( Optional をもう少しまともに使ってみる )
  21. その19 ( Spring Boot を 1.4.4 → 1.4.5 にバージョンアップする )
  22. その20 ( 気になった点を修正する )
  23. その21 ( Log4jdbc Spring Boot Starter を入れてみる )
  24. その22 ( application.properties に記述する spring.datasource.tomcat.~ の設定を見直す )
  25. その23 ( Spring Security 関連で修正した方がよい箇所を見直す )
  26. その24 ( Spring Boot を 1.4.5 → 1.4.6 にバージョンアップする )
  27. その25 ( jar ファイルを作成して動作確認する )
  28. その26 ( jar ファイルを作成して動作確認する2 )
  29. 番外編 ( Thymeleaf 3 へのバージョンアップを試してみる )
  30. 番外編 ( Thymeleaf 3 へのバージョンアップを試してみる2 )
  31. その27 ( Thymeleaf parser-level comment blocks で @thymesVar のコメント文が HTML に出力されないようにする )
  32. 感想

IntelliJ IDEA を 2016.3.3 → 2016.3.4 へ、Git for Windows を 2.11.0(3) → 2.11.1 へバージョンアップ

IntelliJ IDEA を 2016.3.3 → 2016.3.4 へバージョンアップする

IntelliJ IDEA の 2016.3.4 がリリースされたのでバージョンアップします。

※ksbysample-nexus-repomng/ksbysample-library-depend-nospring プロジェクトを開いた状態でバージョンアップしています。

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

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

    f:id:ksby:20170206001521p:plain

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

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

  5. 処理が終了すると Gradle Tool Window のツリーの表示が other グループしかない初期の状態に戻っていますので、左上の「Refresh all Gradle projects」ボタンをクリックして更新します。更新が完了すると build グループ等が表示されます。

  6. clean タスク実行 → Rebuild Project 実行をした後、build タスクを実行して “BUILD SUCCESSFUL” のメッセージが表示されることを確認します。

    f:id:ksby:20170206002118p:plain

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

    f:id:ksby:20170206002300p:plain

Git for Windows を 2.11.0(3) → 2.11.1 へバージョンアップする

Git for Windows の 2.11.1 がリリースされていたのでバージョンアップします。

  1. https://git-for-windows.github.io/ の「Download」ボタンをクリックして Git-2.11.1-64-bit.exe をダウンロードします。

  2. Git-2.11.1-64-bit.exe を実行します。

  3. 「Git 2.11.1 Setup」ダイアログが表示されます。[Next >]ボタンをクリックします。

  4. 「Select Components」画面が表示されます。全てのチェックが外れたままであることを確認した後、[Next >]ボタンをクリックします。

  5. 「Adjusting your PATH environment」画面が表示されます。中央の「Use Git from the Windows Command Prompt」が選択されていることを確認後、[Next >]ボタンをクリックします。

  6. 「Configuring the line ending conversions」画面が表示されます。「Checkout Windows-style, commit Unix-style line endings」が選択されていることを確認した後、[Next >]ボタンをクリックします。

  7. 「Configuring the terminal emulator to use with Git Bash」画面が表示されます。「Use Windows'default console window」が選択されていることを確認した後、[Next >]ボタンをクリックします。

  8. 「Configuring extra options」画面が表示されます。「Enable file system caching」だけがチェックされていることを確認した後、[Next >]ボタンをクリックします。

  9. 「Configuring experimental options」画面が表示されます。全てのチェックが外れたままであることを確認した後、[Install]ボタンをクリックします。

  10. インストールが完了すると「Completing the Git Setup Wizard」のメッセージが表示された画面が表示されます。中央の「View ReleaseNotes.html」のチェックを外した後、「Finish」ボタンをクリックしてインストーラーを終了します。

  11. コマンドプロンプトを起動して git のバージョンが git version 2.11.1.windows.1 になっていることを確認します。

    f:id:ksby:20170206005119p:plain

  12. git-cmd.exe を起動して日本語の表示・入力が問題ないかを確認します。

    f:id:ksby:20170206005311p:plain

  13. 特に問題はないようですので、2.11.1 で作業を進めたいと思います。

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

概要

記事一覧はこちらです。

Spring Boot + Spring Integration でいろいろ試してみる ( その15 )( RequestHandlerRetryAdvice のサンプルを作ってみる ) の続きです。

ExpressionEvaluatingRequestHandlerAdvice のサンプルを作成します。ExpressionEvaluatingRequestHandlerAdvice は以下の2つを指定するための RequestHandlerAdvice です。

  • 成功、失敗(例外が throw された)時の処理を SpEL で記述できる。
  • 成功・失敗時に送信する MessageChannel を指定できる。

参照したサイト・書籍

  1. Spring Integration Reference Manual - 8.9.2 Provided Advice Classes - Expression Evaluating Advice
    http://docs.spring.io/spring-integration/docs/current/reference/html/messaging-endpoints-chapter.html#expression-advice

  2. Move file with file-adapter with SI
    http://stackoverflow.com/questions/33835657/move-file-with-file-adapter-with-si

  3. instanceof in SpEL
    http://stackoverflow.com/questions/7628437/instanceof-in-spel

目次

  1. Spring Boot を 1.4.3 → 1.4.4 へ、Spring IO Platform を Athens-SR2 → Athens-SR3 へバージョンアップする
  2. C:\eipapp\ksbysample-eipapp-advice ディレクトリを変更する
  3. application.properties, logback-spring.xml を追加する
  4. ExpressionEvaluatingRequestHandlerAdvice のサンプルを作成する
    1. setOnSuccessExpressionString, setOnFailureExpressionString だけ指定する
    2. setSuccessChannelName, setFailureChannelName だけ指定することはできない
    3. setOnSuccessExpressionString, setOnFailureExpressionString+setSuccessChannelName, setFailureChannelName の組み合わせで指定する
    4. setOnSuccessExpressionString, setOnFailureExpressionString で Success, Failure 用の MessageChannel に渡す payload の型を変更する
  5. ExpressionEvaluatingRequestHandlerAdvice を使用した Advice は Bean として定義しないと機能しない?
  6. ExpressionEvaluatingRequestHandlerAdvice を e -> e.advice(…) で指定した後に処理を書いたらどうなる?
  7. RequestHandlerRetryAdvice と一緒に指定してみる
  8. 続くのか。。。?

手順

Spring Boot を 1.4.3 → 1.4.4 へ、Spring IO Platform を Athens-SR2 → Athens-SR3 へバージョンアップする

Spring Boot、Spring IO Platform がバージョンアップされていますので、build.gradle を リンク先の内容 に変更してバージョンアップします。これにより Spring Integration も 4.3.6 → 4.3.7 へバージョンアップされます。

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

ちょうど試そうとしていた ExpressionEvaluatingRequestHandlerAdvice が、Spring Integration 4.3.7 から以下の変更が入っていました。

  • setOnSuccessExpression(String)、setOnFailureExpression(String) が Deprecated となり、setOnSuccessExpressionString(String)、setOnFailureExpressionString(String) に変わりました。

C:\eipapp\ksbysample-eipapp-advice ディレクトリを変更する

in06, in07, in08, in09, success, error ディレクトリを追加します。

C:\eipapp\ksbysample-eipapp-advice
├ error
├ in01
├ in02
├ in03
├ in04
├ in05
├ in06
├ in07
├ in08
├ in09
└ success

application.properties, logback-spring.xml を追加する

SpEL を使用するので org.springframework.integration.expression.ExpressionUtils の WARN ログが出力されないようにします。

  1. src/main/resources の下に application.properties を作成し、リンク先の内容 を記述します。

  2. src/main/resources の下に logback-spring.xml を作成し、リンク先の内容 を記述します。

ExpressionEvaluatingRequestHandlerAdvice のサンプルを作成する

動作確認のためにサンプルを作成していきます。サンプルは src/main/java/ksbysample/eipapp/advice の下に SuccessOrFailureFlowConfig.java を作成して、その中に記述します。完成形は リンク先の内容 です。

setOnSuccessExpressionString, setOnFailureExpressionString だけ指定する

成功時にはファイルを削除し、失敗時にはファイルを error ディレクトリへ移動するサンプルを作成します。

まずは成功する場合を試します。advice 対象の処理内で例外を throw しません。

    private final String EIPAPP_ADVICE_ROOT_DIR = "C:/eipapp/ksbysample-eipapp-advice";


    // setOnSuccessExpressionString, setOnFailureExpressionString だけ指定するサンプル
    //  ・成功時にはファイルを削除する。
    //  ・失敗時にはファイルを error ディレクトリへ移動する。
    //  ・削除、移動の処理は SpEL で記述する。

    @Bean
    public Advice deleteOrMoveAdvice() {
        ExpressionEvaluatingRequestHandlerAdvice advice
                = new ExpressionEvaluatingRequestHandlerAdvice();
        advice.setOnSuccessExpressionString("payload.delete()");
        advice.setOnFailureExpressionString(
                "payload.renameTo(new java.io.File('" + EIPAPP_ADVICE_ROOT_DIR + "/error/' + payload.name))");
        // setTrapException(true) を指定すると throw された例外が再 throw されず、
        // ログに出力されない
        advice.setTrapException(true);
        return advice;
    }

    @Bean
    public IntegrationFlow in06Flow() {
        return IntegrationFlows
                .from(s -> s.file(new File(EIPAPP_ADVICE_ROOT_DIR + "/in06"))
                        , e -> e.poller(Pollers.fixedDelay(1000)))
                .<File>handle((p, h) -> {
                    log.info("★★★ " + p.getAbsolutePath());
//                    if (true) {
//                        throw new RuntimeException("エラーです");
//                    }
                    return null;
                }, e -> e.advice(deleteOrMoveAdvice()))
                .get();
    }

bootRun タスクを実行した後 C:\eipapp\ksbysample-eipapp-advice\in06 の下に empty.txt を置くとファイルは削除されました。

f:id:ksby:20170205112733p:plain
↓↓↓
f:id:ksby:20170205113033p:plain

今度は失敗する場合を試します。advice 対象の処理内で例外を throw します。

                .<File>handle((p, h) -> {
                    log.info("★★★ " + p.getAbsolutePath());
                    if (true) {
                        throw new RuntimeException("エラーです");
                    }
                    return null;
                }, e -> e.advice(deleteOrMoveAdvice()))

bootRun タスクを実行した後 C:\eipapp\ksbysample-eipapp-advice\in06 の下に empty.txt を置くとファイルは error ディレクトリへ移動しました。

f:id:ksby:20170205113837p:plain
↓↓↓
f:id:ksby:20170205114107p:plain

また今回、以下のように記述していますが、

                .<File>handle((p, h) -> {

これは以下と同じです。

                .handle((GenericHandler<File>) (p, h) -> {

第2引数の e -> ... を記述する場合にはどちらかのパターンで記述しないとコンパイルエラーになります。IntelliJ IDEA では記述がない場合 .handle に赤波下線が表示され、Alt+Enter を押すと後者のパターンで補完されます(ただし、File ではなく Object になります)。

setSuccessChannelName, setFailureChannelName だけ指定することはできない

以下の実装では動作しません。

    @Bean
    public Advice successOrFailureChannelAdvice() {
        ExpressionEvaluatingRequestHandlerAdvice advice
                = new ExpressionEvaluatingRequestHandlerAdvice();
        advice.setSuccessChannelName("successFlow.input");
        advice.setFailureChannelName("failureFlow.input");
        advice.setTrapException(true);
        return advice;
    }

payload をそのまま Success, Failure 用の MessageChannel に渡す場合には advice.setOnSuccessExpressionString("payload"); のように "payload" とだけ記述した setOn…ExpressionString(…) を書く必要があります。

    @Bean
    public Advice successOrFailureChannelAdvice() {
        ExpressionEvaluatingRequestHandlerAdvice advice
                = new ExpressionEvaluatingRequestHandlerAdvice();
        advice.setOnSuccessExpressionString("payload");
        advice.setSuccessChannelName("successFlow.input");
        advice.setOnFailureExpressionString("payload");
        advice.setFailureChannelName("failureFlow.input");
        advice.setTrapException(true);
        return advice;
    }

setOnSuccessExpressionString, setOnFailureExpressionString+setSuccessChannelName, setFailureChannelName の組み合わせで指定する

サンプルの動作は上で書いた成功時にはファイルを削除し、失敗時にはファイルを error ディレクトリへ移動するというものですが、今度は SpEL ではなく転送した MessageChannel の先の処理で削除、移動します。

まずは成功する場合を試します。advice 対象の処理内で例外を throw しません。

    // setOnSuccessExpressionString, setOnFailureExpressionString+setSuccessChannelName, setFailureChannelName
    // の組み合わせで指定するサンプル
    //  ・成功時には successFlow.input へ Message を送信する。
    //    successFlow ではファイルを削除する。
    //  ・失敗時には failureFlow.input へ Message を送信する。
    //    failureFlow ではファイルを error ディレクトリへ移動する。

    @Bean
    public Advice successOrFailureChannelAdvice() {
        ExpressionEvaluatingRequestHandlerAdvice advice
                = new ExpressionEvaluatingRequestHandlerAdvice();
        advice.setOnSuccessExpressionString("payload");
        advice.setSuccessChannelName("successFlow.input");
        // setOnFailureExpressionString に "payload" と記述しても Failure 用の MessageChannel には
        // File クラスではなく MessageHandlingExpressionEvaluatingAdviceException クラスの payload が渡される
        advice.setOnFailureExpressionString("payload");
        advice.setFailureChannelName("failureFlow.input");
        advice.setTrapException(true);
        return advice;
    }

    @Bean
    public IntegrationFlow in07Flow() {
        return IntegrationFlows
                .from(s -> s.file(new File(EIPAPP_ADVICE_ROOT_DIR + "/in07"))
                        , e -> e.poller(Pollers.fixedDelay(1000)))
                .<File>handle((p, h) -> {
                    log.info("★★★ " + p.getAbsolutePath());
//                    if (true) {
//                        throw new RuntimeException("エラーです");
//                    }
                    return null;
                }, e -> e.advice(successOrFailureChannelAdvice()))
                .get();
    }

    @Bean
    public IntegrationFlow successFlow() {
        return f -> f
                .<File>handle((p, h) -> {
                    // ファイルを削除する
                    try {
                        Files.delete(Paths.get(p.getAbsolutePath()));
                        log.info("ファイルを削除しました ( {} )", p.getAbsolutePath());
                    } catch (IOException e) {
                        throw new RuntimeException(e);
                    }
                    return null;
                });
    }

    @Bean
    public IntegrationFlow failureFlow() {
        return f -> f
                .<ExpressionEvaluatingRequestHandlerAdvice.MessageHandlingExpressionEvaluatingAdviceException>
                        handle((p, h) -> {
                    // MessageHandlingExpressionEvaluatingAdviceException クラスの payload から
                    // Exception 発生前の File クラスの payload を取得する
                    File file = (File) p.getEvaluationResult();

                    // ファイルを error ディレクトリへ移動する
                    Path src = Paths.get(file.getAbsolutePath());
                    Path dst = Paths.get(EIPAPP_ADVICE_ROOT_DIR + "/error/" + file.getName());
                    try {
                        Files.move(src, dst, StandardCopyOption.REPLACE_EXISTING);
                        log.info("ファイルを移動しました ( {} --> {} )"
                                , src.toAbsolutePath(), dst.toAbsolutePath());
                    } catch (IOException e) {
                        throw new RuntimeException(e);
                    }
                    return null;
                });
    }

bootRun タスクを実行した後 C:\eipapp\ksbysample-eipapp-advice\in07 の下に empty.txt を置くとファイルは削除されました。

f:id:ksby:20170205142858p:plain
↓↓↓
f:id:ksby:20170205143117p:plain

今度は失敗する場合を試します。advice 対象の処理内で例外を throw します。

                .<File>handle((p, h) -> {
                    log.info("★★★ " + p.getAbsolutePath());
                    if (true) {
                        throw new RuntimeException("エラーです");
                    }
                    return null;
                }, e -> e.advice(successOrFailureChannelAdvice()))

bootRun タスクを実行した後 C:\eipapp\ksbysample-eipapp-advice\in07 の下に empty.txt を置くとファイルは error ディレクトリへ移動しました。

f:id:ksby:20170205143420p:plain
↓↓↓
f:id:ksby:20170205143902p:plain

SpEL だけで処理が書けるならそれで済ませた方がコード量は少なくて楽ですね。

setOnSuccessExpressionString, setOnFailureExpressionString で Success, Failure 用の MessageChannel に渡す payload の型を変更する

setOnSuccessExpressionString, setOnFailureExpressionString に記述する SpEL の結果により Success, Failure 用の MessageChannel に送信される Message の payload にセットされるデータの型を変えることができます。

以下のように実装すると元の Message の payload は File クラスですが、Success, Failure 用の MessageChannel に送信される Message の payload は String クラスになります(まあ Failure 用に送信される Message の payload は更に MessageHandlingExpressionEvaluatingAdviceException になるので実際は少し面倒ですが)。

    // setOnSuccessExpressionString, setOnFailureExpressionString で Success, Failure 用の
    // MessageChannel に渡す payload の型を変更するサンプル
    //  ・元の File クラスの payload から String クラスの payload の Message に変換して
    //    Success, Failure 用の MessageChannel に送信する

    @Bean
    public Advice convertFileToStringAdvice() {
        ExpressionEvaluatingRequestHandlerAdvice advice
                = new ExpressionEvaluatingRequestHandlerAdvice();
        advice.setOnSuccessExpressionString("payload + ' の処理に成功しました。'");
        advice.setSuccessChannelName("printFlow.input");
        advice.setOnFailureExpressionString(
                "payload + ' の処理に失敗しました ( ' + #exception.class.name + ', ' + #exception.cause.message + ' )'");
        advice.setFailureChannelName("printFlow.input");
        advice.setTrapException(true);
        return advice;
    }

    @Bean
    public IntegrationFlow in08Flow() {
        return IntegrationFlows
                .from(s -> s.file(new File(EIPAPP_ADVICE_ROOT_DIR + "/in08"))
                        , e -> e.poller(Pollers.fixedDelay(1000)))
                .<File>handle((p, h) -> {
                    log.info("★★★ " + p.getAbsolutePath());
//                    if (true) {
//                        throw new RuntimeException("エラーです");
//                    }
                    return null;
                }, e -> e.advice(convertFileToStringAdvice()))
                .get();
    }

    @Bean
    public IntegrationFlow printFlow() {
        return f -> f
                // setFailureChannelName(...) の指定で転送された Message は
                // MessageHandlingExpressionEvaluatingAdviceException クラスなので、
                // .transform(...) で SpEL を利用して元の String を取得する
                .transform("payload instanceof T(org.springframework.integration.handler.advice.ExpressionEvaluatingRequestHandlerAdvice$MessageHandlingExpressionEvaluatingAdviceException)"
                        + " ? payload.evaluationResult : payload")
                .handle((p, h) -> {
                    System.out.println("●●● " + p);
                    return null;
                });

    }

まずは成功する場合を試します。advice 対象の処理内で例外を throw しません。

bootRun タスクを実行した後 C:\eipapp\ksbysample-eipapp-advice\in08 の下に empty.txt を置くと以下のログが出力されます。

f:id:ksby:20170205161843p:plain

今度は失敗する場合を試します。advice 対象の処理内で例外を throw します。

                .<File>handle((p, h) -> {
                    log.info("★★★ " + p.getAbsolutePath());
                    if (true) {
                        throw new RuntimeException("エラーです");
                    }
                    return null;
                }, e -> e.advice(convertFileToStringAdvice()))

bootRun タスクを実行した後 C:\eipapp\ksbysample-eipapp-advice\in08 の下に empty.txt を置くと以下のログが出力されます。

f:id:ksby:20170205162204p:plain

ExpressionEvaluatingRequestHandlerAdvice を使用した Advice は Bean として定義しないと機能しない?

結論から言うと setSuccessChannelName, setFailureChannelName 等のメソッドを使用して成功、失敗時に別の MessageChannel に Message を送信しないのであれば Bean を作成する必要はありませんが、送信する場合には必ず Bean にする必要があります。

例えば in06Flow を以下のようにメソッド内で ExpressionEvaluatingRequestHandlerAdvice のインスタンスを生成するよう変更します。

    @Bean
    public IntegrationFlow in06Flow() {
        ExpressionEvaluatingRequestHandlerAdvice advice
                = new ExpressionEvaluatingRequestHandlerAdvice();
        advice.setOnSuccessExpressionString("payload.delete()");
        advice.setOnFailureExpressionString(
                "payload.renameTo(new java.io.File('" + EIPAPP_ADVICE_ROOT_DIR + "/error/' + payload.name))");
        advice.setTrapException(true);

        return IntegrationFlows
                .from(s -> s.file(new File(EIPAPP_ADVICE_ROOT_DIR + "/in06"))
                        , e -> e.poller(Pollers.fixedDelay(1000)))
                .<File>handle((p, h) -> {
                    log.info("★★★ " + p.getAbsolutePath());
//                    if (true) {
//                        throw new RuntimeException("エラーです");
//                    }
                    return null;
                }, e -> e.advice(advice))
                .get();
    }

bootRun タスクを実行した後 C:\eipapp\ksbysample-eipapp-advice\in06 の下に empty.txt を置くとファイルは削除されます。

f:id:ksby:20170205170845p:plain
↓↓↓
f:id:ksby:20170205171202p:plain

今度は in07Flow を以下のようにメソッド内で ExpressionEvaluatingRequestHandlerAdvice のインスタンスを生成するよう変更します。

    @Bean
    public IntegrationFlow in07Flow() {
        ExpressionEvaluatingRequestHandlerAdvice advice
                = new ExpressionEvaluatingRequestHandlerAdvice();
        advice.setOnSuccessExpressionString("payload");
        advice.setSuccessChannelName("successFlow.input");
        advice.setOnFailureExpressionString("payload");
        advice.setFailureChannelName("failureFlow.input");
        advice.setTrapException(true);

        return IntegrationFlows
                .from(s -> s.file(new File(EIPAPP_ADVICE_ROOT_DIR + "/in07"))
                        , e -> e.poller(Pollers.fixedDelay(1000)))
                .<File>handle((p, h) -> {
                    log.info("★★★ " + p.getAbsolutePath());
//                    if (true) {
//                        throw new RuntimeException("エラーです");
//                    }
                    return null;
                }, e -> e.advice(advice))
                .get();
    }

bootRun タスクを実行した後 C:\eipapp\ksbysample-eipapp-advice\in07 の下に empty.txt を置いてもファイルは削除されませんでした。

f:id:ksby:20170205171851p:plain
↓↓↓
f:id:ksby:20170205172155p:plain

ログには Caused by: java.lang.IllegalArgumentException: BeanFactory must not be null の例外が throw されていました。

f:id:ksby:20170205172343p:plain

ExpressionEvaluatingRequestHandlerAdvice を e -> e.advice(…) で指定した後に処理を書いたらどうなる?

成功、失敗時に別の MessageChannel に Message を送信する場合としない場合、それぞれで動作を確認してみます。

最初は送信しない場合です。in06Flow を以下のように変更します。

    @Bean
    public IntegrationFlow in06Flow() {
        return IntegrationFlows
                .from(s -> s.file(new File(EIPAPP_ADVICE_ROOT_DIR + "/in06"))
                        , e -> e.poller(Pollers.fixedDelay(1000)))
                .<File>handle((p, h) -> {
                    log.info("★★★ PASS1 " + p.getAbsolutePath());
//                    if (true) {
//                        throw new RuntimeException("エラーです");
//                    }
                    return p;
                }, e -> e.advice(deleteOrMoveAdvice()))
                .handle((p, h) -> {
                    log.info("★★★ PASS2 " + p.getClass().getName());
                    return null;
                })
                .get();
    }
  • advice を指定している処理で return null;return p; に変更して Message を次の処理に渡すようにします。またログに “PASS1” の文字列を追加します。
  • 2つ目の .handle(...) を追加します。

bootRun タスクを実行した後 C:\eipapp\ksbysample-eipapp-advice\in06 の下に empty.txt を置くと以下のログが出力されました。追加した .handle(...) の処理が実行されています。

f:id:ksby:20170205182324p:plain

in06 ディレクトリの下に置いた empty.txt も削除されていました。ExpressionEvaluatingRequestHandlerAdvice で指定した処理も実行されていました。

f:id:ksby:20170205181614p:plain
↓↓↓
f:id:ksby:20170205182056p:plain

次は送信する場合です。in07Flow を以下のように変更します。

    @Bean
    public IntegrationFlow in07Flow() {
        return IntegrationFlows
                .from(s -> s.file(new File(EIPAPP_ADVICE_ROOT_DIR + "/in07"))
                        , e -> e.poller(Pollers.fixedDelay(1000)))
                .<File>handle((p, h) -> {
                    log.info("★★★ PASS1 " + p.getAbsolutePath());
//                    if (true) {
//                        throw new RuntimeException("エラーです");
//                    }
                    return p;
                }, e -> e.advice(successOrFailureChannelAdvice()))
                .handle((p, h) -> {
                    log.info("★★★ PASS2 " + p.getClass().getName());
                    return null;
                })
                .get();
    }

    @Bean
    public IntegrationFlow successFlow() {
        return f -> f
                .<File>handle((p, h) -> {
                    // ファイルを削除する
                    try {
                        Files.delete(Paths.get(p.getAbsolutePath()));
                        log.info("★★★ PASS3 ファイルを削除しました ( {} )", p.getAbsolutePath());
                    } catch (IOException e) {
                        throw new RuntimeException(e);
                    }
                    return null;
                });
    }
  • advice を指定している処理で return null;return p; に変更して Message を次の処理に渡すようにします。またログに “PASS1” の文字列を追加します。
  • 2つ目の .handle(...) を追加します。
  • successFlow のログに “★★★ PASS3” の文字列を追加します。

bootRun タスクを実行した後 C:\eipapp\ksbysample-eipapp-advice\in07 の下に empty.txt を置くと以下のログが出力されました。追加した .handle(...) の処理が実行されています。

f:id:ksby:20170205183630p:plain

in07 ディレクトリの下に置いた empty.txt も削除されていました。ExpressionEvaluatingRequestHandlerAdvice で指定した処理も実行されていました。

f:id:ksby:20170205183303p:plain
↓↓↓
f:id:ksby:20170205183527p:plain

以上の結果から、以下のことが分かりました。

  • advice を書いても次の処理は実行されます。
  • 1つ目の .handle(...) の処理 → ExpressionEvaluatingRequestHandlerAdvice で指定した成功時の処理 → 2つ目の .handle(...) の処理、の順で実行されます。

RequestHandlerRetryAdvice と一緒に指定してみる

以下のコードを追加します。

    // RequestHandlerRetryAdvice と ExpressionEvaluatingRequestHandlerAdvice を一緒に指定するサンプル
    //  ・RequestHandlerRetryAdvice に指定する RetryTemplate には FlowConfig.java に書いた
    //    simpleAndFixedRetryTemplate Bean を使用する

    @Autowired
    @Qualifier("simpleAndFixedRetryTemplate")
    private RetryTemplate simpleAndFixedRetryTemplate;

    @Bean
    public IntegrationFlow in09Flow() {
        RequestHandlerRetryAdvice retryAdvice = new RequestHandlerRetryAdvice();
        retryAdvice.setRetryTemplate(this.simpleAndFixedRetryTemplate);
        retryAdvice.setRecoveryCallback(context -> {
            // リトライが全て失敗するとこの処理が実行される
            MessageHandlingException e = (MessageHandlingException) context.getLastThrowable();
            Message<?> message = ((MessageHandlingException) context.getLastThrowable()).getFailedMessage();
            File payload = (File) message.getPayload();
            log.error("●●● " + e.getRootCause().getClass().getName());
            log.error("●●● " + payload.getName());
            // 例外を再 throw して ExpressionEvaluatingRequestHandlerAdvice の失敗時の処理
            // が実行されるようにする
            throw e;
        });

        return IntegrationFlows
                .from(s -> s.file(new File(EIPAPP_ADVICE_ROOT_DIR + "/in09"))
                        , e -> e.poller(Pollers.fixedDelay(1000)))
                .<File>handle((p, h) -> {
                    RetryContext retryContext = RetrySynchronizationManager.getContext();
                    log.info("★★★ リトライ回数 = " + retryContext.getRetryCount());

                    // 例外を throw して必ずリトライさせる
                    if (true) {
                        throw new RuntimeException("エラーです");
                    }
                    return null;
                }, e -> e
                        // RequestHandlerRetryAdvice と ExpressionEvaluatingRequestHandlerAdvice
                        // を一緒に指定する場合には RequestHandlerRetryAdvice を後に書くこと。
                        // 最初に書くとリトライしてくれない。
                        .advice(successOrFailureChannelAdvice())
                        .advice(retryAdvice))
                .get();
    }

bootRun タスクを実行した後 C:\eipapp\ksbysample-eipapp-advice\in09 の下に empty.txt を置くと5回リトライした後、error ディレクトリにファイルを移動しました。

f:id:ksby:20170205192401p:plain

ちなみにコメントに書いてありますが、RequestHandlerRetryAdvice を先に書くとこうなります。

                }, e -> e
                        // RequestHandlerRetryAdvice と ExpressionEvaluatingRequestHandlerAdvice
                        // を一緒に指定する場合には RequestHandlerRetryAdvice を後に書くこと。
                        // 最初に書くとリトライしてくれない。
                        .advice(retryAdvice)
                        .advice(successOrFailureChannelAdvice()))
                .get();

f:id:ksby:20170205192650p:plain

リトライされずにすぐに ExpressionEvaluatingRequestHandlerAdvice の失敗時の処理が実行されます。

上の結果は AOP の処理がどう挿入されるかに依存するので、一緒に指定した場合の処理順序はもしかすると今後ライブラリの実装内容によって変わるかもしれません。リトライ処理は RequestHandlerRetryAdvice は使用せずに RetryTemplate で直接 .handle(...) 内に記述して、advice で指定するのは ExpressionEvaluatingRequestHandlerAdvice だけにする方が無難かもしれません。

続くのか。。。?

あと1つ残った RequestHandlerCircuitBreakerAdvice や、TransactionSynchronizationFactory を使用して Flow 全体の正常、失敗を見て処理を行う方法をまとめておきたいですが、RequestHandlerRetryAdvice, ExpressionEvaluatingRequestHandlerAdvice が調べると意外にボリュームがあって重かったので、一旦サンプル作成に戻ります。気が向いたらまた書きます。

ソースコード

build.gradle

group 'ksbysample'
version '1.0.0-RELEASE'

buildscript {
    ext {
        springBootVersion = '1.4.4.RELEASE'
    }
    repositories {
        mavenCentral()
        maven { url "http://repo.spring.io/repo/" }
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
        classpath("io.spring.gradle:dependency-management-plugin:0.6.1.RELEASE")
    }
}

..........

dependencyManagement {
    imports {
        mavenBom 'io.spring.platform:platform-bom:Athens-SR3'
        mavenBom 'org.springframework.cloud:spring-cloud-dependencies:Camden.RELEASE'
    }
}

..........
  • buildscript の中の springBootVersion = '1.4.3.RELEASE'springBootVersion = '1.4.4.RELEASE' へ変更します。
  • dependencyManagement の中の mavenBom 'io.spring.platform:platform-bom:Athens-SR2'mavenBom 'io.spring.platform:platform-bom:Athens-SR3' へ変更します。

application.properties

spring.application.name=advice
spring.zipkin.base-url=http://localhost:9411/
spring.sleuth.sampler.percentage=1.0

logback-spring.xml

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <include resource="org/springframework/boot/logging/logback/defaults.xml"/>

    <springProperty scope="context" name="springAppName" source="spring.application.name"/>
    <property name="CONSOLE_LOG_PATTERN"
              value="%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(${level:-%5p}) %clr([${springAppName:-},%X{X-B3-TraceId},%X{X-B3-SpanId}]){yellow} %clr(${PID:-}){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}"/>
    <include resource="org/springframework/boot/logging/logback/console-appender.xml"/>

    <root level="INFO">
        <appender-ref ref="CONSOLE"/>
    </root>
    <logger name="org.springframework.integration.expression.ExpressionUtils" level="ERROR"/>
</configuration>

<logger name="org.springframework.integration.expression.ExpressionUtils" level="ERROR"/> 以外に、以前と比較して以下の点を変更しました。

  • %X{X-B3-TraceId:-}%X{X-B3-TraceId} へ変更しました。
  • %X{X-B3-SpanId:-}%X{X-B3-SpanId} へ変更しました。
  • ,%X{X-Span-Export:-} を削除しました。
  • <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender"> ... </appender> と独自で指定せずに <include resource="org/springframework/boot/logging/logback/console-appender.xml"/> を使用するようにしました。

SuccessOrFailureFlowConfig.java

package ksbysample.eipapp.advice;

import lombok.extern.slf4j.Slf4j;
import org.aopalliance.aop.Advice;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.integration.dsl.IntegrationFlow;
import org.springframework.integration.dsl.IntegrationFlows;
import org.springframework.integration.dsl.core.Pollers;
import org.springframework.integration.handler.advice.ExpressionEvaluatingRequestHandlerAdvice;
import org.springframework.integration.handler.advice.RequestHandlerRetryAdvice;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHandlingException;
import org.springframework.retry.RetryContext;
import org.springframework.retry.support.RetrySynchronizationManager;
import org.springframework.retry.support.RetryTemplate;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;

@Slf4j
@Configuration
public class SuccessOrFailureFlowConfig {

    private final String EIPAPP_ADVICE_ROOT_DIR = "C:/eipapp/ksbysample-eipapp-advice";


    // setOnSuccessExpressionString, setOnFailureExpressionString だけ指定するサンプル
    //  ・成功時にはファイルを削除する。
    //  ・失敗時にはファイルを error ディレクトリへ移動する。
    //  ・削除、移動の処理は SpEL で記述する。

    @Bean
    public Advice deleteOrMoveAdvice() {
        ExpressionEvaluatingRequestHandlerAdvice advice
                = new ExpressionEvaluatingRequestHandlerAdvice();
        advice.setOnSuccessExpressionString("payload.delete()");
        advice.setOnFailureExpressionString(
                "payload.renameTo(new java.io.File('" + EIPAPP_ADVICE_ROOT_DIR + "/error/' + payload.name))");
        // setTrapException(true) を指定すると throw された例外が再 throw されず、
        // ログに出力されない
        advice.setTrapException(true);
        return advice;
    }

    @Bean
    public IntegrationFlow in06Flow() {
        return IntegrationFlows
                .from(s -> s.file(new File(EIPAPP_ADVICE_ROOT_DIR + "/in06"))
                        , e -> e.poller(Pollers.fixedDelay(1000)))
                .<File>handle((p, h) -> {
                    log.info("★★★ " + p.getAbsolutePath());
//                    if (true) {
//                        throw new RuntimeException("エラーです");
//                    }
                    return null;
                }, e -> e.advice(deleteOrMoveAdvice()))
                .get();
    }


    // setOnSuccessExpressionString, setOnFailureExpressionString+setSuccessChannelName, setFailureChannelName
    // の組み合わせで指定するサンプル
    //  ・成功時には successFlow.input へ Message を送信する。
    //    successFlow ではファイルを削除する。
    //  ・失敗時には failureFlow.input へ Message を送信する。
    //    failureFlow ではファイルを error ディレクトリへ移動する。

    @Bean
    public Advice successOrFailureChannelAdvice() {
        ExpressionEvaluatingRequestHandlerAdvice advice
                = new ExpressionEvaluatingRequestHandlerAdvice();
        advice.setOnSuccessExpressionString("payload");
        advice.setSuccessChannelName("successFlow.input");
        // setOnFailureExpressionString に "payload" と記述しても Failure 用の MessageChannel には
        // File クラスではなく MessageHandlingExpressionEvaluatingAdviceException クラスの payload が渡される
        advice.setOnFailureExpressionString("payload");
        advice.setFailureChannelName("failureFlow.input");
        advice.setTrapException(true);
        return advice;
    }

    @Bean
    public IntegrationFlow in07Flow() {
        return IntegrationFlows
                .from(s -> s.file(new File(EIPAPP_ADVICE_ROOT_DIR + "/in07"))
                        , e -> e.poller(Pollers.fixedDelay(1000)))
                .<File>handle((p, h) -> {
                    log.info("★★★ " + p.getAbsolutePath());
//                    if (true) {
//                        throw new RuntimeException("エラーです");
//                    }
                    return null;
                }, e -> e.advice(successOrFailureChannelAdvice()))
                .get();
    }

    @Bean
    public IntegrationFlow successFlow() {
        return f -> f
                .<File>handle((p, h) -> {
                    // ファイルを削除する
                    try {
                        Files.delete(Paths.get(p.getAbsolutePath()));
                        log.info("ファイルを削除しました ( {} )", p.getAbsolutePath());
                    } catch (IOException e) {
                        throw new RuntimeException(e);
                    }
                    return null;
                });
    }

    @Bean
    public IntegrationFlow failureFlow() {
        return f -> f
                .<ExpressionEvaluatingRequestHandlerAdvice.MessageHandlingExpressionEvaluatingAdviceException>
                        handle((p, h) -> {
                    // MessageHandlingExpressionEvaluatingAdviceException クラスの payload から
                    // Exception 発生前の File クラスの payload を取得する
                    File file = (File) p.getEvaluationResult();

                    // ファイルを error ディレクトリへ移動する
                    Path src = Paths.get(file.getAbsolutePath());
                    Path dst = Paths.get(EIPAPP_ADVICE_ROOT_DIR + "/error/" + file.getName());
                    try {
                        Files.move(src, dst, StandardCopyOption.REPLACE_EXISTING);
                        log.info("ファイルを移動しました ( {} --> {} )"
                                , src.toAbsolutePath(), dst.toAbsolutePath());
                    } catch (IOException e) {
                        throw new RuntimeException(e);
                    }
                    return null;
                });
    }


    // setOnSuccessExpressionString, setOnFailureExpressionString で Success, Failure 用の
    // MessageChannel に渡す payload の型を変更するサンプル
    //  ・元の File クラスの payload から String クラスの payload の Message に変換して
    //    Success, Failure 用の MessageChannel に送信する

    @Bean
    public Advice convertFileToStringAdvice() {
        ExpressionEvaluatingRequestHandlerAdvice advice
                = new ExpressionEvaluatingRequestHandlerAdvice();
        advice.setOnSuccessExpressionString("payload + ' の処理に成功しました。'");
        advice.setSuccessChannelName("printFlow.input");
        advice.setOnFailureExpressionString(
                "payload + ' の処理に失敗しました ( ' + #exception.class.name + ', ' + #exception.cause.message + ' )'");
        advice.setFailureChannelName("printFlow.input");
        advice.setTrapException(true);
        return advice;
    }

    @Bean
    public IntegrationFlow in08Flow() {
        return IntegrationFlows
                .from(s -> s.file(new File(EIPAPP_ADVICE_ROOT_DIR + "/in08"))
                        , e -> e.poller(Pollers.fixedDelay(1000)))
                .<File>handle((p, h) -> {
                    log.info("★★★ " + p.getAbsolutePath());
//                    if (true) {
//                        throw new RuntimeException("エラーです");
//                    }
                    return null;
                }, e -> e.advice(convertFileToStringAdvice()))
                .get();
    }

    @Bean
    public IntegrationFlow printFlow() {
        return f -> f
                // setFailureChannelName(...) の指定で転送された Message は
                // MessageHandlingExpressionEvaluatingAdviceException クラスなので、
                // .transform(...) で SpEL を利用して元の String を取得する
                .transform("payload instanceof T(org.springframework.integration.handler.advice.ExpressionEvaluatingRequestHandlerAdvice$MessageHandlingExpressionEvaluatingAdviceException)"
                        + " ? payload.evaluationResult : payload")
                .handle((p, h) -> {
                    System.out.println("●●● " + p);
                    return null;
                });

    }


    // RequestHandlerRetryAdvice と ExpressionEvaluatingRequestHandlerAdvice を一緒に指定するサンプル
    //  ・RequestHandlerRetryAdvice に指定する RetryTemplate には FlowConfig.java に書いた
    //    simpleAndFixedRetryTemplate Bean を使用する

    @Autowired
    @Qualifier("simpleAndFixedRetryTemplate")
    private RetryTemplate simpleAndFixedRetryTemplate;

    @Bean
    public IntegrationFlow in09Flow() {
        RequestHandlerRetryAdvice retryAdvice = new RequestHandlerRetryAdvice();
        retryAdvice.setRetryTemplate(this.simpleAndFixedRetryTemplate);
        retryAdvice.setRecoveryCallback(context -> {
            // リトライが全て失敗するとこの処理が実行される
            MessageHandlingException e = (MessageHandlingException) context.getLastThrowable();
            Message<?> message = ((MessageHandlingException) context.getLastThrowable()).getFailedMessage();
            File payload = (File) message.getPayload();
            log.error("●●● " + e.getRootCause().getClass().getName());
            log.error("●●● " + payload.getName());
            // 例外を再 throw して ExpressionEvaluatingRequestHandlerAdvice の失敗時の処理
            // が実行されるようにする
            throw e;
        });

        return IntegrationFlows
                .from(s -> s.file(new File(EIPAPP_ADVICE_ROOT_DIR + "/in09"))
                        , e -> e.poller(Pollers.fixedDelay(1000)))
                .<File>handle((p, h) -> {
                    RetryContext retryContext = RetrySynchronizationManager.getContext();
                    log.info("★★★ リトライ回数 = " + retryContext.getRetryCount());

                    // 例外を throw して必ずリトライさせる
                    if (true) {
                        throw new RuntimeException("エラーです");
                    }
                    return null;
                }, e -> e
                        // RequestHandlerRetryAdvice と ExpressionEvaluatingRequestHandlerAdvice
                        // を一緒に指定する場合には RequestHandlerRetryAdvice を後に書くこと。
                        // 最初に書くとリトライしてくれない。
                        .advice(successOrFailureChannelAdvice())
                        .advice(retryAdvice))
                .get();
    }

}

履歴

2017/02/05
初版発行。