かんがるーさんの日記

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

Spring Boot 1.5.x の Web アプリを 2.0.x へバージョンアップする ( その10 )( Tomcat connection Pool → HikariCP に変更する )

概要

記事一覧はこちらです。

Spring Boot 1.5.x の Web アプリを 2.0.x へバージョンアップする ( その9 )( gradle-errorprone-plugin を 0.0.16 → 0.6 にバージョンアップ。。。しようと思いましたが止めました ) の続きです。

参照したサイト・書籍

目次

  1. build.gradle を変更する
  2. application.properties を変更する
  3. logback-spring.xml を変更する
  4. dataSource Bean の実装を変更する
  5. clean タスク → Rebuild Project → build タスクを実行する
  6. JMX への登録状況を確認する

手順

build.gradle を変更する

dependencies block から implementation("org.apache.tomcat:tomcat-jdbc") を削除します。

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

application.properties を変更する

src/main/resources/application.properties を以下のように変更します。

spring.datasource.hikari.jdbc-url=jdbc:postgresql://localhost/ksbylending
spring.datasource.hikari.username=ksbylending_user
spring.datasource.hikari.password=xxxxxxxx
spring.datasource.hikari.driver-class-name=org.postgresql.Driver
spring.datasource.hikari.leak-detection-threshold=5000
spring.datasource.hikari.register-mbeans=true

src/main/resources/application-product.properties から以下の4行を削除します。

  • spring.datasource.tomcat.initialSize=10
  • spring.datasource.tomcat.maxActive=100
  • spring.datasource.tomcat.maxIdle=100
  • spring.datasource.tomcat.minIdle=10

logback-spring.xml を変更する

Tomcat connection pool の SlowQueryReport interceptor がなくなりましたので、<if condition='"${spring.profiles.active}" == "product" &amp;&amp; "${slowquery.logging.file}" != ""'> ... </if> の設定を削除します。

dataSource Bean の実装を変更する

src/main/java/ksbysample/webapp/lending/config/ApplicationConfig.java の以下の点を変更します。

    @Bean
    @ConfigurationProperties("spring.datasource.hikari")
    public DataSource dataSource() {
        return DataSourceBuilder.create()
                .type(HikariDataSource.class)
                .build();
    }
  • @ConfigurationProperties("spring.datasource.tomcat")@ConfigurationProperties("spring.datasource.hikari") に変更します。
  • .type(org.apache.tomcat.jdbc.pool.DataSource.class).type(HikariDataSource.class) に変更します。

clean タスク → Rebuild Project → build タスクを実行する

clean タスク → Rebuild Project → build タスクを実行すると BUILD SUCCESSFUL が出力されました。

f:id:ksby:20181010224407p:plain

JMX への登録状況を確認する

まずは IntelliJ IDEA から spring.profiles.acive=develop で起動した後、jconsole を起動して確認します。

f:id:ksby:20181010224802p:plain f:id:ksby:20181010225209p:plain f:id:ksby:20181010225240p:plain

次に ksbysample-webapp-lending-2.0.4-RELEASE.jar を作成して、D:\webapps\ksbysample-webapp-lending\bat\webapp_startup.bat から spring.profiles.acive=product で起動した後、jconsole を起動して確認します。

f:id:ksby:20181010225609p:plain f:id:ksby:20181010225740p:plain f:id:ksby:20181010225821p:plain

表示が違いますが、spring.profiles.acive=develop の時は implementation("com.integralblue:log4jdbc-spring-boot-starter:1.0.2") が適用されているためと思われます。src/main/resources/application.properties に spring.autoconfigure.exclude=com.integralblue.log4jdbc.spring.Log4jdbcAutoConfiguration を追加してから jconsole で見てみます。

f:id:ksby:20181010234216p:plain

今度は spring.profiles.acive=product で起動した時と同じ表示になりました。

D:\webapps\ksbysample-webapp-lending\bat\webapp_startup.bat から spring.profiles.acive=product で起動した時に本当にコネクションプーリングをしているのかを PostgreSQL の管理ツール pgAdmin4 で見てみると、ログイン画面からログインした直後に 10 セッション接続していることが確認できました。

f:id:ksby:20181011003013p:plain

また、Spring Boot + npm + Geb で入力フォームを作ってテストする ( その75 )( コネクションプーリング用ライブラリを Tomcat connection pool → HikariCP に切り替える ) の時は @EnableMBeanExport(registration = RegistrationPolicy.IGNORE_EXISTING) を付けないと jar から起動した時に org.springframework.jmx.export.UnableToRegisterMBeanException: Unable to register MBean [HikariDataSource (HikariPool-1)] with key 'dataSource'; ... の例外が発生しましたが、今回は付けなくても例外は発生しませんでした。例外が発生する/しないの違いがよく分かりませんでした。。。

履歴

2018/10/11
初版発行。

Spring Boot 1.5.x の Web アプリを 2.0.x へバージョンアップする ( その9 )( gradle-errorprone-plugin を 0.0.16 → 0.6 にバージョンアップ。。。しようと思いましたが止めました )

概要

記事一覧はこちらです。

Spring Boot 1.5.x の Web アプリを 2.0.x へバージョンアップする ( その8 )( 一旦動作確認し、動作しない点があれば修正する ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • gradle-errorprone-plugin の 0.6 がリリースされていて、設定方法も大きく変わっていました。0.0.16 → 0.6 にバージョンアップしてみます。。。が、出来なかったという内容です。

参照したサイト・書籍

目次

  1. build.gradle を変更する
  2. build タスクを実行してみる

手順

build.gradle を変更する

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

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 "com.github.spotbugs" version "1.6.4"
    id "pmd"
    id "net.ltgt.errorprone" version "0.6"
    id "de.undercouch.download" version "3.4.3"
}

..........

[compileJava, compileTestGroovy, compileTestJava]*.options*.encoding = "UTF-8"
[compileJava, compileTestGroovy, compileTestJava]*.options*.compilerArgs << "-Xlint:all,-options,-processing,-path"
tasks.withType(JavaCompile).configureEach {
    options.errorprone.errorproneArgs << "-Xep:RemoveUnusedImports:WARN"
    options.errorprone.errorproneArgs << "-Xep:InsecureCryptoUsage:OFF"
    options.errorprone.errorproneArgs << "-Xep:ParameterName:OFF"
}

..........

dependencies {
    ..........

    // for Error Prone ( http://errorprone.info/ )
    errorprone("com.google.errorprone:error_prone_core:${errorproneVersion}")
    compileOnly("com.google.errorprone:error_prone_annotations:${errorproneVersion}")
    // JDK 8 support
    errorproneJavac("com.google.errorprone:javac:9+181-r4173-1")

    ..........
}
  • id "net.ltgt.errorprone" version "0.0.16"id "net.ltgt.errorprone" version "0.6" に変更します。
  • [compileJava, compileTestGroovy, compileTestJava]*.options*.compilerArgs = [ ... ] で compile と error-prone のオプションを指定していたのを [compileJava, compileTestGroovy, compileTestJava]*.options*.compilerArgs << "-Xlint:all,-options,-processing,-path"tasks.withType(JavaCompile).configureEach { ... } に分けます。
  • dependencies block の以下の点を変更します。
    • errorproneJavac("com.google.errorprone:javac:9+181-r4173-1") を追加します。

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

build タスクを実行してみる

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

f:id:ksby:20181006065051p:plain f:id:ksby:20181006065156p:plain

compileTestJava タスクでエラーが 34個出て build は失敗しました。エラーの内容を見ると、テストコードで Lombok を使用して自動生成させている Getter/Setter を errorproneJavac が認識できていないようです。options.errorprone.disableWarningsInGeneratedCode = true とかを指定してもエラーはなくなりませんでした。テストコードで Lombok を使用しないように変更すればエラーはなくなるのですが、そうまでして 0.6 にバージョンアップするか迷います。。。

https://github.com/tbroyer/gradle-errorprone-plugin には Error Prone requires at least a JDK 9 compiler. という記述があり JDK 9 以上が本来想定されている環境のようですので、今回は gradle-errorprone-plugin のバージョンアップは止めることにします。JDK を 8 → 11 に変える時に再度試そうと思います。

履歴

2018/10/06
初版発行。

Spring Boot 1.5.x の Web アプリを 2.0.x へバージョンアップする ( その8 )( 一旦動作確認し、動作しない点があれば修正する )

概要

記事一覧はこちらです。

Spring Boot 1.5.x の Web アプリを 2.0.x へバージョンアップする ( その7 )( src/main/resources/static の下の css や js ファイルにアクセスできない原因とは? ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • ここまでいろいろ変更したので Tomcat を起動して画面から正常に動作するか確認し、動作しない点があれば修正します。

参照したサイト・書籍

目次

  1. 動作確認1
  2. RabbitMQ へのメッセージ送信時に correlationId を付けるよう修正する
  3. 動作確認2
  4. 次回は。。。

手順

動作確認1

動作確認前に DB のデータを以下の状態にします。

  • user_info, user_role テーブルのデータは開発時のままにします。
  • lending_app, lending_book, library_forsearch テーブルのデータはクリアします。

メールを受信するので smtp4dev を起動します。

f:id:ksby:20181003001818p:plain

Tomcat を起動後、以下の手順で動作確認します。

  • ブラウザを起動して http://localhost:8080/ にアクセスしてログイン画面を表示します。tanaka.taro@sample.com / taro でログインします。

    f:id:ksby:20181003003418p:plain

  • 検索対象図書館登録画面が表示されます。"東京都" で検索した後、一覧表示されている図書館から「国立国会図書館東京本館」を選択します。

    f:id:ksby:20181003003550p:plain

  • ログアウトします。

  • ログイン画面に戻るので suzuki.hanako@test.co.jp / hanako でログインします。
  • 貸出希望書籍 CSV ファイルアップロード画面が表示されます。以下の内容が記述された CSV ファイルをアップロードします。

    "ISBN","書名"
    "978-4-7741-6366-6","GitHub実践入門"
    "978-4-7741-5377-3","JUnit実践入門"
    "978-4-7973-8014-9","Java最強リファレンス"
    "978-4-7973-4778-4","アジャイルソフトウェア開発の奥義"
    "978-4-87311-704-1","Javaによる関数型プログラミング"

    f:id:ksby:20181003003808p:plain

    f:id:ksby:20181003004102p:plain

この後メールが送信されてくるはずですが、メールは送信されず NullPointerException が延々と発生していました。

f:id:ksby:20181003004454p:plain

RabbitMQ へのメッセージ送信時に correlationId を付けるよう修正する

NullPointerException が発生している org.springframework.amqp.core.MessageProperties.hashCode(MessageProperties.java:528) をクリックしてみると result = prime * result + this.correlationId.hashCode(); のところでした。Spring Boot 1.5.x の Web アプリを 2.0.x へバージョンアップする ( その4 )( AbstractJsonpResponseBodyAdvice を削除し、失敗しているテストを成功させる ) に書きましたが、2.0 にバージョンアップするとメッセージ送信時に correlationId を必ずセットしないといけなくなっているのですが、セットしていないのが原因のようです。

f:id:ksby:20181003004621p:plain

correlationId は自動で一意の値を割り当ててもらえれば良さそうに思えたので何か方法がないか調べたところ、Spring AMQP - 3.3.3 Stock Trading のページに UUID.randomUUID().toString() を使う方法が記載されていました。今回はこの方法を使うことにします。

メッセージを送信している src/main/java/ksbysamplevwebapp/lending/service/queue/InquiringStatusOfBookQueueService.java の sendMessage メソッドを以下のように変更します。

    public void sendMessage(Long lendingAppId) {
        InquiringStatusOfBookQueueMessage body = new InquiringStatusOfBookQueueMessage();
        body.setLendingAppId(lendingAppId);
        Message message = converter.toMessage(body
                , MessagePropertiesBuilder.newInstance().setCorrelationId(UUID.randomUUID().toString()).build());
        rabbitTemplate.send(Constant.QUEUE_NAME_INQUIRING_STATUSOFBOOK, message);
    }
  • Message message = converter.toMessage(body, MessagePropertiesBuilder.newInstance().setCorrelationId(UUID.randomUUID().toString()).build()); を追加します。
  • メッセージ送信時に呼び出すメソッドを rabbitTemplate.convertAndSendrabbitTemplate.send に変更します。

動作確認2

RabbitMQ、Tomcat を再起動して最初からやり直します。

  • 貸出希望書籍 CSV ファイルアップロード画面で CSV ファイルをアップロードします。

f:id:ksby:20181003064514p:plain

  • 「貸出状況を確認しました」のメールが送信されるので、メールに記述されている URL にアクセスします。

f:id:ksby:20181003064631p:plain f:id:ksby:20181003064730p:plain

  • 貸出申請画面が表示されます。3冊程「申請する」を選択して申請します。

f:id:ksby:20181003064947p:plain f:id:ksby:20181003065102p:plain

  • ログアウトします。
  • 「貸出申請がありました」のメールが送信されるので、メールに記述されている URL にアクセスします。ログイン画面が表示されるので、tanaka.taro@sample.com / taro でログインします。

f:id:ksby:20181003065153p:plain f:id:ksby:20181003065244p:plain

  • 貸出承認画面が表示されます。「承認」あるいは「却下」を選択して確定させます。

f:id:ksby:20181003065505p:plain f:id:ksby:20181003065546p:plain

  • ログアウトします。
  • 「貸出申請が承認・却下されました」のメールが送信されるので、メールに記述されている URL にアクセスします。ログイン画面が表示されるので、suzuki.hanako@test.co.jp / hanako でログインします。

f:id:ksby:20181003065701p:plain f:id:ksby:20181003065749p:plain

  • 貸出申請結果確認画面が表示されるので内容を確認します。

f:id:ksby:20181003065852p:plain

動作しなかったのは RabbitMQ へのメッセージ送受信のところだけでした。

次回は。。。

以下の順に進める予定です。

  • gradle-errorprone-plugin を 0.0.16 → 0.6 へバージョンアップします。知らないうちに plugin の方が大きく変更されていました。
  • Tomcat connection Pool → HikariCP に変更します。

履歴

2018/10/03
初版発行。

Spring Boot 1.5.x の Web アプリを 2.0.x へバージョンアップする ( その7 )( src/main/resources/static の下の css や js ファイルにアクセスできない原因とは? )

概要

記事一覧はこちらです。

Spring Boot 1.5.x の Web アプリを 2.0.x へバージョンアップする ( その6 )( FindBugs 3.0.1 → SpotBugs 3.1.7 に切り替える ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • ここまでいろいろ変更したので Tomcat を起動して画面から正常に動作するか確認しようとしたところ、最初のログイン画面が正常に表示されていませんでした。
    • CSS が適用されていないようなのですが、原因を調査します。

参照したサイト・書籍

目次

  1. Spring Boot 1.5 の時と 2.0 の時の CSS のリクエスト・レスポンスを比較してみる
  2. src/main/resources/static の下のファイルにアクセスできない原因を調査・解消する
  3. src/main/resources/static の下のファイルにキャッシュ期間を設定する

手順

Spring Boot 1.5 の時と 2.0 の時の CSS のリクエスト・レスポンスを比較してみる

1.5 の時はログイン画面は正常に表示されていたので、1.5 の時と 2.0 の時の CSS のリクエスト・レスポンスを比較してみます。まずは 2.0 から。

Tomcat を起動して DevTools を起動した Chromehttp://localhost:8080/ にアクセスすると、コンソールに MIMEtext/html になっているというエラーメッセージが表示されています。

f:id:ksby:20180929091430p:plain

  • Refused to apply style from '<URL>' because its MIME type ('text/html') is not a supported stylesheet MIME type, and strict MIME checking is enabled.
  • Refused to execute script from 'http://localhost:8080/' because its MIME type ('text/html') is not executable, and strict MIME type checking is enabled.

Fiddler でログイン画面表示時のキャプチャを取得し、/css/bootstrap.min.css のリクエスト・レスポンスを見てみると、以下のようになっていました。

f:id:ksby:20180929091845p:plain

HTTP/1.1 302
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Location: http://localhost:8080/
Content-Length: 0
Date: Sat, 29 Sep 2018 00:18:08 GMT

HTTPステータスコードが 302 で Location: http://localhost:8080/ ヘッダが返ってきていました。なぜかリダイレクトされています。。。

次に 1.5 の時を見てみます。master ブランチへ切り替えてから build し直して Tomcat を起動し、ブラウザでアクセスします。画面も正常に表示され、コンソールには何も表示されていません。

f:id:ksby:20180929194301p:plain

Fiddler で /css/bootstrap.min.css のリクエスト・レスポンスを見てみると、以下のようになっていました。

f:id:ksby:20180929194453p:plain

HTTP/1.1 200
Last-Modified: Sat, 29 Sep 2018 10:35:37 GMT
Cache-Control: no-store
Accept-Ranges: bytes
Content-Type: text/css
Content-Length: 117309
Date: Sat, 29 Sep 2018 10:44:10 GMT

HTTPステータスコードは 200 が返ってきます。

状況は分かったので 2.0 の作業用ブランチに戻します。Spring Boot 1.5+Spring Security の構成の時には src/main/resources/static の下のファイルにアクセスできた(200 が返ってきていた)のですが、Spring Boot 2.0+Spring Security の構成だとアクセスできなくなっているようです。

src/main/resources/static の下のファイルにアクセスできない原因を調査・解消する

stackoverflow で Serving static web resources in Spring Boot & Spring Security application という QA を見つけました。

この中の回答によると、1.5 以下では public/** or static/** がデフォルトで許可されていましたが、2.0 ではデフォルトで全て許可されなくなったそうです。

src/main/java/ksbysample/webapp/lending/config/WebSecurityConfig.java の以下の点を変更します。

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                // 認証の対象外にしたいURLがある場合には、以下のような記述を追加します
                // 複数URLがある場合はantMatchersメソッドにカンマ区切りで対象URLを複数列挙します
                // .antMatchers("/country/**").permitAll()
                .requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll()
                .antMatchers("/fonts/**").permitAll()
                ..........
  • .requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll() を追加します。

Tomcat を起動してブラウザでアクセスすると、今度は画面が正常に表示されました。

f:id:ksby:20180930002750p:plain

Fiddler で見ると HTTPステータスコードの 200 が返ってきています。

f:id:ksby:20180930083514p:plain

HTTP/1.1 200
Last-Modified: Sat, 29 Sep 2018 23:28:18 GMT
Accept-Ranges: bytes
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Type: text/css
Content-Length: 117309
Date: Sat, 29 Sep 2018 23:34:21 GMT

src/main/resources/static の下のファイルにキャッシュ期間を設定する

※以降の変更はコミットはしません。

src/main/resources/static の下のファイルにアクセスできるようになりましたが、Cache-Control: no-cache, no-store, max-age=0, must-revalidate 等の HTTP ヘッダが付いていてキャッシュが無効になっています。css や js はキャッシュを設定したいことがあるので、設定してみます。

src/main/java/ksbysample/webapp/lending/config/WebMvcConfig.java に addResourceHandlers メソッドを override して設定します。

package ksbysample.webapp.lending.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.CacheControl;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.thymeleaf.spring5.SpringTemplateEngine;

import java.util.concurrent.TimeUnit;

/**
 * ???
 */
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    /**
     * Thymeleaf 3 のパフォーマンスを向上させるために SpEL コンパイラを有効にする
     *
     * @param templateEngine {@link SpringTemplateEngine} オブジェクト
     */
    @Autowired
    public void configureThymeleafSpringTemplateEngine(SpringTemplateEngine templateEngine) {
        templateEngine.setEnableSpringELCompiler(true);
    }

    /**
     * css, js のキャッシュ期間を設定する(css は 1時間、js は 15分)
     *
     * @param registry {@link ResourceHandlerRegistry} オブジェクト
     */
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/css/**")
                .addResourceLocations("classpath:/static/css/")
                .setCacheControl(CacheControl.maxAge(1, TimeUnit.HOURS).cachePublic());

        registry.addResourceHandler("/js/**")
                .addResourceLocations("classpath:/static/js/")
                .setCacheControl(CacheControl.maxAge(15, TimeUnit.MINUTES).cachePublic());
    }

}

Tomcat を起動して css, js のレスポンスを見るとキャッシュ期間が設定されていることが確認できます。

/cssCache-Control: max-age=3600, public とキャッシュ期間が1時間で設定されています。

f:id:ksby:20180930092201p:plain

HTTP/1.1 200
Last-Modified: Sat, 29 Sep 2018 23:28:18 GMT
Cache-Control: max-age=3600, public
Accept-Ranges: bytes
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
X-Frame-Options: DENY
Content-Type: text/css
Content-Length: 117309
Date: Sun, 30 Sep 2018 00:20:13 GMT

/js は Cache-Control: max-age=900, public とキャッシュ期間が15分で設定されています。

f:id:ksby:20180930092307p:plain

HTTP/1.1 200
Last-Modified: Sat, 29 Sep 2018 23:28:18 GMT
Cache-Control: max-age=900, public
Accept-Ranges: bytes
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
X-Frame-Options: DENY
Content-Type: application/javascript
Content-Length: 84349
Date: Sun, 30 Sep 2018 00:20:13 GMT

履歴

2018/09/30
初版発行。

Spring Boot 1.5.x の Web アプリを 2.0.x へバージョンアップする ( その6 )( FindBugs 3.0.1 → SpotBugs 3.1.7 に切り替える )

概要

記事一覧はこちらです。

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

参照したサイト・書籍

目次

  1. build.gradle を変更する
  2. config/findbugs/findbugs-exclude.xml を削除する
  3. clean タスク → Rebuild Project → build タスクを実行する
  4. 出力された警告を解消する
    1. org.springframework.validation.Errors.rejectValue(String, String) の 非 null パラメータに null を渡しています。(Bug type NP_NONNULL_PARAM_VIOLATION)
    2. null になっている可能性があるメソッドの戻り値を利用しています。(Bug type NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE)
    3. target は,非 null でなければならないが null 可能としてマークされています。(Bug type NP_PARAMETER_MUST_BE_NONNULL_BUT_MARKED_AS_NULLABLE)
  5. 再度 clean タスク → Rebuild Project → build タスクを実行する
  6. 次回は。。。

手順

build.gradle を変更する

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

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 "com.github.spotbugs" version "1.6.4"
    id "pmd"
    id "net.ltgt.errorprone" version "0.0.16"
    id "de.undercouch.download" version "3.4.3"
}

..........

spotbugs {
    toolVersion = "3.1.7"
    ignoreFailures = true
    effort = "max"
    spotbugsTest.enabled = false
}
tasks.withType(com.github.spotbugs.SpotBugsTask) {
    reports {
        xml.enabled = false
        html.enabled = true
    }
}

..........

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"
    def spotbugsVersion = "3.1.7"

    ..........

    // for SpotBugs
    compileOnly("com.github.spotbugs:spotbugs:${spotbugsVersion}")
    compileOnly("net.jcip:jcip-annotations:1.0")
    compileOnly("com.github.spotbugs:spotbugs-annotations:${spotbugsVersion}")
    testImplementation("com.google.code.findbugs:jsr305:3.0.2")
}
  • plugins block の以下の点を変更します。
    • id "findbugs"id "com.github.spotbugs" version "1.6.4" に変更します。
  • タスク名を findbugsspotbugs に変更し、タスク内の以下の点を変更します。
    • toolVersion = "3.0.1"toolVersion = "3.1.7" に変更します。
    • sourceSets = [project.sourceSets.main] を削除します。
    • 警告を無視したい場合には @SuppressFBWarnings を付ければ回避できるので、excludeFilter = file("${rootProject.projectDir}/config/spotbugs/spotbugs-exclude.xml") を削除します。
    • spotbugsTest.enabled = false を追加します。
    • tasks.withType(FindBugs)tasks.withType(com.github.spotbugs.SpotBugsTask) に変更します。
  • dependencies block の以下の点を変更します。
    • 以下の4行を追加します。
      • def spotbugsVersion = "3.1.7"
      • compileOnly("com.github.spotbugs:spotbugs:${spotbugsVersion}")
      • compileOnly("net.jcip:jcip-annotations:1.0")
      • compileOnly("com.github.spotbugs:spotbugs-annotations:${spotbugsVersion}")
    • testImplementation("com.google.code.findbugs:jsr305:3.0.2")compileOnly("com.github.spotbugs:spotbugs-annotations:${spotbugsVersion}") の下に移動します。

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

config/findbugs/findbugs-exclude.xml を削除する

使用しなくなったので、config/findbugs/findbugs-exclude.xml を削除します。

clean タスク → Rebuild Project → build タスクを実行する

clean タスク → Rebuild Project → build タスクを実行してみると spotbugsMain タスクで SpotBugs rule violations were found. のメッセージが出力されました。

f:id:ksby:20180926011753p:plain

build/reports/spotbugs/main.html を Firefox で開くと、High Priority Warnings が 4件、Medium Priority Warnings が 12件の合計 16件の警告が出ています。

f:id:ksby:20180926012138p:plain

出力された警告を解消する

org.springframework.validation.Errors.rejectValue(String, String) の 非 null パラメータに null を渡しています。(Bug type NP_NONNULL_PARAM_VIOLATION)

警告が出ている原因の箇所は src/main/java/ksbysample/webapp/lending/web/lendingapp/LendingappFormValidator.java の以下の画像の赤線で囲んだところと、

f:id:ksby:20180926065207p:plain

src/main/java/ksbysample/webapp/lending/web/lendingapproval/LendingapprovalFormValidator.java の以下の画像の赤線で囲んだところでした。

f:id:ksby:20180926073243p:plain

src/main/java/ksbysample/webapp/lending/web/lendingapp/LendingappFormValidator.java は以下のように対応します。

  • errors.rejectValue の第2引数 errcode に null を渡していましたが、非 null パラメータに null を渡しているので警告が出ていました。その後に呼び出している errors.reject の第1引数にしている errcode を渡すようにします(同じエラーのため)。
  • errors.reject の第2引数 defaultMessage に null を渡していましたが、非 null パラメータに null を渡しているので警告が出ていました。errors.reject は第1引数のみのものも定義されているので、第2引数の null を削除します。

f:id:ksby:20180926071224p:plain

src/main/java/ksbysample/webapp/lending/web/lendingapproval/LendingapprovalFormValidator.java は src/main/resources/messages_ja_JP.properties にメッセージを定義して errors.rejectValue で呼び出すように修正します。

LendingapprovalParamForm.lendingAppId.emptyerr=貸出申請IDが指定されていません。
LendingapprovalForm.lendingApp.nodataerr=指定された貸出申請IDでは貸出申請されておりません。
LendingapprovalForm.applyingBookFormList.approvalResult.notAllCheckedErr=全ての書籍で承認か却下を選択してください。
LendingapprovalForm.applyingBookFormList.approvalReason.empty=却下理由が入力されていません。
  • LendingapprovalForm.applyingBookFormList.approvalReason.empty=却下理由が入力されていません。 を追加します。

f:id:ksby:20180926220329p:plain

null になっている可能性があるメソッドの戻り値を利用しています。(Bug type NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE

3つのソースで警告が出ており、1つ目は src/main/java/ksbysample/webapp/lending/aspect/logging/RequestAndResponseLogger.java の以下の画像の赤線で囲んだところでした。

f:id:ksby:20180926223026p:plain

ここは Optional を使って以下のように変更します。

f:id:ksby:20180926223747p:plain

2つ目は src/main/java/ksbysample/webapp/lending/service/calilapi/CalilApiService.java の以下の画像の赤線で囲んだところでした。

f:id:ksby:20180926230100p:plain

org.springframework.http.HttpEntity#getBody を見ると @Nullable アノテーションが付いていて、null の場合がありうるため警告が出ていました。

f:id:ksby:20180926230320p:plain

response.getBody() を変数に取得して null チェックをするように修正します。

f:id:ksby:20180926235422p:plain

src/main/resources/messages_ja_JP.properties に CalilApiService.checkapi.response.emptybody=蔵書検索APIのレスポンスボディが空です。 のメッセージを追加しています。

3つ目は src/main/java/ksbysample/webapp/lending/service/openweathermapapi/OpenWeatherMapApiService.java の以下の画像の赤線で囲んだところでした。

f:id:ksby:20180928010950p:plain

OpenWeatherMapApi 関連は JSONP のサンプルとして実装しましたが、AbstractJsonpResponseBodyAdvice も非推奨になったので全て削除します。以下のパッケージを削除します。

  • ksbysample.webapp.lending.service.openweathermapapi
  • ksbysample.webapp.lending.webapi.weather
  • ksbysample.webapp.lending.service.openweathermapapi

target は,非 null でなければならないが null 可能としてマークされています。(Bug type NP_PARAMETER_MUST_BE_NONNULL_BUT_MARKED_AS_NULLABLE)

警告が出ているのは以下の3つのソースで、

  • src/main/java/ksbysample/webapp/lending/web/booklist/UploadBooklistFormValidator.java
  • src/main/java/ksbysample/webapp/lending/web/lendingapp/LendingappFormValidator.java
  • src/main/java/ksbysample/webapp/lending/web/lendingapproval/LendingapprovalFormValidator.java

以下の画像の赤線で囲んだところでした。

f:id:ksby:20180928073540p:plain

override 元の org.springframework.validation.Validator#validate では引数 target に @Nullable アノテーションが付いていますが、override した後では NonNull の想定で実装していて矛盾があるという警告でした。

f:id:ksby:20180928073818p:plain

Assert.notNull(target, "target must not be null"); を追加して null の場合には例外が throw されるようにします。

f:id:ksby:20180929004531p:plain

再度 clean タスク → Rebuild Project → build タスクを実行する

再度 clean タスク → Rebuild Project → build タスクを実行してみると spotbugsMain タスクで何も出力されなくなりました。

f:id:ksby:20180929013923p:plain

次回は。。。

いろいろ対応したので一旦 Tomcat を起動して画面が表示できるか確認してみたのですが、画面が正常に表示されませんでした。CSS が適用されていないようです。原因を調査することにします。

f:id:ksby:20180929015332p:plain

履歴

2018/09/29
初版発行。

Spring Boot + Spring Integration でいろいろ試してみる ( その29 )( Docker Compose でサーバを構築する、FTP+SFTPサーバ編 )

概要

記事一覧はこちらです。

Spring Integration のアプリケーションで使用するサーバを Docker Compose で構築します。

D:\eipapp\ksbysample-eipapp-dockerserver
├ ftp
│ ├ download
│ ├ upload
│ └ uploading
└ sftp
   ├ download
   ├ upload
   └ uploading

参照したサイト・書籍

目次

  1. FTP サーバを構築する
    1. docker-compose.yml を変更する
    2. サーバを起動する
    3. 動作確認
  2. SFTP サーバを構築する
    1. users.conf を作成する
    2. docker-compose.yml を変更する
    3. サーバを起動する
    4. 動作確認
  3. FTP でアップロードするサンプルを作成する
  4. FTP でダウンロードするサンプルを作成する
  5. SFTP でアップロード・ダウンロードするサンプルを作成する

手順

FTP サーバを構築する

docker-compose.yml を変更する

docker-compose.yml の以下の点を変更します。

version: '3'

services:
  ..........

  # stilliard/pure-ftpd
  # https://hub.docker.com/r/stilliard/pure-ftpd/
  #
  # 起動した pure-ftpd のコンテナ(ftp-server) にアクセスする場合には以下のコマンドを実行する
  # docker exec -it ftp-server /bin/bash
  #
  ftp-server:
    image: stilliard/pure-ftpd:latest
    container_name: ftp-server
    ports:
      - "21:21"
      - "30000-30009:30000-30009"
    environment:
      - PUBLICHOST=localhost
      - FTP_USER_NAME=test
      - FTP_USER_PASS=12345678
      - FTP_USER_HOME=/home/ftpusers/test
    restart: always
  • servicesftp-server を追加します。
  • FTPユーザは環境変数 FTP_USER_NAME, FTP_USER_PASS, FTP_USER_HOME で設定します。
  • この方法だと1ユーザしか設定できません。複数ユーザを作成したい場合には https://download.pureftpd.org/pure-ftpd/doc/README.Virtual-Users を参考に作成します。
  • image は stilliard/pure-ftpd:hardened ではなく stilliard/pure-ftpd:latest を指定します。stilliard/pure-ftpd:hardened だと Spring Integration でファイルをアップロードできません(Spring Integration はファイルアップロード時にファイル名の最後に .writing という文字列を付けてアップロードし、完了後にリネームで取り除くのですが、stilliard/pure-ftpd:hardened だとリネームができませんでした)。

サーバを起動する

コマンドプロンプトを起動し docker-compose.yml のあるディレクトリへ移動して docker-compose up -d コマンドを実行して起動します。

f:id:ksby:20180923143047p:plain

IntelliJ IDEA の docker plugin を見ると ftp-server コンテナが起動していることが確認できます。

f:id:ksby:20180923143153p:plain

動作確認

WinSCP で接続してみます。「ログイン」ダイアログでホスト名、ユーザ名、パスワードを入力して「ログイン」ボタンをクリックします。

f:id:ksby:20180923021517p:plain

無事ログインできました。ファイルのアップロード、ダウンロードも問題なく出来ました。

f:id:ksby:20180923021858p:plain

一旦 docker-compose down コマンドを実行してコンテナを停止・削除します。

SFTP サーバを構築する

users.conf を作成する

プロジェクトの直下に docker/sftp-server/config ディレクトリを作成します。

docker/sftp-server/config の下に users.conf を新規作成し、以下の内容を記述します。

user01:pass01:::upload,download
user02:pass02:::upload,download
user03:pass03:::upload,download
  • 改行コードは LF にします(CRLF だとログインできません)。
  • ログインした直後のルートディレクトリにはファイルをアップロードできません(Permission denied が表示されます、ただしダウンロードは可能)。アップロードしたい場合には、アップロード先のディレクトリ名を users.conf の一番右の場所に記述します(複数ディレクトリを作成したい場合にはカンマで区切ります)。

以下のディレクトリ構成になります。

f:id:ksby:20180923051753p:plain

docker-compose.yml を変更する

docker-compose.yml の以下の点を変更します。

version: '3'

services:
  ..........

  # atmoz/sftp
  # https://hub.docker.com/r/atmoz/sftp/
  #
  # 起動した sftp のコンテナ(sftp-server) にアクセスする場合には以下のコマンドを実行する
  # docker exec -it sftp-server /bin/bash
  #
  sftp-server:
    image: atmoz/sftp
    container_name: sftp-server
    ports:
      - "22:22"
    volumes:
      - ./docker/sftp-server/config/users.conf:/etc/sftp/users.conf:ro

サーバを起動する

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

f:id:ksby:20180923111811p:plain

IntelliJ IDEA の docker plugin を見ると sftp-server コンテナが起動していることが確認できます。

f:id:ksby:20180923144007p:plain

動作確認

WinSCP で接続してみます。「ログイン」ダイアログでホスト名、ユーザ名、パスワードを入力して「ログイン」ボタンをクリックします。

f:id:ksby:20180923112237p:plain

「警告」ダイアログが表示されますので、「更新」ボタンをクリックします。

f:id:ksby:20180923112547p:plain

無事ログインできました。download, upload ディレクトリへファイルのアップロード、ダウンロードも問題なく出来ました。

f:id:ksby:20180923112738p:plain

FTP でアップロードするサンプルを作成する

最初にアプリケーションを起動した時にメールのサンプルが実行されないよう src/main/java/ksbysample/eipapp/dockerserver/flow/MailFlowConfig.java@Configuration アノテーションコメントアウトします。

// このサンプルを実行したい場合には、@Configuration のコメントアウトを外すこと
//@Configuration
public class MailFlowConfig {

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

dependencies {
    implementation('org.springframework.boot:spring-boot-starter-integration')
    implementation("org.springframework.boot:spring-boot-starter-mail")
    implementation('org.springframework.integration:spring-integration-mail')
    implementation('org.springframework.integration:spring-integration-file')
    implementation('org.springframework.integration:spring-integration-ftp')
    implementation("org.apache.commons:commons-lang3")
    testImplementation('org.springframework.boot:spring-boot-starter-test')
}
  • 以下の行を追加します。
    • implementation('org.springframework.integration:spring-integration-file')
    • implementation('org.springframework.integration:spring-integration-ftp')

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

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

package ksbysample.eipapp.dockerserver.flow;

import org.apache.commons.net.ftp.FTPClient;
import org.apache.commons.net.ftp.FTPFile;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.expression.common.LiteralExpression;
import org.springframework.integration.dsl.IntegrationFlow;
import org.springframework.integration.dsl.IntegrationFlows;
import org.springframework.integration.dsl.Pollers;
import org.springframework.integration.file.FileReadingMessageSource;
import org.springframework.integration.file.filters.AcceptAllFileListFilter;
import org.springframework.integration.file.remote.handler.FileTransferringMessageHandler;
import org.springframework.integration.file.remote.session.CachingSessionFactory;
import org.springframework.integration.file.remote.session.SessionFactory;
import org.springframework.integration.ftp.session.DefaultFtpSessionFactory;
import org.springframework.integration.handler.LoggingHandler;
import org.springframework.messaging.support.GenericMessage;

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;

@Configuration
public class FtpFlowConfig {

    private static final String FTP_SERVER = "localhost";
    private static final int FTP_PORT = 21;
    private static final String FTP_USER = "test";
    private static final String FTP_PASSWORD = "12345678";

    private static final String FTP_REMOTE_DIR = "/";
    private static final String FTP_LOCAL_ROOT_DIR = "D:/eipapp/ksbysample-eipapp-dockerserver/ftp";
    private static final String FTP_LOCAL_UPLOAD_DIR = FTP_LOCAL_ROOT_DIR + "/upload";
    private static final String FTP_LOCAL_UPLOADING_DIR = FTP_LOCAL_ROOT_DIR + "/uploading";

    @Bean
    public SessionFactory<FTPFile> ftpSessionFactory() {
        DefaultFtpSessionFactory factory = new DefaultFtpSessionFactory();
        factory.setHost(FTP_SERVER);
        factory.setPort(FTP_PORT);
        factory.setUsername(FTP_USER);
        factory.setPassword(FTP_PASSWORD);
        factory.setClientMode(FTPClient.PASSIVE_LOCAL_DATA_CONNECTION_MODE);
        return new CachingSessionFactory<>(factory);
    }

    /****************************************
     * FTPアップロード処理のサンプル             *
     ****************************************/

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

    @Bean
    public FileTransferringMessageHandler<FTPFile> ftpFileTransferringMessageHandler() {
        FileTransferringMessageHandler<FTPFile> handler
                = new FileTransferringMessageHandler<>(ftpSessionFactory());
        handler.setRemoteDirectoryExpression(new LiteralExpression(FTP_REMOTE_DIR));
        return handler;
    }

    @Bean
    public IntegrationFlow ftpUploadFlow() {
        return IntegrationFlows.from(
                // 200ミリ秒毎に ftp ディレクトリを監視し、ファイルがあれば処理を進める
                ftpUploadFileMessageSource(), c -> c.poller(Pollers.fixedDelay(200)))
                // ファイルを uploading ディレクトリへ移動する
                .<File>handle((p, h) -> {
                    try {
                        Path movedFilePath = Files.move(p.toPath(), Paths.get(FTP_LOCAL_UPLOADING_DIR, p.getName())
                                , StandardCopyOption.REPLACE_EXISTING);
                        return new GenericMessage<>(movedFilePath.toFile(), h);
                    } catch (IOException e) {
                        throw new RuntimeException(e);
                    }
                })
                // FTPサーバにファイルをアップロードする
                .wireTap(f -> f.handle(ftpFileTransferringMessageHandler()))
                .log(LoggingHandler.Level.WARN)
                // アップロードしたファイルを削除する
                .<File>handle((p, h) -> {
                    p.delete();
                    return null;
                })
                .get();
    }

}

動作確認してみます。最初に docker-compose up -dFTP サーバを起動し、WinSCP で何もアップロードされていないことを確認します。

f:id:ksby:20180923150702p:plain

アプリケーションを起動後、D:\eipapp\ksbysample-eipapp-dockerserver\ftp\upload の下にファイルを1つ配置します。ファイルをアップロードしたことを示すログが出力されることが確認できます。

f:id:ksby:20180923150938p:plain

WinSCP で見ると配置したファイルがアップロードされていることが確認できます。

f:id:ksby:20180923151032p:plain

アップロードされたファイルを削除し、アプリケーションを停止します。

FTP でダウンロードするサンプルを作成する

src/main/java/ksbysample/eipapp/dockerserver/flow/FtpFlowConfig.javaFTP ダウンロード処理を追加します。

@Configuration
public class FtpFlowConfig {

    ..........
    private static final String FTP_LOCAL_DOWNLOAD_DIR = FTP_LOCAL_ROOT_DIR + "/download";

    ..........

    /****************************************
     * FTPダウンロード処理のサンプル             *
     ****************************************/

    @Bean
    public FtpInboundFileSynchronizer ftpInboundFileSynchronizer() {
        FtpInboundFileSynchronizer synchronizer = new FtpInboundFileSynchronizer(ftpSessionFactory());
        synchronizer.setRemoteDirectory(FTP_REMOTE_DIR);
        synchronizer.setFilter(new AcceptAllFileListFilter<>());
        synchronizer.setPreserveTimestamp(true);
        synchronizer.setDeleteRemoteFiles(true);
        return synchronizer;
    }

    @Bean
    public FtpInboundFileSynchronizingMessageSource ftpDownloadFileMessageSource() {
        FtpInboundFileSynchronizingMessageSource messageSource
                = new FtpInboundFileSynchronizingMessageSource(ftpInboundFileSynchronizer());
        messageSource.setLocalDirectory(new File(FTP_LOCAL_DOWNLOAD_DIR));
        messageSource.setLocalFilter(new AcceptAllFileListFilter<>());
        messageSource.setMaxFetchSize(1);
        return messageSource;
    }

    @Bean
    public IntegrationFlow ftpDownloadFlow() {
        return IntegrationFlows.from(
                // 1秒毎に FTPサーバを監視し、ファイルがあれば download ディレクトリにダウンロードする
                ftpDownloadFileMessageSource(), c -> c.poller(Pollers.fixedDelay(1000)))
                .log(LoggingHandler.Level.ERROR)
                // ファイルを upload ディレクトリへ移動する
                .<File>handle((p, h) -> {
                    try {
                        Files.move(p.toPath(), Paths.get(FTP_LOCAL_UPLOAD_DIR, p.getName())
                                , StandardCopyOption.REPLACE_EXISTING);
                        return null;
                    } catch (IOException e) {
                        throw new RuntimeException(e);
                    }
                })
                .get();
    }

}

動作確認します。アプリケーションを起動後、D:\eipapp\ksbysample-eipapp-dockerserver\ftp\upload の下にファイルを1つ配置します。ファイルのアップロード・ダウンロードが繰り返されていることを示すログが出力されます。

f:id:ksby:20180923230000p:plain

アプリケーションを停止し、docker-compose downFTP サーバも停止します。

アプリケーションを起動した時に FTP のサンプルが実行されないよう src/main/java/ksbysample/eipapp/dockerserver/flow/FtpFlowConfig.java の @Configuration アノテーションコメントアウトします。

// このサンプルを実行したい場合には、@Configuration のコメントアウトを外すこと
//@Configuration
public class FtpFlowConfig {

SFTP でアップロード・ダウンロードするサンプルを作成する

SFTP のサンプルは FTP とほぼ同じ書き方になるので、アップロード・ダウンロードのサンプルを一気に作成します。

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

dependencies {
    implementation('org.springframework.boot:spring-boot-starter-integration')
    implementation("org.springframework.boot:spring-boot-starter-mail")
    implementation('org.springframework.integration:spring-integration-mail')
    implementation('org.springframework.integration:spring-integration-file')
    implementation('org.springframework.integration:spring-integration-ftp')
    implementation('org.springframework.integration:spring-integration-sftp')
    implementation("org.apache.commons:commons-lang3")
    testImplementation('org.springframework.boot:spring-boot-starter-test')
}
  • implementation('org.springframework.integration:spring-integration-sftp') を追加します。

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

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

package ksbysample.eipapp.dockerserver.flow;

import com.jcraft.jsch.ChannelSftp;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.expression.common.LiteralExpression;
import org.springframework.integration.dsl.IntegrationFlow;
import org.springframework.integration.dsl.IntegrationFlows;
import org.springframework.integration.dsl.Pollers;
import org.springframework.integration.file.FileReadingMessageSource;
import org.springframework.integration.file.filters.AcceptAllFileListFilter;
import org.springframework.integration.file.remote.handler.FileTransferringMessageHandler;
import org.springframework.integration.file.remote.session.CachingSessionFactory;
import org.springframework.integration.file.remote.session.SessionFactory;
import org.springframework.integration.handler.LoggingHandler;
import org.springframework.integration.sftp.inbound.SftpInboundFileSynchronizer;
import org.springframework.integration.sftp.inbound.SftpInboundFileSynchronizingMessageSource;
import org.springframework.integration.sftp.session.DefaultSftpSessionFactory;
import org.springframework.messaging.support.GenericMessage;

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;

@Configuration
public class SftpFlowConfig {

    private static final String SFTP_SERVER = "localhost";
    private static final int SFTP_PORT = 22;
    private static final String SFTP_USER = "user01";
    private static final String SFTP_PASSWORD = "pass01";

    private static final String SFTP_REMOTE_DIR = "/upload";
    private static final String SFTP_LOCAL_ROOT_DIR = "D:/eipapp/ksbysample-eipapp-dockerserver/sftp";
    private static final String SFTP_LOCAL_UPLOAD_DIR = SFTP_LOCAL_ROOT_DIR + "/upload";
    private static final String SFTP_LOCAL_UPLOADING_DIR = SFTP_LOCAL_ROOT_DIR + "/uploading";
    private static final String SFTP_LOCAL_DOWNLOAD_DIR = SFTP_LOCAL_ROOT_DIR + "/download";

    @Bean
    public SessionFactory<ChannelSftp.LsEntry> sftpSessionFactory() {
        DefaultSftpSessionFactory factory = new DefaultSftpSessionFactory();
        factory.setHost(SFTP_SERVER);
        factory.setPort(SFTP_PORT);
        factory.setUser(SFTP_USER);
        factory.setPassword(SFTP_PASSWORD);
        factory.setAllowUnknownKeys(true);
        return new CachingSessionFactory<>(factory);
    }

    /****************************************
     * SFTPアップロード処理のサンプル            *
     ****************************************/

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

    @Bean
    public FileTransferringMessageHandler<ChannelSftp.LsEntry> sftpFileTransferringMessageHandler() {
        FileTransferringMessageHandler<ChannelSftp.LsEntry> handler
                = new FileTransferringMessageHandler<>(sftpSessionFactory());
        handler.setRemoteDirectoryExpression(new LiteralExpression(SFTP_REMOTE_DIR));
        return handler;
    }

    @Bean
    public IntegrationFlow sftpUploadFlow() {
        return IntegrationFlows.from(
                // 200ミリ秒毎に upload ディレクトリを監視し、ファイルがあれば処理を進める
                sftpUploadFileMessageSource(), c -> c.poller(Pollers.fixedDelay(200)))
                // ファイルを uploading ディレクトリへ移動する
                .<File>handle((p, h) -> {
                    try {
                        Path movedFilePath = Files.move(p.toPath(), Paths.get(SFTP_LOCAL_UPLOADING_DIR, p.getName())
                                , StandardCopyOption.REPLACE_EXISTING);
                        return new GenericMessage<>(movedFilePath.toFile(), h);
                    } catch (IOException e) {
                        throw new RuntimeException(e);
                    }
                })
                // SFTPサーバにファイルをアップロードする
                .wireTap(f -> f.handle(sftpFileTransferringMessageHandler()))
                .log(LoggingHandler.Level.WARN)
                // アップロードしたファイルを削除する
                .<File>handle((p, h) -> {
                    p.delete();
                    return null;
                })
                .get();
    }

    /****************************************
     * SFTPダウンロード処理のサンプル            *
     ****************************************/

    @Bean
    public SftpInboundFileSynchronizer sftpInboundFileSynchronizer() {
        SftpInboundFileSynchronizer synchronizer = new SftpInboundFileSynchronizer(sftpSessionFactory());
        synchronizer.setRemoteDirectory(SFTP_REMOTE_DIR);
        synchronizer.setFilter(new AcceptAllFileListFilter<>());
        synchronizer.setPreserveTimestamp(true);
        synchronizer.setDeleteRemoteFiles(true);
        return synchronizer;
    }

    @Bean
    public SftpInboundFileSynchronizingMessageSource sftpDownloadFileMessageSource() {
        SftpInboundFileSynchronizingMessageSource messageSource
                = new SftpInboundFileSynchronizingMessageSource(sftpInboundFileSynchronizer());
        messageSource.setLocalDirectory(new File(SFTP_LOCAL_DOWNLOAD_DIR));
        messageSource.setLocalFilter(new AcceptAllFileListFilter<>());
        messageSource.setMaxFetchSize(1);
        return messageSource;
    }

    @Bean
    public IntegrationFlow sftpDownloadFlow() {
        return IntegrationFlows.from(
                // 1秒毎に SFTPサーバを監視し、ファイルがあれば download ディレクトリにダウンロードする
                sftpDownloadFileMessageSource(), c -> c.poller(Pollers.fixedDelay(1000)))
                .log(LoggingHandler.Level.ERROR)
                // ファイルを upload ディレクトリへ移動する
                .<File>handle((p, h) -> {
                    try {
                        Files.move(p.toPath(), Paths.get(SFTP_LOCAL_UPLOAD_DIR, p.getName())
                                , StandardCopyOption.REPLACE_EXISTING);
                        return null;
                    } catch (IOException e) {
                        throw new RuntimeException(e);
                    }
                })
                .get();
    }

}

動作確認します。最初に docker-compose up -d で SFTP サーバを起動します。

アプリケーションを起動後、D:\eipapp\ksbysample-eipapp-dockerserver\sftp\upload の下にファイルを1つ配置します。ファイルのアップロード・ダウンロードが繰り返されていることを示すログが出力されることが確認できます。

f:id:ksby:20180924212652p:plain f:id:ksby:20180924212822p:plain

アプリケーションを停止し、docker-compose down で SFTP サーバも停止します。

アプリケーションを起動した時に FTP のサンプルが実行されないよう src/main/java/ksbysample/eipapp/dockerserver/flow/SftpFlowConfig.java の @Configuration アノテーションコメントアウトします。

// このサンプルを実行したい場合には、@Configuration のコメントアウトを外すこと
//@Configuration
public class SftpFlowConfig {

履歴

2018/09/24
初版発行。

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

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

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

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

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

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

    f:id:ksby:20180922003745p:plain

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

    f:id:ksby:20180922003835p:plain

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

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

    f:id:ksby:20180922004218p:plain

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

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

    f:id:ksby:20180922004358p:plain

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

    f:id:ksby:20180922005138p:plain

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

    f:id:ksby:20180922010000p:plain