Spring Boot 1.5.x の Web アプリを 2.0.x へバージョンアップする ( その5 )( checkstyle を 7.8.1 → 8.12 に、PMD を 5.8.1 → 6.7.0 にバージョンアップする )
概要
記事一覧はこちらです。
- 今回の手順で確認できるのは以下の内容です。
- checkstyle を 7.8.1 → 8.12 にバージョンアップします。
- PMD を 5.8.1 → 6.7.0 にバージョンアップします。
参照したサイト・書籍
目次
- checkstyle を 7.8.1 → 8.12 にバージョンアップする
- PMD を 5.8.1 → 6.7.0 にバージョンアップする
- build.gradle を変更して build してみる
The constant name 'springProfiles' doesn't match '[A-Z][A-Z_0-9]*'
Do not use hard coded encryption keys
Comment is too large: Too many lines
Avoid throwing raw exception types.
The constant name 'logger' doesn't match '[A-Z][A-Z_0-9]*'
StringBuffer (or StringBuilder).append is called consecutively without reusing the target variable.
This class has too many methods, consider refactoring it.
Avoid unnecessary constructors - the compiler will generate these for you
It is a good practice to call super() in a constructor
A method/constructor should not explicitly throw java.lang.Exception
The constant name 'serialVersionUID' doesn't match '[A-Z][A-Z_0-9]*'
Useless parentheses.
Document empty constructor
There is log block not surrounded by if
Avoid short class names like ...
Avoid using Literals in Conditional Statements
Avoid instantiating new objects inside loops
Prefer StringBuilder (non-synchronized) or StringBuffer (synchronized) over += for concatenating strings
Assigning an Object to null is a code smell. Consider refactoring.
Avoid using redundant field initializer for 'errcode'
Avoid catching generic exceptions such as NullPointerException, RuntimeException, Exception in try-catch block
- 最後に
手順
checkstyle を 7.8.1 → 8.12 にバージョンアップする
build.gradle の以下の点を変更します。
checkstyle { configFile = file("${rootProject.projectDir}/config/checkstyle/google_checks.xml") toolVersion = "8.12" sourceSets = [project.sourceSets.main] }
- checkstyle タスクで
toolVersion = "7.8.1"
→toolVersion = "8.12"
に変更します。
変更後、Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。
clean タスク実行 → Rebuild Project 実行 → build タスクを実行すると、checkstyleMain タスクで失敗しました。
コマンドラインから gradlew --stacktrace --debug build > gradle-debug.log 2>&1
コマンドを実行してログをファイルに出力した後、gradle-debug.log を IntelliJ IDEA のメインメニューの「Tools」-「Tail File in Console...」で開きます。
Property 'maxLineLength' in module LeftCurly does not exist, please check the documentation
というエラーが出ていました。Spring Boot + npm + Geb で入力フォームを作ってテストする ( その28 )( Spring Boot を 1.5.4 → 1.5.7 へ、error-prone を 2.0.15 → 2.1.1 へバージョンアップする ) で書きましたが、LeftCurly module から maxLineLength プロパティがなくなったので削除します。
config/checkstyle/google_checks.xml を以下のように変更します。
<module name="NeedBraces"/> <module name="LeftCurly"/> <module name="RightCurly"> <property name="id" value="RightCurlySame"/> <property name="tokens" value="LITERAL_TRY, LITERAL_CATCH, LITERAL_FINALLY, LITERAL_IF, LITERAL_ELSE, LITERAL_DO"/> </module>
- LeftCurly module に記述していた
<property name="maxLineLength" value="100"/>
を削除して、<module name="LeftCurly">...</module>
→<module name="LeftCurly"/>
に変更します。
再度 clean タスク実行 → Rebuild Project 実行 → build タスクを実行すると、今度は BUILD SUCCESSFUL が表示されました。
Checkstyle plugin の設定も 8.12 に変更しておきます。
PMD を 5.8.1 → 6.7.0 にバージョンアップする
以下の記事をベースに進めます。
- Spring Boot + npm + Geb で入力フォームを作ってテストする ( その55 )( PMD を 5.8.1 → 6.4.0 へバージョンアップする )
- Spring Boot + npm + Geb で入力フォームを作ってテストする ( その56 )( PMD を 5.8.1 → 6.4.0 へバージョンアップする2 )
設定ファイルも以下のファイルをコピーします。
build.gradle を変更して build してみる
まずは build.gradle の以下の点を変更します。
pmd { toolVersion = "5.8.1" sourceSets = [project.sourceSets.main] ignoreFailures = true consoleOutput = true ruleSetFiles = rootProject.files("/config/pmd/pmd-project-rulesets.xml") ruleSets = [] }
toolVersion = "5.8.1"
→toolVersion = "6.7.0"
に変更します。
変更後、Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。
pmd-project-rulesets.xml をダウンロードして、config/pmd/pmd-project-rulesets.xml にコピーします。
clean タスク実行 → Rebuild Project 実行 → build タスクを実行しますが、警告が大量に出力されました。1つずつ解消していきます。
The constant name 'springProfiles' doesn't match '[A-Z][A-Z_0-9]*'
定数名を 英大文字+スネークケースにしていなかったので、警告が出ていました。警告に従って springProfiles
→ SPRING_PROFILES
に変更します。
Do not use hard coded encryption keys
SecretKeySpec クラスのコンストラクタの第1引数に渡すキー文字列をクラス内に定数として定義していたので、外部に定義するよう警告が出ていました。このサンプルではこのままにしますので、クラスに @SuppressWarnings({"PMD.HardCodedCryptoKey"})
を付けて警告が出ないようにします。
Comment is too large: Too many lines
コメントの行数が多いという警告なのですが、警告が出たところを見ると以下の内容でした。
この警告は不要なので削除します。config/pmd/pmd-project-rulesets.xml では一旦 exclude した後、<rule ref="category/java/documentation.xml/CommentSize">...</rule>
で定義し直していたのですが、<rule ref="category/java/documentation.xml/CommentSize">...</rule>
を削除して exclude するだけにします。
Avoid throwing raw exception types.
適切な Exception クラスを定義せずに throw new RuntimeException(e);
と RuntimeException を throw していたので警告が出ていました。このサンプルでは config/pmd/pmd-project-rulesets.xml を以下のように変更して、警告が出ないようにします。
<rule ref="category/java/design.xml"> <exclude name="AvoidThrowingRawExceptionTypes"/> <exclude name="CyclomaticComplexity"/> <exclude name="DataClass"/> <exclude name="LawOfDemeter"/> <exclude name="LoosePackageCoupling"/> <exclude name="NcssCount"/> <exclude name="UseObjectForClearerAPI"/> <exclude name="UseUtilityClass"/> </rule>
<exclude name="AvoidThrowingRawExceptionTypes"/>
を追加します。
The constant name 'logger' doesn't match '[A-Z][A-Z_0-9]*'
logger を private static final Logger logger = LoggerFactory.getLogger(ControllerAndEventNameLogger.class);
と定義していたので、定数なのに英大文字+スネークケースでないと警告が出ていました。でも、変数は logger のままにしたいので static
を削除すると、今度は The Logger variable declaration does not contain the static and final modifiers
という警告が出ます。
今回は static を削除して private static final Logger logger = ...
→ private final Logger logger = ...
に変更し、config/pmd/pmd-project-rulesets.xml を以下のように変更します。
<rule ref="category/java/errorprone.xml"> <exclude name="BeanMembersShouldSerialize"/> <exclude name="DataflowAnomalyAnalysis"/> <exclude name="LoggerIsNotStaticFinal"/> <exclude name="MissingStaticMethodInNonInstantiatableClass"/> </rule>
<exclude name="LoggerIsNotStaticFinal"/>
を追加します。
StringBuffer (or StringBuilder).append is called consecutively without reusing the target variable.
1行ずつ .append(...)
を呼び出していたので警告が出ていました。
.append(...)
を連続で呼び出すようにします。
This class has too many methods, consider refactoring it.
メソッド数が多いので警告が出ていました。この警告は不要なので、config/pmd/pmd-project-rulesets.xml を以下のように変更します。
<rule ref="category/java/design.xml"> <exclude name="AvoidThrowingRawExceptionTypes"/> <exclude name="CyclomaticComplexity"/> <exclude name="DataClass"/> <exclude name="LawOfDemeter"/> <exclude name="LoosePackageCoupling"/> <exclude name="NcssCount"/> <exclude name="TooManyMethods"/> <exclude name="UseObjectForClearerAPI"/> <exclude name="UseUtilityClass"/> </rule>
<exclude name="TooManyMethods"/>
を追加します。
Avoid unnecessary constructors - the compiler will generate these for you
以下のように空の public コンストラクタを定義していたら不要との警告でした。コンストラクタの定義を削除します。
It is a good practice to call super() in a constructor
継承クラスのコンストラクタで super();
を呼び出していないという警告でした。super();
の呼び出しを追加します。
A method/constructor should not explicitly throw java.lang.Exception
メソッドに throws Exception
を付けているとこの警告が出ていました。メソッドに付けている throws Exception
を削除し、もしメソッド内から呼び出しているメソッドに throws Exception
が付いていて削除できない場合には、メソッドに @SuppressWarnings("PMD.SignatureDeclareThrowsException")
を付けて警告が出ないようにします。
The constant name 'serialVersionUID' doesn't match '[A-Z][A-Z_0-9]*'
private static final long serialVersionUID = ...
と static final
が付いているので定数と判断されたが、英大文字/数字+スネークケースでないので警告が出ていました。serialVersionUID はこういう宣言だと思うので警告出さなくてもいいと思うのですが。。。 @SuppressWarnings("PMD.FieldNamingConventions")
を付けて警告が出ないようにします。
Useless parentheses.
不要なカッコが書かれているという警告でした。この警告は不要なので、config/pmd/pmd-project-rulesets.xml を以下のように変更します。
<rule ref="category/java/codestyle.xml"> <exclude name="AtLeastOneConstructor"/> <exclude name="ClassNamingConventions"/> <exclude name="CommentDefaultAccessModifier"/> <exclude name="DefaultPackage"/> <exclude name="LongVariable"/> <exclude name="LocalVariableCouldBeFinal"/> <exclude name="MethodArgumentCouldBeFinal"/> <exclude name="OnlyOneReturn"/> <exclude name="ShortVariable"/> <exclude name="UnnecessaryAnnotationValueElement"/> <exclude name="UselessParentheses"/> <exclude name="VariableNamingConventions"/> </rule>
<exclude name="UselessParentheses"/>
を追加します。
Document empty constructor
コンストラクタをオーバーロードしている時に、中身が空でコメントも書かれていないものがあると出る警告でした。// This constructor is intentionally empty. Nothing special is needed here.
というコメントを記述して警告が出ないようにします。
There is log block not surrounded by if
logger.info("★★★ リトライ回数 = " + context.getRetryCount());
のようにlogger で変数を出力する時に {}
を使わずに +
で結合しているために警告が出ていました。logger.info("★★★ リトライ回数 = {}", context.getRetryCount());
という書き方に変更します。
Avoid short class names like ...
クラス名が短い(デフォルトでは5文字以内)と出る警告でした。この警告は不要なので、config/pmd/pmd-project-rulesets.xml を以下のように変更します。
<rule ref="category/java/codestyle.xml"> <exclude name="AtLeastOneConstructor"/> <exclude name="ClassNamingConventions"/> <exclude name="CommentDefaultAccessModifier"/> <exclude name="DefaultPackage"/> <exclude name="LongVariable"/> <exclude name="LocalVariableCouldBeFinal"/> <exclude name="MethodArgumentCouldBeFinal"/> <exclude name="OnlyOneReturn"/> <exclude name="ShortClassName"/> <exclude name="ShortVariable"/> <exclude name="UnnecessaryAnnotationValueElement"/> <exclude name="UselessParentheses"/> <exclude name="VariableNamingConventions"/> </rule>
<exclude name="ShortClassName"/>
を追加します。
Avoid using Literals in Conditional Statements
メソッドの処理内に数値リテラルを直接記述していたので警告が出ていました。定数を定義して、数値リテラルと置き換えます。
Avoid instantiating new objects inside loops
ループ処理内で new でオブジェクトを生成していたので警告が出ていました。@SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
を付けるか、ループの外で生成したオブジェクトをループ内で使い回すように変更します。
Prefer StringBuilder (non-synchronized) or StringBuffer (synchronized) over += for concatenating strings
+=
を使用しているところを StringBuilder か StringBuffer に置き換えるように出た警告でした。StringBuilder を使用するよう変更します。
Assigning an Object to null is a code smell. Consider refactoring.
変数宣言時以外の場所で変数に null をセットしていると出る警告でした。直接 null をセットしないよう実装を変えるようにします。
Avoid using redundant field initializer for 'errcode'
int の変数を宣言した時に 0 をセットしていたのですが、default の初期値なので警告が出ていました。セットしないようにします。
Avoid catching generic exceptions such as NullPointerException, RuntimeException, Exception in try-catch block
catch (Exception e) { ... }
を記述していると出る警告でした。使用している外部ライブラリが Exception を throw するため変更も削除もできないので、`` を付けて警告が出ないようにします。
最後に
これで全ての警告に対応しました。clean タスク実行 → Rebuild Project 実行 → build タスクを実行すると BUILD SUCCESSFUL の文字が出力されます。
config/pmd/pmd-project-rulesets.xml は以下のようになりました。
<?xml version="1.0" encoding="UTF-8"?> <ruleset name="mybraces" xmlns="http://pmd.sourceforge.net/ruleset/2.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://pmd.sourceforge.net/ruleset/2.0.0 http://pmd.sourceforge.net/ruleset_2_0_0.xsd"> <description>project rulesets</description> <!-- rulesets の種類・説明は 以下の URL 参照 https://github.com/pmd/pmd/tree/master/pmd-java/src/main/resources/category/java https://github.com/pmd/pmd/tree/master/pmd-java/src/main/resources/rulesets/java https://pmd.github.io/pmd-6.7.0/pmd_rules_java.html ※"pmd-6.7.0" の部分は適用しているバージョンに変更すること。 --> <rule ref="category/java/bestpractices.xml"/> <rule ref="category/java/codestyle.xml"> <exclude name="AtLeastOneConstructor"/> <exclude name="ClassNamingConventions"/> <exclude name="CommentDefaultAccessModifier"/> <exclude name="DefaultPackage"/> <exclude name="LocalVariableCouldBeFinal"/> <exclude name="LongVariable"/> <exclude name="MethodArgumentCouldBeFinal"/> <exclude name="OnlyOneReturn"/> <exclude name="ShortClassName"/> <exclude name="ShortVariable"/> <exclude name="UnnecessaryAnnotationValueElement"/> <exclude name="UselessParentheses"/> <exclude name="VariableNamingConventions"/> </rule> <rule ref="category/java/design.xml"> <exclude name="AvoidThrowingRawExceptionTypes"/> <exclude name="CyclomaticComplexity"/> <exclude name="DataClass"/> <exclude name="LawOfDemeter"/> <exclude name="LoosePackageCoupling"/> <exclude name="NcssCount"/> <exclude name="TooManyMethods"/> <exclude name="UseObjectForClearerAPI"/> <exclude name="UseUtilityClass"/> </rule> <rule ref="category/java/documentation.xml"> <!-- CommentRequired はここでは exclude し、下で別途定義する --> <exclude name="CommentRequired"/> <exclude name="CommentSize"/> <exclude name="UncommentedEmptyMethodBody"/> </rule> <rule ref="category/java/documentation.xml/CommentRequired"> <properties> <property name="fieldCommentRequirement" value="Ignored"/> <property name="enumCommentRequirement" value="Ignored"/> </properties> </rule> <rule ref="category/java/errorprone.xml"> <exclude name="BeanMembersShouldSerialize"/> <exclude name="DataflowAnomalyAnalysis"/> <exclude name="LoggerIsNotStaticFinal"/> <exclude name="MissingStaticMethodInNonInstantiatableClass"/> </rule> <rule ref="category/java/multithreading.xml"> <exclude name="UseConcurrentHashMap"/> </rule> <rule ref="category/java/performance.xml"/> <rule ref="category/java/security.xml"/> </ruleset>
履歴
2018/09/21
初版発行。
Spring Boot + Spring Integration でいろいろ試してみる ( その28 )( Spring Cloud for AWS で S3 へファイルをアップロード・ダウンロードする )
概要
記事一覧 はこちらです。
今回作成したソースの GitHub レポジトリ はこちらです。
今回は Spring Cloud for Amazon Web Services のサンプルを作成します。仕様や、S3 Bucket とアップロード・ダウンロード用の IAM ユーザは前回と同じにして、実装だけ変えます。
参照したサイト・書籍
Spring Cloud for Amazon Web Services
https://cloud.spring.io/spring-cloud-aws/AWS SDK for Java - Amazon S3 バケットの作成、一覧表示、削除
https://docs.aws.amazon.com/ja_jp/sdk-for-java/v1/developer-guide/examples-s3-buckets.htmlAWS SDK for Java - AWS SDK for Java を使用したオブジェクトの削除
https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/dev/DeletingOneObjectUsingJava.html
目次
- ksbysample-eipapp-cloudaws プロジェクトを作成する
- upload ディレクトリ → S3 へアップロードする処理を実装する
- S3 → download ディレクトリへダウンロード → upload ディレクトリへ移動する処理を実装する
- 動作確認
- S3 Bucket とアップロード・ダウンロード用の IAM ユーザを削除する
- Spring Integration Extension for AWS と Spring Cloud for AWS を使用してみた感想
手順
ksbysample-eipapp-cloudaws プロジェクトを作成する
Spring Initializr でプロジェクトの雛形を作成します。
作成後、build.gradle を以下のように変更します。
buildscript { ext { springBootVersion = '2.0.4.RELEASE' } repositories { mavenCentral() } dependencies { classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") } } apply plugin: 'java' apply plugin: 'eclipse' apply plugin: 'org.springframework.boot' apply plugin: 'io.spring.dependency-management' group = 'ksbysample' version = '0.0.1-SNAPSHOT' sourceCompatibility = 1.8 repositories { mavenCentral() } dependencyManagement { imports { mavenBom org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES mavenBom 'org.springframework.cloud:spring-cloud-aws:2.0.0.RELEASE' } } dependencies { def lombokVersion = "1.18.2" implementation('org.springframework.boot:spring-boot-starter-integration') implementation('org.springframework.cloud:spring-cloud-aws-context') implementation('org.springframework.integration:spring-integration-file') testImplementation('org.springframework.boot:spring-boot-starter-test') // for lombok annotationProcessor("org.projectlombok:lombok:${lombokVersion}") compileOnly("org.projectlombok:lombok:${lombokVersion}") }
- dependencyManagement block を追加します。
- dependencies block に以下の2行を追加します。
implementation('org.springframework.cloud:spring-cloud-aws-context')
implementation('org.springframework.integration:spring-integration-file')
- lombok の
@Slf4j
アノテーションを使いたいので、dependencies block に以下の3行を追加します。def lombokVersion = "1.18.2"
annotationProcessor("org.projectlombok:lombok:${lombokVersion}")
compileOnly("org.projectlombok:lombok:${lombokVersion}")
メインクラス名を KsbysampleEipappCloudawsApplication → Application に変更した後、clean タスク実行 → Rebuild Project 実行 → build タスクを実行して "BUILD SUCCESSFUL" のメッセージが出力されることを確認します。
※最初 build.gradle の dependencies block には Spring Cloud for Amazon Web Services に記載されている implementation('org.springframework.cloud:spring-cloud-starter-aws')
を記載したのですが、テストを実行した時に java.lang.IllegalStateException: There is no EC2 meta data available, because the application is not running in the EC2 environment. Region detection is only possible if the application is running on a EC2 instance
というエラーが出たので implementation('org.springframework.cloud:spring-cloud-aws-context')
に変更しました。
upload ディレクトリ → S3 へアップロードする処理を実装する
src/main/java/ksbysample/eipapp/cloudaws の下に flow パッケージを作成した後、その下に FlowConfig.java を新規作成して、以下の内容を記述します。
@Slf4j @Configuration public class FlowConfig { private static final String EIPAPP_ROOT_DIR_PATH = "D:/eipapp/ksbysample-eipapp-cloudaws"; private static final String UPLOAD_DIR_PATH = EIPAPP_ROOT_DIR_PATH + "/upload"; private static final String UPLOADING_DIR_PATH = EIPAPP_ROOT_DIR_PATH + "/uploading"; private static final String S3_BUCKET = "s3bucket-integration-test-ksby"; // リージョンは環境変数 AWS_REGION に(東京リージョンなら ap-northeast-1)、 // AccessKeyId, SecretAccessKey はそれぞれ環境変数 AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY にセットする @Bean public TransferManager transferManager() { return TransferManagerBuilder.standard().build(); } /******************************************************** * upload ディレクトリ --> S3 ファイルアップロード処理 * ********************************************************/ @Bean public FileReadingMessageSource uploadFileMessageSource() { FileReadingMessageSource source = new FileReadingMessageSource(); source.setDirectory(new File(UPLOAD_DIR_PATH)); source.setFilter(new AcceptAllFileListFilter<>()); return source; } @Bean public IntegrationFlow uploadToS3Flow() { return IntegrationFlows.from( // 200ミリ秒毎に upload ディレクトリを監視し、ファイルがあれば処理を進める uploadFileMessageSource(), c -> c.poller(Pollers.fixedDelay(200))) // ファイルを uploading ディレクトリへ移動する .<File>handle((p, h) -> { try { Path movedFilePath = Files.move(p.toPath(), Paths.get(UPLOADING_DIR_PATH, p.getName()) , StandardCopyOption.REPLACE_EXISTING); return new GenericMessage<>(movedFilePath.toFile(), h); } catch (IOException e) { throw new RuntimeException(e); } }) // ここから下はマルチスレッドで並列処理する .channel(c -> c.executor(Executors.newFixedThreadPool(2))) // 処理開始のログを出力し、S3 へアップロードする .<File>handle((p, h) -> { log.warn(String.format("☆☆☆ %s を S3 にアップロードします", p.getName())); try { // .waitForUploadResult() も呼び出してアップロード完了を待たないとファイルはアップロードされない transferManager() .upload(S3_BUCKET, p.getName(), p) .waitForUploadResult(); } catch (InterruptedException e) { throw new RuntimeException(e); } return new GenericMessage<>(p, h); }) // アップロードしたファイルを削除し、処理終了のログを出力する .<File>handle((p, h) -> { p.delete(); log.warn(String.format("★★★ %s を S3 にアップロードしました", p.getName())); return null; }) .get(); } }
upload ディレクトリに5ファイルだけ配置して動作確認してみます。最初に IntelliJ IDEA のメインメニューから「Run」-「Edit Configurations...」を選択して「Run/Debug Configurations」ダイアログを表示後、AWS_REGION、AWS_ACCESS_KEY_ID、AWS_SECRET_ACCESS_KEY を設定します。
s3bucket-integration-test-ksby バケットが空の状態であることを確認します。
アプリケーションを起動し upload ディレクトリに5ファイル配置すると、ファイルがアップロードされます。
再び s3bucket-integration-test-ksby バケットを見ると5ファイルアップロードされていました。ダウンロードして元の画像と比較すると同じファイルで問題ありませんでした。
S3 → download ディレクトリへダウンロード → upload ディレクトリへ移動する処理を実装する
src/main/java/ksbysample/eipapp/cloudaws/flow/FlowConfig.java に以下の処理を追加します。
@Slf4j @Configuration public class FlowConfig { .......... private static final String DOWNLOAD_DIR_PATH = EIPAPP_ROOT_DIR_PATH + "/download"; .......... /******************************************************** * S3 --> download ディレクトリ ファイルダウンロード処理 * ********************************************************/ @Bean public MessageSource<File> downloadFileFromS3MessageSource() { return () -> { try { File downloadFile = null; String key = null; ObjectListing objectListing = amazonS3().listObjects(S3_BUCKET); if (objectListing.getObjectSummaries().size() > 0) { S3ObjectSummary summary = objectListing.getObjectSummaries().iterator().next(); key = summary.getKey(); downloadFile = Paths.get(DOWNLOAD_DIR_PATH, key).toFile(); transferManager().download(S3_BUCKET, key, downloadFile) .waitForCompletion(); } return downloadFile != null ? MessageBuilder.withPayload(downloadFile).setHeader("s3Path", key).build() : null; } catch (InterruptedException e) { throw new RuntimeException(e); } }; } @Bean public IntegrationFlow downloadFromS3Flow() { return IntegrationFlows.from( // 200ミリ秒毎に S3 Bucket を監視し、ファイルがあれば処理を進める downloadFileFromS3MessageSource(), c -> c.poller(Pollers .fixedDelay(200))) // download ディレクトリに保存されたファイルを upload ディレクトリに移動する // ちなみに download ディレクトリからファイルを移動か削除しないと s3InboundFileSynchronizingMessageSource() // から Message が延々と送信され続けるので、必ず移動か削除する必要がある .<File>handle((p, h) -> { try { Files.move(p.toPath(), Paths.get(UPLOAD_DIR_PATH, p.getName()) , StandardCopyOption.REPLACE_EXISTING); log.error(String.format("◎◎◎ %s をダウンロードしました", p.getName())); return new GenericMessage<>(p, h); } catch (IOException e) { throw new RuntimeException(e); } }) .<File>handle((p, h) -> { amazonS3().deleteObject(S3_BUCKET, (String) h.get("s3Path")); return null; }) .get(); } }
アップロードの処理をコメントアウトして、アップロードしたファイルをダウンロードしてみます。
アプリケーションを起動すると S3 Bucket にあるファイルが download ディレクトリにダウンロード → upload ディレクトリへ移動されて、
S3 Bucket の中は空になっていました。
ダウンロードされたファイルは元々アップロードしたファイルと全く同じで問題ありませんでした。
動作確認
アプリケーションを起動後、最初は upload ディレクトリにファイルを1つだけ配置してみます。
ファイルのアップロードとダウンロードを繰り返し、エラーも発生しませんでした。
今度は 30ファイル配置してみます。
ダウンロード・アップロードが実行されました。全然制御していないので、同じファイルばかりダウンロードされていますが。。。
S3 Bucket とアップロード・ダウンロード用の IAM ユーザを削除する
以下のコマンドを実行して、作成した S3 Bucket とアップロード・ダウンロード用の IAM ユーザを削除します。
Spring Integration Extension for AWS と Spring Cloud for AWS を使用してみた感想
- Spring Integration Extension for AWS を使用すると Spring Integration らしく実装できて、かつ便利になったという感じがします。
- Spring Cloud for AWS は AWS SDK for Java の知識がないとやりたいことが実装できなくて、S3 を使うだけの場合、あまりメリットを感じられませんでした。build.gradle に
implementation('org.springframework.cloud:spring-cloud-aws-context')
を記述すれば依存性解決をしてくれることくらいのような気がします。 implementation('org.springframework.cloud:spring-cloud-starter-aws')
を記述すると、裏で何かいろいろ設定されるのか、PC 上で起動できなくなる(起動するためにいろいろ調べないといけない)ので、やり過ぎの感じがします。EC2 インスタンス上で起動すれば便利に感じるのかもしれませんが、今回は PC 上で実行していたので分かりません。implementation('org.springframework.integration:spring-integration-aws:2.0.0.RELEASE')
はそんなことはなかったので、Spring Integration Extension for AWS の方で不要な自動設定を無効にしてくれているのでしょうか。。。- Spring Cloud for AWS のダウンロード処理を実装しようとしていて思ったのが、Spring Integration の MessageSource の作成方法の理解が全然足りないということでした。Spring Integration Extension for AWS だと Spring Integration に既にある AbstractInboundFileSynchronizer や AbstractInboundFileSynchronizingMessageSource を利用して実装されているのですが、それらのクラスは今回初めて知りました。Spring Integration、まだまだ奥が深いです。
履歴
2018/09/16
初版発行。
Spring Boot + Spring Integration でいろいろ試してみる ( その27 )( Spring Integration Extension for AWS で S3 へファイルをアップロード・ダウンロードする )
概要
記事一覧 はこちらです。
今回作成したソースの GitHub レポジトリ はこちらです。
Spring Integration のアプリケーションから AWS の S3 へファイルをアップロード・ダウンロードするのに使えそうな Spring のライブラリとして以下の2種類がありますが、
それぞれのサンプルを作成してみます。まずは Spring Integration Extension for Amazon Web Services (AWS) から。
ちなみにこの2つのライブラリの違いですが、Spring Integration Extension for Amazon Web Services (AWS) に Note the Spring Integration AWS Extension is based on the Spring Cloud AWS project.
と記載されていました。Spring Integration Extension for AWS は Spring Cloud for AWS をベースに Spring Integration の Adapter 等を作成したもののようです。
参照したサイト・書籍
spring-projects/spring-integration-aws
https://github.com/spring-projects/spring-integration-awsAWS CloudFormation - AWS Identity and Access Management のテンプレートスニペット
https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/quickref-iam.htmlAmazon Simple Storage Service - AWS SDK for Java を使用したオブジェクトのアップロード
https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/dev/UploadObjSingleOpJava.htmlAWS SDK for Java - AWS 認証情報の使用
https://docs.aws.amazon.com/ja_jp/sdk-for-java/v1/developer-guide/credentials.htmlAmazon Simple Storage Service - ユーザーポリシーの例
https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/dev/example-policies-s3.htmlAmazon Simple Storage Service - ポリシーでのアクセス許可の指定
https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/dev/using-with-s3-actions.html
目次
- 仕様を決める
- ksbysample-eipapp-integrationaws プロジェクトを作成する
- S3 Bucket とアップロード・ダウンロード用の IAM ユーザを作成する
- upload ディレクトリ → S3 へアップロードする処理を実装する
- S3 → download ディレクトリへダウンロード → upload ディレクトリへ移動する処理を実装する
- 動作確認
手順
仕様を決める
- S3 Bucket を1つ作成します。
- 作成した S3 Bucket へアップロード・ダウンロードするための IAM ユーザを1つ作成します。
- アプリケーションは AWS 側ではなく PC 上で実行します。
- 作成したアプリケーションを実行するディレクトリ構成を以下のようにします。
D:\eipapp\ksbysample-eipapp-integrationaws ├ download ├ upload ├ uploading └ ksbysample-eipapp-integrationaws-0.0.1-SNAPSHOT.jar
- 最初に upload ディレクトリの下に画像ファイルを 30ファイル配置します。1ファイル 3~4MB 程度のファイルです。
- 一定時間毎に upload ディレクトリを監視し、ファイルがあれば S3 Buket へアップロードします。アップロードするファイルは upload ディレクトリから uploading ディレクトリへ移動してから S3 へアップロードします(ディレクトリ監視に引っかからないようにするため)。
- S3 へのアップロードはマルチスレッドで並列処理します。
- 一定時間毎に S3 Bucket を監視し、ファイルがあれば PC にダウンロードします。こちらはシングルスレッドで処理します。
- upload ディレクトリ →(アップロードFlow)→ S3 Bucket
→(ダウンロードFlow)→ download ディレクトリ → upload ディレクトリ
→(続く)
という流れになります。
ksbysample-eipapp-integrationaws プロジェクトを作成する
Spring Initializr でプロジェクトの雛形を作成します。
作成後、build.gradle を以下のように変更します。
buildscript { ext { springBootVersion = '2.0.4.RELEASE' } repositories { mavenCentral() } dependencies { classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") } } apply plugin: 'java' apply plugin: 'eclipse' apply plugin: 'org.springframework.boot' apply plugin: 'io.spring.dependency-management' group = 'ksbysample' version = '0.0.1-SNAPSHOT' sourceCompatibility = 1.8 repositories { mavenCentral() } dependencyManagement { imports { mavenBom org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES } } dependencies { def lombokVersion = "1.18.2" implementation('org.springframework.boot:spring-boot-starter-integration') implementation('org.springframework.integration:spring-integration-aws:2.0.0.RELEASE') implementation('org.springframework.integration:spring-integration-file') testImplementation('org.springframework.boot:spring-boot-starter-test') // for lombok annotationProcessor("org.projectlombok:lombok:${lombokVersion}") compileOnly("org.projectlombok:lombok:${lombokVersion}") }
- dependencyManagement block を追加します。
- dependencies block に以下の2行を追加します。
implementation('org.springframework.integration:spring-integration-aws:2.0.0.RELEASE')
implementation('org.springframework.integration:spring-integration-file')
- lombok の
@Slf4j
アノテーションを使いたいので、dependencies block に以下の3行を追加します。def lombokVersion = "1.18.2"
annotationProcessor("org.projectlombok:lombok:${lombokVersion}")
compileOnly("org.projectlombok:lombok:${lombokVersion}")
メインクラス名を KsbysampleEipappIntegrationawsApplication → Application に変更した後、clean タスク実行 → Rebuild Project 実行 → build タスクを実行して "BUILD SUCCESSFUL" のメッセージが出力されることを確認します。
S3 Bucket とアップロード・ダウンロード用の IAM ユーザを作成する
※AWS アカウントがすでに作成されていること、AWS CLI をインストール済であること、AWS CloudFormation を実行可能な環境を作成済であること、が前提で以下の手順を記載しています。
プロジェクトのルート直下に create-s3bucket.yaml を新規作成し、以下の内容を記述します。
AWSTemplateFormatVersion: 2010-09-09 Description: Spring Integration Test Stack Resources: ######################### # S3 Bucket ######################### IntegrationTestS3Bucket: Type: AWS::S3::Bucket Properties: BucketName: "s3bucket-integration-test-ksby" ######################### # IAM User & IAM Policy ######################### IntegrationTestUser: Type: AWS::IAM::User Properties: UserName: "iam-user-integration-test" IntegrationTestUserAccessKey: Type: AWS::IAM::AccessKey Properties: UserName: !Ref IntegrationTestUser IntegrationTestPolicy: Type: AWS::IAM::Policy Properties: PolicyName: "iam-policy-integration-test" PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - s3:ListBucket - s3:GetBucketLocation Resource: !Join [ "", [ "arn:aws:s3:::", !Ref IntegrationTestS3Bucket ] ] - Effect: Allow Action: - s3:PutObject - s3:PutObjectAcl - s3:GetObject - s3:GetObjectAcl - s3:DeleteObject Resource: !Join [ "", [ "arn:aws:s3:::", !Ref IntegrationTestS3Bucket, "/*" ] ] Users: - !Ref IntegrationTestUser Outputs: AccessKeyId: Value: !Ref IntegrationTestUserAccessKey SecretAccessKey: Value: !GetAtt IntegrationTestUserAccessKey.SecretAccessKey
git-cmd.exe を起動した後、以下のコマンドを実行してテストで使用する S3 Bucket とアップロード・ダウンロード用の IAM ユーザを作成します。
upload ディレクトリ → S3 へアップロードする処理を実装する
src/main/java/ksbysample/eipapp/integrationaws の下に flow パッケージを作成した後、その下に FlowConfig.java を新規作成して、以下の内容を記述します。
@Slf4j @Configuration public class FlowConfig { private static final String EIPAPP_ROOT_DIR_PATH = "D:/eipapp/ksbysample-eipapp-integrationaws"; private static final String UPLOAD_DIR_PATH = EIPAPP_ROOT_DIR_PATH + "/upload"; private static final String UPLOADING_DIR_PATH = EIPAPP_ROOT_DIR_PATH + "/uploading"; private static final String S3_BUCKET = "s3bucket-integration-test-ksby"; // リージョンは環境変数 AWS_REGION に(東京リージョンなら ap-northeast-1)、 // AccessKeyId, SecretAccessKey はそれぞれ環境変数 AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY にセットする @Bean public AmazonS3 amazonS3() { return AmazonS3ClientBuilder.standard().build(); } /******************************************************** * upload ディレクトリ --> S3 ファイルアップロード処理 * ********************************************************/ @Bean public FileReadingMessageSource uploadFileMessageSource() { FileReadingMessageSource source = new FileReadingMessageSource(); source.setDirectory(new File(UPLOAD_DIR_PATH)); source.setFilter(new AcceptAllFileListFilter<>()); return source; } @Bean public MessageHandler uploadToS3MessageHandler() { return new S3MessageHandler(amazonS3(), S3_BUCKET); } @Bean public IntegrationFlow uploadToS3Flow() { return IntegrationFlows.from( // 200ミリ秒毎に upload ディレクトリを監視し、ファイルがあれば処理を進める uploadFileMessageSource(), c -> c.poller(Pollers.fixedDelay(200))) // ファイルを uploading ディレクトリへ移動する .<File>handle((p, h) -> { try { Path movedFilePath = Files.move(p.toPath(), Paths.get(UPLOADING_DIR_PATH, p.getName()) , StandardCopyOption.REPLACE_EXISTING); return new GenericMessage<>(movedFilePath.toFile(), h); } catch (IOException e) { throw new RuntimeException(e); } }) // ここから下はマルチスレッドで並列処理する .channel(c -> c.executor(Executors.newFixedThreadPool(5))) // 処理開始のログを出力する // 上の .channel(...) の直後に .log(...) を書くと並列処理されないため、.handle(...) を書いてその中でログに出力する .<File>handle((p, h) -> { log.warn(String.format("☆☆☆ %s を S3 にアップロードします", p.getName())); return new GenericMessage<>(p, h); }) // S3 へアップロードする // S3MessageHandler は Outbound Channel Adapter で .handle(...) メソッドに渡しただけでは次の処理に行かないので、 // .wireTap(...) で呼び出す .wireTap(sf -> sf .handle(uploadToS3MessageHandler())) // アップロードしたファイルを削除し、処理終了のログを出力する .<File>handle((p, h) -> { p.delete(); log.warn(String.format("★★★ %s を S3 にアップロードしました", p.getName())); return null; }) .get(); } }
upload ディレクトリに5ファイルだけ配置して動作確認してみます。最初に IntelliJ IDEA のメインメニューから「Run」-「Edit Configurations...」を選択して「Run/Debug Configurations」ダイアログを表示後、AWS_REGION、AWS_ACCESS_KEY_ID、AWS_SECRET_ACCESS_KEY を設定します。
s3bucket-integration-test-ksby バケットが空の状態であることを確認します。
アプリケーションを起動し upload ディレクトリに5ファイル配置すると、ファイルがアップロードされます。
再び s3bucket-integration-test-ksby バケットを見ると5ファイルアップロードされていました。ダウンロードして元の画像と比較すると同じファイルで問題ありませんでした。
S3 → download ディレクトリへダウンロード → upload ディレクトリへ移動する処理を実装する
src/main/java/ksbysample/eipapp/integrationaws/flow/FlowConfig.java に以下の処理を追加します。
@Slf4j @Configuration public class FlowConfig { .......... private static final String DOWNLOAD_DIR_PATH = EIPAPP_ROOT_DIR_PATH + "/download"; .......... /******************************************************** * S3 --> download ディレクトリ ファイルダウンロード処理 * ********************************************************/ @Bean public S3InboundFileSynchronizer s3InboundFileSynchronizer() { S3InboundFileSynchronizer synchronizer = new S3InboundFileSynchronizer(amazonS3()); synchronizer.setDeleteRemoteFiles(true); synchronizer.setPreserveTimestamp(true); synchronizer.setRemoteDirectory(S3_BUCKET); return synchronizer; } @Bean public S3InboundFileSynchronizingMessageSource s3InboundFileSynchronizingMessageSource() { S3InboundFileSynchronizingMessageSource messageSource = new S3InboundFileSynchronizingMessageSource(s3InboundFileSynchronizer()); messageSource.setLocalDirectory(new File(DOWNLOAD_DIR_PATH)); messageSource.setLocalFilter(new AcceptAllFileListFilter<>()); return messageSource; } @Bean public IntegrationFlow downloadFromS3Flow() { return IntegrationFlows.from( // 1秒毎に S3 Bucket を監視し、ファイルがあれば処理を進める s3InboundFileSynchronizingMessageSource(), c -> c.poller(Pollers .fixedDelay(1000) // 1度に最大100ファイルダウンロードする // .maxMessagesPerPoll(...) を書かないと 1ファイルずつダウンロードされる .maxMessagesPerPoll(100))) // download ディレクトリに保存されたファイルを upload ディレクトリに移動する // ちなみに download ディレクトリからファイルを移動か削除しないと s3InboundFileSynchronizingMessageSource() // から Message が延々と送信され続けるので、必ず移動か削除する必要がある .<File>handle((p, h) -> { try { Files.move(p.toPath(), Paths.get(UPLOAD_DIR_PATH, p.getName()) , StandardCopyOption.REPLACE_EXISTING); log.error(String.format("◎◎◎ %s をダウンロードしました", p.getName())); return null; } catch (IOException e) { throw new RuntimeException(e); } }) .get(); } }
アップロードの処理をコメントアウトして、先程アップロードしたファイルをダウンロードしてみます。
アプリケーションを起動すると S3 Bucket にあるファイルが download ディレクトリにダウンロード → upload ディレクトリへ移動されて、
S3 Bucket の中は空になっていました。
ダウンロードされたファイルは元々アップロードしたファイルと全く同じで問題ありませんでした。
動作確認
アプリケーションを起動後、最初は upload ディレクトリにファイルを1つだけ配置してみます。
ファイルのアップロードとダウンロードを繰り返し、エラーも発生しませんでした。
今度は 30ファイル配置してみます。
なんか想定と違う動きをしています。。。
- アップロード処理がマルチスレッドなので当然処理は多くて当たり前なのですが、ダウンロード処理がほとんど動きません。
org.springframework.messaging.MessagingException: Problem occurred while synchronizing remote to local directory; nested exception is org.springframework.messaging.MessagingException: Failure occurred while copying 's3bucket-integration-test-ksby/P7250021.JPG' from the remote to the local directory; nested exception is java.net.SocketTimeoutException: Read timed out
というエラーも出ています。- エラーの後に一気にダウンロードが実行されています。
1CPU2Core の PC で動かしているのですが、おそらく 5スレッドだと重いのと、ダウロード処理がどうも S3 Bucket 内に見つけたファイルを全て PC にダウンロードしてから処理を進めているようなので(ダウンロードのログが出ていないのに download ディレクトリにファイルが出来ていました)、以下の点を変更します。
@Bean public IntegrationFlow uploadToS3Flow() { .......... // ここから下はマルチスレッドで並列処理する .channel(c -> c.executor(Executors.newFixedThreadPool(2))) .......... } .......... @Bean public S3InboundFileSynchronizingMessageSource s3InboundFileSynchronizingMessageSource() { S3InboundFileSynchronizingMessageSource messageSource = new S3InboundFileSynchronizingMessageSource(s3InboundFileSynchronizer()); messageSource.setLocalDirectory(new File(DOWNLOAD_DIR_PATH)); messageSource.setLocalFilter(new AcceptAllFileListFilter<>()); messageSource.setMaxFetchSize(1); return messageSource; } @Bean public IntegrationFlow downloadFromS3Flow() { return IntegrationFlows.from( // 1秒毎に S3 Bucket を監視し、ファイルがあれば処理を進める s3InboundFileSynchronizingMessageSource(), c -> c.poller(Pollers .fixedDelay(200))) .......... } }
- uploadToS3Flow 内のマルチスレッド数を 5 → 2 に変更します。
- s3InboundFileSynchronizingMessageSource 内に
messageSource.setMaxFetchSize(1);
を追加し、S3 Bucket に見つけたファイルを全てダウンロードするのではなく 1ファイルだけダウンロードするようにします。 - downloadFromS3Flow のポーリング間隔を uploadToS3Flow と同じ 200ミリ秒にし、
.maxMessagesPerPoll(100)
を削除します。
変更後に再度実行すると、最初はアップロードばかりが動きますが、途中からアップロードとダウンロードがバランスよく動くようになりました。
jar ファイルを作成してコマンドラインから実行してみます。まずは jar ファイルを生成して D:\eipapp\ksbysample-eipapp-integrationaws の下にコピーします。
コピー後、環境変数 AWS_REGION、AWS_ACCESS_KEY_ID、AWS_SECRET_ACCESS_KEY を設定してからアプリケーションを実行します。
ksbysample-eipapp-integrationaws.log を IntelliJ IDEA のメインメニューの「Tools」-「Tail File in Console...」で開いておきます。
upload ディレクトリに 30ファイル配置すると、ダウンロード・アップロードが実行されました。
Ctrl+C でアプリケーションを終了させた後、S3 にアップロードされたファイルを削除しておきます。
履歴
2018/09/11
初版発行。
IntelliJ IDEA を 2018.2.2 → 2018.2.3 へバージョンアップ
IntelliJ IDEA を 2018.2.2 → 2018.2.3 へバージョンアップする
IntelliJ IDEA の 2018.2.3 がリリースされているのでバージョンアップします。
- IntelliJ IDEA 2018.2.3 is here!
https://blog.jetbrains.com/idea/2018/09/intellij-idea-2018-2-3-is-here/
※ksbysample-webapp-lending プロジェクトを開いた状態でバージョンアップしています。
IntelliJ IDEA のメインメニューから「Help」-「Check for Updates...」を選択します。
「IDE and Plugin Updates」ダイアログが表示されます。左下に「Update and Restart」ボタンが表示されていますので、「Update and Restart」ボタンをクリックします。
Plugin の update も表示されました。このまま「Update and Restart」ボタンをクリックします。
Patch がダウンロードされて IntelliJ IDEA が再起動します。
IntelliJ IDEA が起動すると画面下部に「Indexing…」のメッセージが表示されますので、終了するまで待機します。
IntelliJ IDEA のメインメニューから「Help」-「About」を選択し、2018.2.3 へバージョンアップされていることを確認します。
Gradle Tool Window のツリーを見ると「Tasks」の下に「other」しかない状態になっているので、左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。
clean タスク実行 → Rebuild Project 実行 → build タスクを実行して、"BUILD SUCCESSFUL" のメッセージが出力されることを確認します。
Project Tool Window で src/test を選択した後、コンテキストメニューを表示して「Run with Coverage」-「All Tests」を選択し、テストが全て成功することを確認します。
Spring Boot 1.5.x の Web アプリを 2.0.x へバージョンアップする ( その4 )( AbstractJsonpResponseBodyAdvice を削除し、失敗しているテストを成功させる )
概要
記事一覧はこちらです。
Spring Boot 1.5.x の Web アプリを 2.0.x へバージョンアップする ( その3 )( build.gradle を変更する ) の続きです。
- 今回の手順で確認できるのは以下の内容です。
- AbstractJsonpResponseBodyAdvice が非推奨になったので、使用しないようにします。
- 失敗しているテストを解消します。
参照したサイト・書籍
Spring Security 5 : There is no PasswordEncoder mapped for the id “null”
https://stackoverflow.com/questions/49654143/spring-security-5-there-is-no-passwordencoder-mapped-for-the-id-nullSpring Security 5.0.0.RC1 Released
https://spring.io/blog/2017/11/01/spring-security-5-0-0-rc1-released#password-encoding
目次
- AbstractJsonpResponseBodyAdvice を削除する
- 失敗しているテストを成功させる
- ksbysample.webapp.lending.listener.rabbitmq.InquiringStatusOfBookQueueListenerTest > testReceiveMessage FAILED
- ksbysample.webapp.lending.web.lendingapproval.LendingapprovalControllerTest > ksbysample.webapp.lending.web.lendingapproval.LendingapprovalControllerTest$貸出承認画面の正常処理時のテスト.確定ボタンをクリックした場合_却下と却下理由 FAILED
- ksbysample.webapp.lending.web.LoginControllerTest > ksbysample.webapp.lending.web.LoginControllerTest$ログインエラーのテスト.存在するユーザ名でもパスワードが正しくなければログインはエラーになる FAILED
- ksbysample.webapp.lending.web.LoginControllerTest > ksbysample.webapp.lending.web.LoginControllerTest$ログインエラーのテスト.ログインを5回失敗すればアカウントはロックされる FAILED
- 次回は。。。
手順
AbstractJsonpResponseBodyAdvice を削除する
Javascript で JSONP で WebAPI を呼び出しているところはなかったので、src/main/java/ksbysample/webapp/lending/config/JsonpAdvice.java を削除します。
失敗しているテストを成功させる
失敗していた4件のテストを1つずつ見ていきます。
ksbysample.webapp.lending.listener.rabbitmq.InquiringStatusOfBookQueueListenerTest > testReceiveMessage FAILED
テストが失敗した原因は、org.apache.commons.lang3.builder.ToStringBuilder#reflectionToString で java.lang.NullPointerException が発生していたためでした。
テストを Debug 実行して調べると、this.correlationId
が null のため this.correlationId.hashCode()
で NullPointerException が発生していました。
もう少し調べた結果、以下の原因であることが分かりました。
- テストのために、本来は RabbitMQ から受信するはずの Message オブジェクトをテストメソッド内で生成していました。下の画像の赤枠の部分です。
- この時
messageConverter.toMessage(...)
の第2引数に MessageProperties オブジェクトが必要なのでnew MessageProperties()
と書いていたのですが、これだと correlationId に値をセットしていません。 messageConverter.toMessage(...)
の後にlistener.receiveMessage(message);
を呼び出しているのですが、listener.receiveMessage(...);
が呼び出されると MethodLogger#logginBeginMethod によりメソッドの情報をログに出力しようとします。この時 MessageProperties#hashCode が呼び出されるのですが、this.correlationId
が null なので NullPointerException が発生していました。
NullPointerException が発生しないようにするには correlationId をセットすればよさそうです。MessageProperties を生成するための MessagePropertiesBuilder というクラスを見つけたので、new MessageProperties()
→ MessagePropertiesBuilder.newInstance().setCorrelationId("test").build()
に変更します。
変更後にテストを実行すると成功しました。
ksbysample.webapp.lending.web.lendingapproval.LendingapprovalControllerTest > ksbysample.webapp.lending.web.lendingapproval.LendingapprovalControllerTest$貸出承認画面の正常処理時のテスト.確定ボタンをクリックした場合_却下と却下理由 FAILED
テストが失敗した原因は、メールの中身が "承認" ではなく "却下" になっていたためでした。
テストメソッドを見たら public void 確定ボタンをクリックした場合_却下と却下理由() throws Exception { ... }
と書いていて、どうも却下のテストをしているので "却下" になっているのが正しいのでは?と思って調べたら、単に前回の修正ミスでした。メソッド内の messageTxt001Resource.getFile()
→ messageTxt002Resource.getFile()
に修正します。
修正後にテストを実行すると成功しました。
ksbysample.webapp.lending.web.LoginControllerTest > ksbysample.webapp.lending.web.LoginControllerTest$ログインエラーのテスト.存在するユーザ名でもパスワードが正しくなければログインはエラーになる FAILED
java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"
というエラーメッセージが出ていました。
stackoverflow を検索すると以下のページが見つかりました。
- Spring Security 5 : There is no PasswordEncoder mapped for the id “null”
- Spring Security 5.0.0.RC1 Released
Spring Security 5.0.0.RC1 Released の「Password Storage Format」にパスワードの文字列のフォーマットが {id}encodedPassword
に変更されたと書かれています。テストで使用している DB に保存されているパスワードには {id}
の部分を付けていないので java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"
のエラーが出たようです。DB に保存するパスワードは bcrypt で暗号化しているので、先頭に {bcrypt}
の文字列を追加するようにします。
以下のファイルを変更します。
変更後にテストを実行すると成功しました。
ksbysample.webapp.lending.web.LoginControllerTest > ksbysample.webapp.lending.web.LoginControllerTest$ログインエラーのテスト.ログインを5回失敗すればアカウントはロックされる FAILED
このエラーも出ていたのですが、上の "{bcrypt}" を入れる対応をしたら出なくなりました。
次回は。。。
clean タスク実行 → Rebuild Project 実行 → build タスクを実行すると "BUILD SUCCESSFUL" のメッセージが出力されました。
次回は checkstyle、PMD をバージョンアップします。
履歴
2018/09/08
初版発行。
Spring Boot 1.5.x の Web アプリを 2.0.x へバージョンアップする ( その3 )( build.gradle を変更する )
概要
記事一覧はこちらです。
Spring Boot 1.5.x の Web アプリを 2.0.x へバージョンアップする ( その2 )( Gradle を 3.5 → 4.10 にバージョンアップする ) の続きです。
- 今回の手順で確認できるのは以下の内容です。
- build.gradle を変更します。
- Spring Initializr で 2.0.4 のプロジェクトを作成して、変更した方がよい点があれば反映します。
- Spring Boot のバージョンを 1.5.4 → 2.0.4 に変更します。
- BOM を Spring IO Platform のものから Spring Boot のものに変更します。
- ライブラリは最新バージョンにアップデートします。
- build.gradle を変更します。
参照したサイト・書籍
Spring Boot 2.0 Release Notes
https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-2.0-Release-NotesSpring Boot 2.0 Migration Guide
https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-2.0-Migration-GuideSpring Boot Gradle Plugin Reference Guide
https://docs.spring.io/spring-boot/docs/current/gradle-plugin/reference/html/spring-gradle-plugins/dependency-management-plugin
https://github.com/spring-gradle-plugins/dependency-management-plugin
目次
- Spring Initializr で 2.0.4 のプロジェクトを作成する
- build.gradle を変更する
- Rebuild Project でエラーの出るクラスを修正する
- src/main/java/ksbysample/webapp/lending/config/ApplicationConfig.java
- src/main/java/ksbysample/webapp/lending/config/WebMvcConfig.java
- src/main/java/ksbysample/webapp/lending/config/WebSecurityConfig.java
- src/main/java/ksbysample/webapp/lending/security/LendingUserDetailsService.java
- src/main/java/ksbysample/webapp/lending/web/WebappErrorController.java
- org.hibernate.validator.constraintsのorg.hibernate.validator.constraints.NotBlankは非推奨になりました
- org.springframework.web.servlet.mvc.method.annotationのorg.springframework.web.servlet.mvc.method.annotation.AbstractJsonpResponseBodyAdviceは非推奨になりました
- build タスク実行時に出るエラーを修正する
- 次回は。。。
手順
Spring Initializr で 2.0.4 のプロジェクトを作成する
Spring Initializr で 2.0.4 のプロジェクトを作成して、生成された build.gradle を見て反映した方が良い点があるか確認します。
以下の build.gradle が作成されました。
buildscript { ext { springBootVersion = '2.0.4.RELEASE' } repositories { mavenCentral() } dependencies { classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") } } apply plugin: 'java' apply plugin: 'eclipse' apply plugin: 'org.springframework.boot' apply plugin: 'io.spring.dependency-management' group = 'com.example' version = '0.0.1-SNAPSHOT' sourceCompatibility = 1.8 repositories { mavenCentral() } dependencies { compile('org.springframework.boot:spring-boot-starter-data-redis') compile('org.springframework.boot:spring-boot-starter-freemarker') compile('org.springframework.boot:spring-boot-starter-mail') compile('org.springframework.boot:spring-boot-starter-security') compile('org.springframework.boot:spring-boot-starter-thymeleaf') compile('org.springframework.boot:spring-boot-starter-web') compile('org.springframework.session:spring-session-data-redis') runtime('org.springframework.boot:spring-boot-devtools') compileOnly('org.projectlombok:lombok') testCompile('org.springframework.boot:spring-boot-starter-test') testCompile('org.springframework.security:spring-security-test') }
反映した方がよいのは Spring Boot 2.0 Migration Guide に書いてある apply plugin: 'io.spring.dependency-management'
だけでした。これは Spring Boot 2.0 Migration Guide に従って変更するので、Spring Initializr で生成された build.gradle からは何も反映しません。
build.gradle を変更する
以下の方針で変更します。
- 以下のドキュメントを見て build.gradle を変更します。
- プラグインの書き方を
apply plugin: ...
から plugins block に変更します。 - checkstyle、findbugs、PMD は後でバージョンアップします。今は何も変更しません。
- レポジトリを
jcenter()
→mavenCentral()
に変更します。 - dependency-management-plugin で参照する BOM ファイルを Spring IO Platform のものから Spring Boot の BOM に変更します。
- dependencies block 内の記述で、非推奨となった compile, testCompile を implementation, testImplementation 等に変更します。
- ライブラリは最新バージョンにアップデートします。
buildscript { ext { group "ksbysample" version "2.0.4-RELEASE" } repositories { mavenCentral() maven { url "https://repo.spring.io/release/" } maven { url "https://plugins.gradle.org/m2/" } } } plugins { id "java" id "eclipse" id "idea" id "org.springframework.boot" version "2.0.4.RELEASE" id "io.spring.dependency-management" version "1.0.6.RELEASE" id "groovy" id "checkstyle" id "findbugs" id "pmd" id "net.ltgt.errorprone" version "0.0.16" id "de.undercouch.download" version "3.4.3" } sourceCompatibility = 1.8 targetCompatibility = 1.8 wrapper { gradleVersion = "4.10" distributionType = Wrapper.DistributionType.ALL } [compileJava, compileTestGroovy, compileTestJava]*.options*.encoding = "UTF-8" [compileJava, compileTestGroovy, compileTestJava]*.options*.compilerArgs = ["-Xlint:all,-options,-processing,-path"] compileJava.options.compilerArgs += [ "-Xep:RemoveUnusedImports:WARN" , "-Xep:InsecureCryptoUsage:OFF" ] // for Doma 2 // JavaクラスとSQLファイルの出力先ディレクトリを同じにする processResources.destinationDir = compileJava.destinationDir // コンパイルより前にSQLファイルを出力先ディレクトリにコピーするために依存関係を逆転する compileJava.dependsOn processResources idea { module { inheritOutputDirs = false outputDir = file("$buildDir/classes/main/") } } springBoot { buildInfo() } configurations { // for Doma 2 domaGenRuntime } checkstyle { configFile = file("${rootProject.projectDir}/config/checkstyle/google_checks.xml") toolVersion = "7.8.1" sourceSets = [project.sourceSets.main] } findbugs { toolVersion = "3.0.1" sourceSets = [project.sourceSets.main] ignoreFailures = true effort = "max" excludeFilter = file("${rootProject.projectDir}/config/findbugs/findbugs-exclude.xml") } tasks.withType(FindBugs) { reports { xml.enabled = false html.enabled = true } } pmd { toolVersion = "5.8.1" sourceSets = [project.sourceSets.main] ignoreFailures = true consoleOutput = true ruleSetFiles = rootProject.files("/config/pmd/pmd-project-rulesets.xml") ruleSets = [] } repositories { mavenCentral() } dependencyManagement { imports { // mavenBom は以下の URL のものを使用する // https://repo.spring.io/release/org/springframework/boot/spring-boot-starter-parent/2.0.4.RELEASE/ // bomProperty に指定可能な property は以下の URL の BOM に記述がある // https://repo.spring.io/release/org/springframework/boot/spring-boot-dependencies/2.0.4.RELEASE/spring-boot-dependencies-2.0.4.RELEASE.pom mavenBom(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES) { // Spring Boot の BOM に定義されているバージョンから変更する場合には、ここに以下のように記述する // bomProperty "thymeleaf.version", "3.0.9.RELEASE" } } } dependencies { def jdbcDriver = "org.postgresql:postgresql:42.2.4" def spockVersion = "1.1-groovy-2.4" def domaVersion = "2.19.3" def lombokVersion = "1.18.2" def errorproneVersion = "2.3.1" def powermockVersion = "2.0.0-beta.5" // dependency-management-plugin によりバージョン番号が自動で設定されるもの // Appendix A. Dependency versions ( http://docs.spring.io/platform/docs/current/reference/htmlsingle/#appendix-dependency-versions ) 参照 implementation("org.springframework.boot:spring-boot-starter-web") implementation("org.springframework.boot:spring-boot-starter-thymeleaf") { exclude group: "org.codehaus.groovy", module: "groovy" } implementation("org.thymeleaf.extras:thymeleaf-extras-springsecurity4") implementation("org.thymeleaf.extras:thymeleaf-extras-java8time") implementation("org.springframework.boot:spring-boot-starter-data-jpa") implementation("org.springframework.boot:spring-boot-starter-freemarker") implementation("org.springframework.boot:spring-boot-starter-mail") implementation("org.springframework.boot:spring-boot-starter-security") implementation("org.springframework.boot:spring-boot-starter-data-redis") implementation("org.springframework.boot:spring-boot-starter-amqp") implementation("org.springframework.boot:spring-boot-devtools") implementation("org.springframework.session:spring-session-core") implementation("org.springframework.session:spring-session-data-redis") implementation("org.springframework.retry:spring-retry") implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310") implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-xml") implementation("org.apache.commons:commons-lang3") implementation("org.codehaus.janino:janino") implementation("org.apache.tomcat:tomcat-jdbc") testImplementation("org.springframework.boot:spring-boot-starter-test") testImplementation("org.springframework.security:spring-security-test") testImplementation("org.yaml:snakeyaml") testImplementation("org.mockito:mockito-core") runtimeOnly("org.springframework.boot:spring-boot-properties-migrator") // dependency-management-plugin によりバージョン番号が自動で設定されないもの、あるいは最新バージョンを指定したいもの runtimeOnly("${jdbcDriver}") implementation("com.integralblue:log4jdbc-spring-boot-starter:1.0.2") implementation("org.simpleframework:simple-xml:2.7.1") implementation("com.univocity:univocity-parsers:2.7.5") implementation("com.google.guava:guava:26.0-jre") testImplementation("org.dbunit:dbunit:2.5.4") testImplementation("com.icegreen:greenmail:1.5.8") testImplementation("org.assertj:assertj-core:3.11.0") testImplementation("com.jayway.jsonpath:json-path:2.4.0") testImplementation("org.jsoup:jsoup:1.11.3") testImplementation("cglib:cglib-nodep:3.2.7") testImplementation("org.spockframework:spock-core:${spockVersion}") testImplementation("org.spockframework:spock-spring:${spockVersion}") testImplementation("com.google.code.findbugs:jsr305:3.0.2") // for lombok annotationProcessor("org.projectlombok:lombok:${lombokVersion}") compileOnly("org.projectlombok:lombok:${lombokVersion}") testCompileOnly("org.projectlombok:lombok:${lombokVersion}") // for Doma annotationProcessor("org.seasar.doma:doma:${domaVersion}") implementation("org.seasar.doma:doma:${domaVersion}") domaGenRuntime("org.seasar.doma:doma-gen:${domaVersion}") domaGenRuntime("${jdbcDriver}") // for Error Prone ( http://errorprone.info/ ) errorprone("com.google.errorprone:error_prone_core:${errorproneVersion}") compileOnly("com.google.errorprone:error_prone_annotations:${errorproneVersion}") // PowerMock testImplementation("org.powermock:powermock-module-junit4:${powermockVersion}") testImplementation("org.powermock:powermock-api-mockito2:${powermockVersion}") } ..........
- 先頭に記述していた以下の2行は buildscript block の中の ext block に移動します。また version は
2.0.4-RELEASE
に変更します。これにより buildscript block が一番上になります。group 'ksbysample'
version '1.5.4-RELEASE'
→version '2.0.4-RELEASE'
- buildscript block の以下の点を変更します。
- ext block から
springBootVersion = '1.5.4.RELEASE'
を削除します。Spring Boot のバージョンは plugins block 内のorg.springframework.boot
プラグインで指定します。 - repositories block 内で
jcenter()
→mavenCentral()
に変更します。 - plugins block を書くように変更すると dependencies block は不要になるので削除します。
- ext block から
- プラグインの書き方を
apply plugin: ...
から plugins block に変更します。ここは変更内容が大きいので詳細は上を見てください。Gradle plugin のバージョンは https://plugins.gradle.org/ で検索して調べます。 springBoot { buildInfo() }
を追加します。- repositories block で
jcenter()
→mavenCentral()
に変更します。 - dependencyManagement block の以下の点を変更します。
mavenBom("io.spring.platform:platform-bom:Brussels-SR3")
→mavenBom(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES)
に変更します。- Spring Boot の BOM では guava は記述されていないので、
bomProperty 'guava.version', '21.0'
を削除します。 - Thymeleaf 関連のライブラリは Spring Boot の BOM に最新バージョン(3.0.9.RELEASE等)が記述されているので、以下の5行を削除します。
bomProperty 'thymeleaf.version', '3.0.6.RELEASE'
bomProperty 'thymeleaf-extras-springsecurity4.version', '3.0.2.RELEASE'
bomProperty 'thymeleaf-layout-dialect.version', '2.2.2'
bomProperty 'thymeleaf-extras-data-attribute.version', '2.0.1'
bomProperty 'thymeleaf-extras-java8time.version', '3.0.0.RELEASE'
- bootRepackage タスクは書かなくても動作するので削除します。
- dependencies block の以下の点を変更します。
- 非推奨になったメソッドを書き換えます。
compile
→implementation
testCompile
→testImplementation
runtime
→runtimeOnly
- Lombok と Doma 2 は annotation processing を使用しているので、
annotationProcessor
の行を追加します。詳細は Spring Boot + npm + Geb で入力フォームを作ってテストする ( その65 )( Gradle を 4.6 → 4.8.1 へ、Checkstyle を 8.8 → 8.11 へ、PMD を 6.4.0 → 6.5.0 へ、error-prone を 2.2.0 → 2.3.1 へバージョンアップする ) 参照。annotationProcessor("org.projectlombok:lombok:${lombokVersion}")
annotationProcessor("org.seasar.doma:doma:${domaVersion}")
- 非推奨になったメソッドを書き換えます。
- ライブラリのバージョンを最新にします。ライブラリの最新バージョンは主に https://mvnrepository.com/ で調べます(一部ライブラリの GitHub ページ等を見る必要あり)。
def jdbcDriver = "org.postgresql:postgresql:42.1.1"
→def jdbcDriver = "org.postgresql:postgresql:42.2.4"
def domaVersion = "2.16.1"
→def domaVersion = "2.19.3"
def lombokVersion = "1.16.16"
→def lombokVersion = "1.18.2"
def errorproneVersion = "2.0.15"
→def errorproneVersion = "2.3.1"
def powermockVersion = "1.6.6"
→def powermockVersion = "2.0.0-beta.5"
implementation("com.integralblue:log4jdbc-spring-boot-starter:1.0.1")
→implementation("com.integralblue:log4jdbc-spring-boot-starter:1.0.2")
implementation("com.univocity:univocity-parsers:2.4.1")
→implementation("com.univocity:univocity-parsers:2.7.5")
testImplementation("org.dbunit:dbunit:2.5.3")
→testImplementation("org.dbunit:dbunit:2.5.4")
testImplementation("com.icegreen:greenmail:1.5.5")
→testImplementation("com.icegreen:greenmail:1.5.8")
testImplementation("org.assertj:assertj-core:3.8.0")
→testImplementation("org.assertj:assertj-core:3.11.0")
testImplementation("com.jayway.jsonpath:json-path:2.2.0")
→testImplementation("com.jayway.jsonpath:json-path:2.4.0")
testImplementation("org.jsoup:jsoup:1.10.3")
→testImplementation("org.jsoup:jsoup:1.11.3")
testImplementation("cglib:cglib-nodep:3.2.5")
→testImplementation("cglib:cglib-nodep:3.2.7")
- guava はバージョン番号を指定する必要があるので、以下のように変更します。
implementation("com.google.guava:guava")
→implementation("com.google.guava:guava:26.0-jre")
- Spring Boot + npm + Geb で入力フォームを作ってテストする ( その73 )( Spring Boot を 1.5.14 → 2.0.4 へバージョンアップする ) の記事で実施した以下の点も反映します。
implementation("org.springframework.session:spring-session")
→implementation("org.springframework.session:spring-session-core")
に変更します。implementation("org.springframework.session:spring-session-data-redis")
を追加します。implementation("org.apache.tomcat:tomcat-jdbc")
を追加します。testImplementation("org.powermock:powermock-api-mockito:${powermockVersion}")
→testImplementation("org.powermock:powermock-api-mockito2:${powermockVersion}")
に変更します。runtimeOnly("org.springframework.boot:spring-boot-properties-migrator")
を追加します。
変更後、Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新すると、特にエラーは出ずに更新されました。
Rebuild Project でエラーの出るクラスを修正する
clean タスク実行 → Rebuild Project 実行 をするとエラーが出るので修正します。
src/main/java/ksbysample/webapp/lending/config/ApplicationConfig.java
- DataSourceBuilder クラスが import できずエラーが出ていました。import 文を削除した後、Alt+Enter を押して import し直します。
src/main/java/ksbysample/webapp/lending/config/WebMvcConfig.java
- WebMvcConfigurerAdapter が非推奨になりました。WebMvcConfigurerAdapter クラスの JavaDoc の説明を読むと WebMvcConfigurer インターフェースに Java 8 のデフォルトメソッドで実装が追加されたので WebMvcConfigurer インターフェースを使用するよう記述されていました。
extends WebMvcConfigurerAdapter
→implements WebMvcConfigurer
に変更します。 - SpringTemplateEngine クラスが import できずエラーが出ていました。import 文を削除した後、Alt+Enter を押して import class し直します。
src/main/java/ksbysample/webapp/lending/config/WebSecurityConfig.java
- 28. Security から @Order(SecurityProperties.ACCESS_OVERRIDE_ORDER) の記述が削除されていたので、おそらく無くなったものと思われます。@Order(SecurityProperties.ACCESS_OVERRIDE_ORDER) を削除します。
- コンストラクタの引数 userDetailsService で赤波線が出ています。IntelliJ IDEA で原因を表示させると Autowired 可能な Bean が2種類以上あるので、その警告でした。
コンストラクタの引数を
UserDetailsService userDetailsService
→@Qualifier("lendingUserDetailsService") UserDetailsService userDetailsService
に変更します。
src/main/java/ksbysample/webapp/lending/security/LendingUserDetailsService.java
エラーは出ていませんが、クラス名が "~Service" なのに @Component
アノテーションなのは違和感があったので、@Component
→ @Service
アノテーションに変更します。
src/main/java/ksbysample/webapp/lending/web/WebappErrorController.java
- ErrorController クラスが import できずエラーが出ていました。import 文を削除した後、Alt+Enter を押して import class し直します。
org.hibernate.validator.constraintsのorg.hibernate.validator.constraints.NotBlankは非推奨になりました
- org.hibernate.validator.constraints.NotBlank は非推奨になったので javax.validation.constraints.NotBlank に変更します。import 文を削除した後、Alt+Enter で import し直します。
- 修正したのは以下のソースです。
org.springframework.web.servlet.mvc.method.annotationのorg.springframework.web.servlet.mvc.method.annotation.AbstractJsonpResponseBodyAdviceは非推奨になりました
org.springframework.web.servlet.mvc.method.annotation.AbstractJsonpResponseBodyAdvice が非推奨になっていました。AbstractJsonpResponseBodyAdvice のソースを見ると、Spring Framework 5.1 で削除する予定なので CORS を使用するよう書かれています。Javascript で JSONP で呼んでいるところも修正する必要があるので、今は一旦無視します。
build タスク実行時に出るエラーを修正する
An unhandled exception was thrown by the Error Prone static analysis plugin. @Data
error-prone により An unhandled exception was thrown by the Error Prone static analysis plugin.
というメッセージが出て @Data アノテーションで引っかかりました。もう少しエラーが発生している詳細な原因が知りたいので、コマンドプロンプトから gradlew --stacktrace build
コマンドを実行します。
引っかかっている BugPattern がすごく見やすくなっていました。いつの間に。。。 今回は ParameterName で引っかかっているようなので、これを無効にします。build.gradle を以下のように変更します。
[compileJava, compileTestGroovy, compileTestJava]*.options*.encoding = "UTF-8" [compileJava, compileTestGroovy, compileTestJava]*.options*.compilerArgs = [ '-Xlint:all,-options,-processing,-path' , '-Xep:RemoveUnusedImports:WARN' , '-Xep:InsecureCryptoUsage:OFF' , '-Xep:ParameterName:OFF' ]
[compileJava, compileTestGroovy, compileTestJava]*.options*.compilerArgs = [ ... ]
とcompileJava.options.compilerArgs += [ ... ]
に分けていたのを前者だけにまとめます。'-Xep:ParameterName:OFF'
を追加します。
警告:[deprecation] org.mockitoのMatchersは推奨されません
import static org.mockito.Matchers.any;
のMatchers
が非推奨になっていたので、import 文を削除した後 Alt+Enter を押して import し直します。
警告:[deprecation] FilesのtoString(File,Charset)は推奨されません
Guava の Files#toString が非推奨になっていました。最近 Guava は使用しなくなる傾向にあるので(Spring Boot の BOM にも入っていません)、Spring Framework に入っている FileCopyUtils#copyToString に置き換えることにします。
例えば Files.toString(new File("src/test/resources/ksbysample/webapp/lending/helper/mail/assertdata/001/message.txt"), Charsets.UTF_8)
は以下のように変更します。
- フィールドに
@Value("ksbysample/webapp/lending/helper/mail/assertdata/001/message.txt") ClassPathResource messageTxtResource;
を追加する。 FileCopyUtils.copyToString(Files.newReader(messageTxtResource.getFile(), StandardCharsets.UTF_8)));
に変更する。
修正したのは以下のソースです。
- src/test/java/ksbysample/webapp/lending/helper/mail/Mail001HelperTest.java
- src/test/java/ksbysample/webapp/lending/helper/mail/Mail002HelperTest.java
- src/test/java/ksbysample/webapp/lending/helper/mail/Mail003HelperTest.java
- src/test/java/ksbysample/webapp/lending/listener/rabbitmq/InquiringStatusOfBookQueueListenerTest.java
- src/test/java/ksbysample/webapp/lending/web/confirmresult/ConfirmresultControllerTest.java
- src/test/java/ksbysample/webapp/lending/web/lendingapp/LendingappControllerTest.java
- src/test/java/ksbysample/webapp/lending/web/lendingapproval/LendingapprovalControllerTest.java
警告:[deprecation] MockMvcRequestBuildersのfileUpload(String,Object...)は推奨されません
MockMvcRequestBuilders#fileUpload が非推奨になっていたので、MockMvcRequestBuilders#multipart に置き換えます。修正したのは以下のソースです。
次回は。。。
ここまでの変更ではまだ BUILD SUCCESSFUL は出ておらず、以下の問題が残っています。
警告:[deprecation] org.springframework.web.servlet.mvc.method.annotationのAbstractJsonpResponseBodyAdviceは推奨されません
が出ている。FindBugs rule violations were found. See the report at: file:///D:/project-springboot/ksbysample-webapp-lending/build/reports/findbugs/main.html
が出ている。- テストが4件失敗している。
次回は AbstractJsonpResponseBodyAdvice の警告と、失敗しているテストを解消します。
履歴
2018/09/05
初版発行。
2019/02/10
* org.hibernate.validator.constraintsのorg.hibernate.validator.constraints.NotBlankは非推奨になりました の中の記述で org.hibernate.validator.constraints.NotEmpty → org.hibernate.validator.constraints.NotBlank、javax.validation.constraints.NotEmpty → javax.validation.constraints.NotBlank に修正しました。
Spring Boot + Spring Integration でいろいろ試してみる ( その26 )( MessageChannel の capacity を超えたメッセージを送信しようとするとどうなるのか? )
概要
記事一覧はこちらです。
MessageChannel には capacity (MessageChannel に格納できる Message の最大数)を設定できますが、Inbound Channel Adapter から取得した Message 数より capacity の数値が低い場合にどのような動作になるのか気になったので確認します。
参照したサイト・書籍
- Flyway deprecation message logged when using Spring Boot 2
https://stackoverflow.com/questions/51905633/flyway-deprecation-message-logged-when-using-spring-boot-2
目次
- ksbysample-eipapp-channelcapacity プロジェクトを作成する
- 仕様を決める
- Application クラスを変更する
- Flyway 用の SQL ファイルを作成する
- application.properties に DB 接続用の設定を記述する
- ApplicationConfig クラスを作成する
- QueueSourceDto クラスを作成する
- FlowConfig クラスを作成する
- 動作確認
- 【検証】
.transactional(this.transactionManager)
が有効か確認する
手順
ksbysample-eipapp-channelcapacity プロジェクトを作成する
Spring Initializr でプロジェクトの雛形を作成した後、build.gradle を以下のように変更します。
buildscript { ext { springBootVersion = '2.0.4.RELEASE' } repositories { mavenCentral() } dependencies { classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") } } apply plugin: 'java' apply plugin: 'eclipse' apply plugin: 'org.springframework.boot' apply plugin: 'io.spring.dependency-management' group = 'ksbysample' version = '0.0.1-SNAPSHOT' sourceCompatibility = 1.8 repositories { mavenCentral() } dependencies { def lombokVersion = "1.18.2" compile('org.springframework.boot:spring-boot-starter-integration') implementation('org.springframework.boot:spring-boot-starter-jdbc') implementation('org.springframework.integration:spring-integration-jdbc') testCompile('org.springframework.boot:spring-boot-starter-test') implementation("com.h2database:h2:1.4.192") implementation("org.flywaydb:flyway-core:5.1.4") // for lombok annotationProcessor("org.projectlombok:lombok:${lombokVersion}") compileOnly("org.projectlombok:lombok:${lombokVersion}") }
- Spring Integration の 19. JDBC Support の Inbound Channel Adapter の機能を使うので、以下の行を追加します。
implementation('org.springframework.integration:spring-integration-jdbc')
- DataSource や PlatformTransactionManager も利用するので以下の行を追加します。
implementation('org.springframework.boot:spring-boot-starter-jdbc')
- JDBC Inbound Channel Adapter のデータ取得元のテーブルを自動生成するために Flyway を入れます。Flyway は Appendix F. Dependency versions に記載があるのでバージョン番号を指定しなくても良いのですが、最新バージョンを使用したいので指定します。DB には H2 を使用します。
implementation("com.h2database:h2:1.4.192")
implementation("org.flywaydb:flyway-core:5.1.4")
- DTO クラスを作成する時に
@Data
アノテーションを使いたいんで lombok も入れます。
仕様を決める
今回作成するサンプルは以下の仕様にします。
- JDBC Inbound Channel Adapter から取得する Message の数を最大 5 にし、取得した後に送信する MessageChannel の capacity の数を 3 にする。
- JDBC Inbound Channel Adapter からは 1秒毎に Message を取得する。
- 送信先の MessageChannel からは 3秒毎に Message を取得する。取得する時の Message の最大数は 1 とする。
- JDBC Inbound Channel Adapter の取得元のテーブル名を QUEUE_SOURCE とし、テーブルには 10レコード入れておく。
これで取得した5つの Message 全てを次の MessageChannel に送信できないという状況になります。
Application クラスを変更する
src/main/java/ksbysample/eipapp/channelcapacity/Application.java の以下の点を変更します。
package ksbysample.eipapp.channelcapacity; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.data.jpa.JpaRepositoriesAutoConfiguration; import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration; import org.springframework.scheduling.annotation.EnableScheduling; @SpringBootApplication(exclude = {JpaRepositoriesAutoConfiguration.class, HibernateJpaAutoConfiguration.class}) @EnableScheduling public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }
- MessageChannel に格納された Message の数を定期的にログに出力するのにスケジューリングの機能を利用したいので、
@EnableScheduling
アノテーションを付与します。 @SpringBootApplication
→@SpringBootApplication(exclude = {JpaRepositoriesAutoConfiguration.class, HibernateJpaAutoConfiguration.class})
に変更します。
Flyway 用の SQL ファイルを作成する
src/main/resources の下に db/migration ディレクトリを作成します。
src/main/resources/db/migration の下に V1__init.sql を新規作成し、以下の内容を記述します。
CREATE TABLE QUEUE_SOURCE ( seq INT NOT NULL, status INT NOT NULL, value VARCHAR(64) NOT NULL, CONSTRAINT PK_QUEUE_SOURCE PRIMARY KEY (seq) ); INSERT INTO QUEUE_SOURCE VALUES (1, 0, 'その1'); INSERT INTO QUEUE_SOURCE VALUES (2, 0, 'その2'); INSERT INTO QUEUE_SOURCE VALUES (3, 0, 'その3'); INSERT INTO QUEUE_SOURCE VALUES (4, 0, 'その4'); INSERT INTO QUEUE_SOURCE VALUES (5, 0, 'その5'); INSERT INTO QUEUE_SOURCE VALUES (6, 0, 'その6'); INSERT INTO QUEUE_SOURCE VALUES (7, 0, 'その7'); INSERT INTO QUEUE_SOURCE VALUES (8, 0, 'その8'); INSERT INTO QUEUE_SOURCE VALUES (9, 0, 'その9'); INSERT INTO QUEUE_SOURCE VALUES (10, 0, 'その10');
application.properties に DB 接続用の設定を記述する
src/main/resources/application.properties に以下の内容を記述します。
spring.datasource.hikari.jdbc-url=jdbc:h2:mem:channelcapacitydb spring.datasource.hikari.username=sa spring.datasource.hikari.password= spring.datasource.hikari.driver-class-name=org.h2.Driver
ここでアプリケーションを1度起動してエラーが出ないことを確認しておきます。
Flyway.setCallbacks(FlywayCallback) has been deprecated and will be removed in Flyway 6.0. Use Flyway.setCallbacks(Callback) instead.
というメッセージの WARN ログが出ていますが、stackoverflow に Flyway deprecation message logged when using Spring Boot 2 という記事がありました。FlywayAutoConfiguration のソースで Flyway.setCallbacks(FlywayCallback) が使用されていることが原因のようです。
ApplicationConfig クラスを作成する
src/main/java/ksbysampleveipapp/channelcapacity の下に ApplicationConfig.java を新規作成し、以下の内容を記述します。
package ksbysample.eipapp.channelcapacity; import com.zaxxer.hikari.HikariDataSource; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.jdbc.DataSourceBuilder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.EnableMBeanExport; import org.springframework.jmx.support.RegistrationPolicy; import javax.sql.DataSource; @Configuration @EnableMBeanExport(registration = RegistrationPolicy.IGNORE_EXISTING) public class ApplicationConfig { @Bean @ConfigurationProperties("spring.datasource.hikari") public DataSource dataSource() { return DataSourceBuilder.create() .type(HikariDataSource.class) .build(); } }
QueueSourceDto クラスを作成する
src/main/java/ksbysampleveipapp/channelcapacity の下に QueueSourceDto.java を新規作成し、以下の内容を記述します。
package ksbysample.eipapp.channelcapacity; import lombok.Data; import lombok.NoArgsConstructor; @Data @NoArgsConstructor public class QueueSourceDto { private int seq; private int status; private String value; }
FlowConfig クラスを作成する
src/main/java/ksbysampleveipapp/channelcapacity の下に FlowConfig.java を新規作成し、以下の内容を記述します。
jdbcMessageSource → selectDbFlow → dstChannel → getDstChannelFlow の順で Message が流れます。
package ksbysample.eipapp.channelcapacity; import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.integration.channel.NullChannel; import org.springframework.integration.channel.QueueChannel; import org.springframework.integration.core.MessageSource; import org.springframework.integration.dsl.IntegrationFlow; import org.springframework.integration.dsl.IntegrationFlows; import org.springframework.integration.dsl.Pollers; import org.springframework.integration.dsl.channel.MessageChannels; import org.springframework.integration.handler.LoggingHandler; import org.springframework.integration.jdbc.JdbcPollingChannelAdapter; import org.springframework.jdbc.core.BeanPropertyRowMapper; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.transaction.PlatformTransactionManager; import javax.sql.DataSource; @Slf4j @Configuration public class FlowConfig { private final DataSource dataSource; private final PlatformTransactionManager transactionManager; private final NullChannel nullChannel; public FlowConfig(DataSource dataSource , PlatformTransactionManager transactionManager , NullChannel nullChannel) { this.dataSource = dataSource; this.transactionManager = transactionManager; this.nullChannel = nullChannel; } @Bean public MessageSource<Object> jdbcMessageSource() { JdbcPollingChannelAdapter adapter = new JdbcPollingChannelAdapter(this.dataSource , "select * from QUEUE_SOURCE where status = 0"); adapter.setRowMapper(new BeanPropertyRowMapper<>(QueueSourceDto.class)); adapter.setUpdateSql("update QUEUE_SOURCE set status = 1 where seq in (:seq)"); return adapter; } @Bean public IntegrationFlow selectDbFlow() { return IntegrationFlows.from(jdbcMessageSource() , e -> e.poller(Pollers .fixedDelay(1000) .maxMessagesPerPoll(5) .transactional(this.transactionManager))) // 取得したデータは List 形式で全件 payload にセットされているので、split で1payload1データに分割する .split() .<QueueSourceDto>log(LoggingHandler.Level.WARN, m -> "☆☆☆ " + m.getPayload().getSeq()) .channel(dstChannel()) .get(); } @Bean public QueueChannel dstChannel() { return MessageChannels.queue(3).get(); } @Bean public IntegrationFlow getDstChannelFlow() { return IntegrationFlows.from(dstChannel()) .bridge(e -> e.poller(Pollers .fixedDelay(3000) .maxMessagesPerPoll(1))) .<QueueSourceDto>log(LoggingHandler.Level.ERROR, m -> "★★★ " + m.getPayload().getSeq()) .channel(nullChannel) .get(); } @Scheduled(initialDelay = 1000, fixedDelay = 1000) public void checkDstChannel() { log.info("dstChannel().getQueueSize() = " + dstChannel().getQueueSize()); } }
動作確認
アプリケーションを実行してみます。
取得したログを見ると以下のように処理されるようです。
- dstChannel に capacity 分の Message が溜まっていると送信する部分の処理(
.channel(dstChannel())
)で止まり、JDBC Inbound Channel Adapter からデータを取りません。 - JDBC Inbound Channel Adapter から最初は
.maxMessagesPerPoll(5)
で指定した5件取得しますが、その後は5件から減った分の1件しか取得しません。常に5件ずつ取得する訳ではありませんでした。
【検証】.transactional(this.transactionManager)
が有効か確認する
※ここから先のコードはコミットしません。
JDBC Inbound Channel Adapter からデータを取得する時に e -> e.poller(Pollers.fixedDelay(1000).maxMessagesPerPoll(5).transactional(this.transactionManager))
と .transactional(this.transactionManager)
を付けてトランザクションを有効にしていますが、本当に効いているのか確認してみます。
dstChannel に Message を送信する時にタイムアウト時間を設定して、タイムアウトした時には RuntimeException を throw するようにします。src/main/java/ksbysampleveipapp/channelcapacity/FlowConfig.java を以下のように変更します。
@Bean public IntegrationFlow selectDbFlow() { return IntegrationFlows.from(jdbcMessageSource() , e -> e.poller(Pollers .fixedDelay(1000) .maxMessagesPerPoll(5) .transactional(this.transactionManager))) // 取得したデータは List 形式で全件 payload にセットされているので、split で1payload1データに分割する .split() .<QueueSourceDto>log(LoggingHandler.Level.WARN, m -> "☆☆☆ " + m.getPayload().getSeq()) // .channel(dstChannel()) .handle((p, h) -> { dstChannel().send(new GenericMessage<>(p, h), 2000); return null; }) .get(); } static class DstChannelInterceptor implements ChannelInterceptor { @Override public void postSend(Message<?> message, MessageChannel channel, boolean sent) { // postSend メソッドは MessageChannel に Message を送信した後に呼び出される // 引数の sent は Message の送信に成功すると true が、失敗すると false がセットされるので、 // false の時に RuntimeException を throw させる if (!sent) { throw new RuntimeException("Can't send message."); } } } @Bean public QueueChannel dstChannel() { QueueChannel queueChannel = MessageChannels.queue(3).get(); queueChannel.addInterceptor(new DstChannelInterceptor()); return queueChannel; }
- ChannelInterceptor インターフェースの実装クラス DstChannelInterceptor を追加します。
- dstChannel Bean に
queueChannel.addInterceptor(new DstChannelInterceptor());
を追加します。 - selectDbFlow Bean の dstChannel へのメッセージを送信処理を
.channel(dstChannel())
→.handle((p, h) -> { dstChannel().send(new GenericMessage<>(p, h), 2000); return null; })
に変更して、取得される時間(3秒)より短い時間(2秒)でタイムアウトさせます。
アプリケーションを実行してみます。
最低でも dstChannel で1件目が処理されていますが、RuntimeException が発生すれると1件目から取り直されていました。adapter.setUpdateSql("update QUEUE_SOURCE set status = 1 where seq in (:seq)");
で update されたのが commit されていないようです。どのような SQL が実行されているか確認してみます。
log4jdbc を入れるので、build.gradle に implementation("com.integralblue:log4jdbc-spring-boot-starter:1.0.2")
を追加します。
dependencies { def lombokVersion = "1.18.2" compile('org.springframework.boot:spring-boot-starter-integration') implementation('org.springframework.boot:spring-boot-starter-jdbc') implementation('org.springframework.integration:spring-integration-jdbc') testCompile('org.springframework.boot:spring-boot-starter-test') implementation("com.h2database:h2:1.4.192") implementation("org.flywaydb:flyway-core:5.1.4") implementation("com.integralblue:log4jdbc-spring-boot-starter:1.0.2") // for lombok annotationProcessor("org.projectlombok:lombok:${lombokVersion}") compileOnly("org.projectlombok:lombok:${lombokVersion}") }
src/main/properties/application.properties に log4jdbc の設定を追加します。
spring.datasource.hikari.jdbc-url=jdbc:h2:mem:channelcapacitydb spring.datasource.hikari.username=sa spring.datasource.hikari.password= spring.datasource.hikari.driver-class-name=org.h2.Driver spring.datasource.hikari.register-mbeans=true # log4jdbc-log4j2 logging.level.jdbc.sqlonly=DEBUG logging.level.jdbc.sqltiming=INFO logging.level.jdbc.audit=INFO logging.level.jdbc.resultset=ERROR logging.level.jdbc.resultsettable=ERROR logging.level.jdbc.connection=DEBUG
アプリケーションを実行してみます。
.maxMessagesPerPoll(5)
を指定しているので5件しか update していないと思っていましたが、テーブル内にある10件全て取得して update されていました。なるほどそんな動作をするのか。。。
また RuntimeException が発生しているので rollback もされていました。.transactional(this.transactionManager)
は有効でした。
dstChannel で処理した分は commit して処理し直されないようにしてみます。src/main/java/ksbysampleveipapp/channelcapacity/FlowConfig.java を以下のように変更します。
@Bean public MessageSource<Object> jdbcMessageSource() { JdbcPollingChannelAdapter adapter = new JdbcPollingChannelAdapter(this.dataSource , "select * from QUEUE_SOURCE where status = 0"); adapter.setRowMapper(new BeanPropertyRowMapper<>(QueueSourceDto.class)); adapter.setUpdateSql("update QUEUE_SOURCE set status = 1 where seq in (:seq)"); adapter.setMaxRowsPerPoll(1); return adapter; } @Bean public IntegrationFlow selectDbFlow() { return IntegrationFlows.from(jdbcMessageSource() , e -> e.poller(Pollers .fixedDelay(1000) .maxMessagesPerPoll(1) .transactional(this.transactionManager))) // 取得したデータは List 形式で全件 payload にセットされているので、split で1payload1データに分割する .split() .<QueueSourceDto>log(LoggingHandler.Level.WARN, m -> "☆☆☆ " + m.getPayload().getSeq()) // .channel(dstChannel()) .handle((p, h) -> { dstChannel().send(new GenericMessage<>(p, h), 2000); return null; }) .get(); }
- jdbcMessageSource Bean の処理に
adapter.setMaxRowsPerPoll(1);
を追加します。 - selectDbFlow Bean の処理で、
.maxMessagesPerPoll(5)
→.maxMessagesPerPoll(1)
に変更します。
これで1件毎に commit されるはずです。アプリケーションを実行してみます。
1件目は dstChannel に Message を送信したら commit されていました。
7件目でタイムアウトが発生しましたが、次に取り直されたのは7件目でした。
結論としては、以下のような考慮が必要になるものと思われます。
- JdbcPollingChannelAdapter のデータ取得元のテーブルに件数がある場合には、JdbcPollingChannelAdapter#setMaxRowsPerPoll による取得件数の設定は必須。
- 取得した後の処理ですぐに split しない方が多分良さそうです。
- rollback が発生した時のことを考慮して(どこから処理をやり直してよいのか?)、MessageSource から取得するデータ件数、送信先の MessageChannel の capacity や MessageChannel から取得する件数等を考える必要があります。
履歴
2018/09/01
初版発行。