Spring Boot + Spring Integration でいろいろ試してみる ( その19 )( Flow の途中で一時的に別の Flow を実行したいなら wireTap! )
概要
記事一覧はこちらです。
- Spring Boot + Spring Integration でいろいろ試してみる ( その17 )( @MessagingGateway でメソッド呼び出しのインターフェースで MessageChannel へ Message を送信する ) で、Flow の途中で FTP アップロードするために
.handleWithAdapter(a -> a.ftp(...))
を呼び出す方法として ExpressionEvaluatingRequestHandlerAdvice +.bridge(e -> e.advice(...))
の組み合わせで実装していたのですが、Flow の途中で別の Flow を実行するための.wireTap(...)
というメソッドが用意されていることに気付いたので、今回は.wireTap(...)
のサンプルを作成します。
参照したサイト・書籍
- Spring Integration Reference Manual - Wire Tap
http://docs.spring.io/spring-integration/docs/4.3.8.RELEASE/reference/html/messaging-channels-section.html#channel-wiretap
目次
- wireTap になぜ気づいたのか?
- in, out ディレクトリを作成する
- ksbysample-eipapp-wiretap プロジェクトを作成する
- サンプルの Flow を作成する
- 動作確認
- 最後に
手順
wireTap になぜ気づいたのか?
- まず Spring Integration Reference Manual を初めに読んだ時には全く頭に残っていませんでした。
- Spring Integration DSL に興味を持つようになって、DSL が書いてあるところはざっと見直したのですが、Wire Tap の DSL のサンプルは以下のようなコードで、MessageChannel 絡みの何かかな?、必要に感じたらまた見直そう、程度にしか思っていなかったはず。
@Bean public PollableChannel myChannel() { return MessageChannels.queue() .wireTap("loggingFlow.input") .get(); }
- Spring Integration Java DSL Reference に書かれているものは以下のコードで、これでは
.wireTap(...)
というメソッドがあることは全然分かりません。
@Bean public MessageChannel priorityChannel() { return MessageChannels.priority(this.mongoDbChannelMessageStore, "priorityGroup") .interceptor(wireTap()) .get(); }
- Spring Integration Java DSL: Line by line tutorial のサンプルでも wireTap は全然出てきません。そうすると Spring Integration を習得する上で覚えるべきものと認識されません。サンプルに出てくるメソッドからまず覚えようとします。
こんな感じで、wireTap は全く気にもしていませんでした。それがなぜ気づいたのかというと、
- Java で Enterprise Integration Pattern 用のフレームワークとして Spring Integration 以外に Apache Camel があります。
- Apache Camel には大量の Component が用意されていて、AWS 関連のコンポーネントとかが使えるとやれることが増えそうだな、と思いました。
- Component の中に Spring Integration Component という Component を見かけて、Spring Integration –> Apache Camel 連携は普通にできそうな感じが。
- でも Spring Support 等を見て少し使ってみようとしましたが、さっぱり分からず。。。 Spring Integration DSL が少しは使えるようになったので Apache Camel も分かるだろう、と思っていましたが、なんというか概要・全体像的なところは似ているのですが、詳細のところでどうしてよいのかがさっぱり分かりません。
- Web を見ていても分からなそうだったので、英語の書籍がないか探して以下の2冊を kindle 版で購入。
- 作者: Scott Cranton,Jakub Korab
- 出版社/メーカー: Packt Publishing
- 発売日: 2013/12/26
- メディア: Kindle版
- この商品を含むブログを見る
- 作者: Jean-Baptiste Onofré
- 出版社/メーカー: Packt Publishing
- 発売日: 2015/06/30
- メディア: Kindle版
- この商品を含むブログを見る
- Mastering Apache Camel を読んでいたところ、「Chaper 2.Message Routing」に “Wire Tap - sending a copy of the message elsewhere” の文章が! メッセージのコピーを他に送れるのかな?と思って本の Wire Tap の章を読んでみましたが、確かにメイン Flow のメッセージはそのままで、途中で別のフローにメッセージのコピーを送信できる仕組みでした。
- Spring Integration でも WireTap ないのかな?と思って Spring Integration Reference Manual を見ると Wire Tap の記述が!
- IntegrationFlow でも書けるのか?と思って試してみたところ、IntegrationFlowDefinition#wireTap がちゃんとありました。メイン Flow の Message は変更せずに、途中に別の Flow を実行できる仕組みでした。
すっごい遠回りでした。Spring Integration のマニュアルや記事だけ見ていると wireTap の使い方って分からないのでは。。。
in, out ディレクトリを作成する
サンプルアプリケーションを作成します。常駐型アプリケーション用として以下の構成のディレクトリを作成します。
C:\eipapp\ksbysample-eipapp-wiretap ├ in └ out
ksbysample-eipapp-wiretap プロジェクトを作成する
IntelliJ IDEA で Gradle プロジェクトを作成し、build.gradle を リンク先の内容 に変更します。
ksbysample-eipapp-wiretap プロジェクトのルート直下に config/checkstyle, config/findbugs ディレクトリを作成します。
config/checkstyle の下に ksbysample-eipapp-messaginggateway プロジェクトの config/checkstyle の下にある google_checks.xml をコピーします。
config/findbugs の下に findbugs-exclude.xml を新規作成し、リンク先の内容 の内容に変更します。
src/main/java の下に ksbysample.eipapp.wiretap パッケージを作成します。
src/main/java/ksbysample/eipapp/wiretap の下に Application.java を作成し、リンク先の内容 を記述します。
src/main/resources の下に application.properties を作成し、リンク先の内容 を記述します。
src/main/resources の下に logback-spring.xml を作成し、リンク先の内容 を記述します。
サンプルの Flow を作成する
サンプルの Flow は以下の仕様で作成します。
src/main/java/ksbysample/eipapp/wiretap の下に FlowConfig.java を新規作成し、リンク先の内容 を記述します。
動作確認
動作確認します。テストには以下のファイルを使用します。
■2014.txt
http://ksby.hatenablog.com/entry/2014/12/27/233427 http://ksby.hatenablog.com/entry/2014/12/29/175849
smtp4dev, freeFTPd を起動します。
ZipkinServer を起動します。今回は起動するだけです。
bootRun を実行して ksbysample-eipapp-wiretap を起動します。
C:\eipapp\ksbysample-eipapp-wiretap\in の下に 2014.txt を置きます。
SFTP サーバに 2014.txt がアップロードされて /in ディレクトリからはファイルが削除されます。
/out ディレクトリにもファイルは残っていません。
メールも届いており、ファイルの中身がメール本文になっています。
SFTP サーバにアップロードされた 2014.txt は以下の内容です。メールで送信した To, Subject が出力されています。
起動したサーバと ksbysample-eipapp-wiretap を停止します。
最後に
Spring Integration Java DSL Reference の Using Protocol Adapters に記載されている Adapter を使う前なら気にしなかったのですが、使うようになったら wireTap を覚えると便利です。個人的には OutboundAdapter って何か使い勝手悪いなあ、と思っていたのが解消されました。
Spring Integration というか Enterprise Integration Patterns ですが、最初何のためにそんな機能があるのかよく分からなくて、慣れてきてやっと使い方が分かるものがあります。もう少し具体的なサンプルで、そういうふうに使えばいいんだと分かるものがあるといいんですけどね。
Spring Integratin にも Mastering Apache Camel のように簡単な例がいくつも載っている本があるといいな、とは思いました。できれば Spring Integration DSL ベースで。
ソースコード
build.gradle
group 'ksbysample' version '1.0.0-RELEASE' buildscript { ext { springBootVersion = '1.4.5.RELEASE' } repositories { mavenCentral() maven { url "http://repo.spring.io/repo/" } maven { url "https://plugins.gradle.org/m2/" } } dependencies { classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") classpath("io.spring.gradle:dependency-management-plugin:0.6.1.RELEASE") // for Error Prone ( http://errorprone.info/ ) classpath("net.ltgt.gradle:gradle-errorprone-plugin:0.0.9") } } apply plugin: 'java' apply plugin: 'eclipse' apply plugin: 'idea' apply plugin: 'org.springframework.boot' apply plugin: 'io.spring.dependency-management' apply plugin: 'groovy' apply plugin: 'net.ltgt.errorprone' apply plugin: 'checkstyle' apply plugin: 'findbugs' sourceCompatibility = 1.8 targetCompatibility = 1.8 [compileJava, compileTestGroovy, compileTestJava]*.options*.compilerArgs = ['-Xlint:all,-options,-processing,-path'] compileJava.options.compilerArgs += ['-Xep:RemoveUnusedImports:WARN'] idea { module { inheritOutputDirs = false outputDir = file("$buildDir/classes/main/") } } checkstyle { configFile = file("${rootProject.projectDir}/config/checkstyle/google_checks.xml") toolVersion = '7.6' 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 } } repositories { mavenCentral() maven { url "http://repo.spring.io/repo/" } } dependencyManagement { imports { mavenBom("io.spring.platform:platform-bom:Athens-SR4") { bomProperty 'guava.version', '21.0' } mavenBom("org.springframework.cloud:spring-cloud-dependencies:Camden.RELEASE") } } dependencies { def spockVersion = "1.1-groovy-2.4-rc-3" def lombokVersion = "1.16.12" def errorproneVersion = '2.0.15' // dependency-management-plugin によりバージョン番号が自動で設定されるもの // Appendix A. Dependency versions ( http://docs.spring.io/platform/docs/current/reference/htmlsingle/#appendix-dependency-versions ) 参照 compile("org.springframework.boot:spring-boot-starter-integration") compile("org.springframework.boot:spring-boot-starter-mail") compile("org.springframework.integration:spring-integration-mail") compile("org.springframework.integration:spring-integration-sftp") compile("org.codehaus.janino:janino") compile("com.google.guava:guava") testCompile("org.springframework.boot:spring-boot-starter-test") // org.springframework.cloud:spring-cloud-dependencies によりバージョン番号が自動で設定されるもの // http://projects.spring.io/spring-cloud/ の「Release Trains」参照 compile("org.springframework.cloud:spring-cloud-starter-zipkin") { exclude module: 'spring-boot-starter-web' } // dependency-management-plugin によりバージョン番号が自動で設定されないもの、あるいは最新バージョンを指定したいもの compile("org.springframework.integration:spring-integration-java-dsl:1.2.1.RELEASE") testCompile("org.assertj:assertj-core:3.6.2") testCompile("org.spockframework:spock-core:${spockVersion}") testCompile("org.spockframework:spock-spring:${spockVersion}") // for lombok compileOnly("org.projectlombok:lombok:${lombokVersion}") testCompileOnly("org.projectlombok:lombok:${lombokVersion}") // for Error Prone ( http://errorprone.info/ ) errorprone("com.google.errorprone:error_prone_core:${errorproneVersion}") compileOnly("com.google.errorprone:error_prone_annotations:${errorproneVersion}") }
findbugs-exclude.xml
<?xml version="1.0" encoding="UTF-8"?> <FindBugsFilter> </FindBugsFilter>
Application.java
package ksbysample.eipapp.wiretap; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.integration.annotation.IntegrationComponentScan; @SpringBootApplication @IntegrationComponentScan public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }
application.properties
spring.application.name=eipapp spring.zipkin.base-url=http://localhost:9411/ spring.sleuth.sampler.percentage=1.0
logback-spring.xml
<?xml version="1.0" encoding="UTF-8"?> <configuration> <include resource="org/springframework/boot/logging/logback/defaults.xml"/> <springProperty scope="context" name="springAppName" source="spring.application.name"/> <property name="CONSOLE_LOG_PATTERN" value="%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(${level:-%5p}) %clr([${springAppName:-},%X{X-B3-TraceId:-},%X{X-B3-SpanId:-},%X{X-Span-Export:-}]){yellow} %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}"/> <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern>${CONSOLE_LOG_PATTERN}</pattern> <charset>UTF-8</charset> </encoder> </appender> <root level="INFO"> <appender-ref ref="CONSOLE"/> </root> <logger name="org.springframework.integration.expression.ExpressionUtils" level="ERROR"/> <logger name="com.jcraft.jsch" level="ERROR"/> </configuration>
FlowConfig.java
package ksbysample.eipapp.wiretap; import com.jcraft.jsch.ChannelSftp; import org.aopalliance.aop.Advice; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.integration.dsl.IntegrationFlow; import org.springframework.integration.dsl.IntegrationFlows; import org.springframework.integration.dsl.core.Pollers; import org.springframework.integration.dsl.support.Transformers; import org.springframework.integration.file.FileHeaders; import org.springframework.integration.file.FileReadingMessageSource; import org.springframework.integration.file.filters.AcceptAllFileListFilter; import org.springframework.integration.file.filters.IgnoreHiddenFileListFilter; import org.springframework.integration.file.remote.session.SessionFactory; import org.springframework.integration.handler.advice.RequestHandlerRetryAdvice; import org.springframework.integration.mail.MailHeaders; import org.springframework.integration.sftp.session.DefaultSftpSessionFactory; import org.springframework.integration.support.MessageBuilder; import org.springframework.retry.backoff.ExponentialBackOffPolicy; import org.springframework.retry.policy.SimpleRetryPolicy; import org.springframework.retry.support.RetryTemplate; import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Paths; import static java.util.Collections.singletonMap; @Configuration public class FlowConfig { private static final String ROOT_DIR = "C:/eipapp/ksbysample-eipapp-wiretap"; private static final String IN_DIR = ROOT_DIR + "/in"; private static final String OUT_DIR = ROOT_DIR + "/out"; private static final String MAIL_FROM = "system@sample.com"; private static final String MAIL_TO = "download@test.co.jp"; private static final String SFTP_UPLOAD_DIR = "/in"; private static final String CRLF = "\r\n"; @Value("${spring.mail.host:localhost}") private String mailHost; @Value("${spring.mail.port:25}") private int mailPort; @Value("${spring.mail.protocol:smtp}") private String mailProtocol; @Value("${spring.mail.default-encoding:UTF-8}") private String mailDefaultEncoding; /** * SFTP サーバに接続するための SessionFactory オブジェクトを生成する * 今回は CachingSessionFactory を使用せず処理毎に接続・切断されるようにする * * @return SFTP サーバ接続用の SessionFactory オブジェクト */ @Bean public SessionFactory<ChannelSftp.LsEntry> sftpSessionFactory() { DefaultSftpSessionFactory factory = new DefaultSftpSessionFactory(true); factory.setHost("localhost"); factory.setPort(22); factory.setUser("send01"); factory.setPassword("send01"); factory.setAllowUnknownKeys(true); return factory; } /** * EIP の1つ wireTap のサンプル Flow * * @return IntegrationFlow オブジェクト */ @Bean public IntegrationFlow wiretapSampleFlow() { return IntegrationFlows // C:/eipapp/ksbysample-eipapp-wiretap/in にファイルが作成されたか 1秒間隔でチェックする .from(s -> s.file(new File(IN_DIR)) // 同じファイルが置かれても処理する .filter(new AcceptAllFileListFilter<>()) .filter(new IgnoreHiddenFileListFilter()) // ファイルが新規作成された時だけ Message を送信する // これを入れないとファイルが存在する限り何度も Message が送信され続ける .useWatchService(true) .watchEvents(FileReadingMessageSource.WatchEventType.CREATE) , e -> e.poller(Pollers.fixedDelay(1000))) // ファイル名とファイルの絶対パス、メール送信用の From, To, Subject を Message の header にセットする .enrichHeaders(h -> h .headerExpression(FileHeaders.FILENAME, "payload.name") .headerExpression(FileHeaders.ORIGINAL_FILE, "payload.absolutePath") .header(MailHeaders.FROM, MAIL_FROM) .header(MailHeaders.TO, MAIL_TO) .headerExpression(MailHeaders.SUBJECT, "payload.name")) .wireTap(f -> f // File の内容を読み込んで payload へセットする .transform(Transformers.fileToString()) .wireTap(sf -> sf // メールを送信する .handleWithAdapter(a -> a.mail(this.mailHost) .port(this.mailPort) .protocol(this.mailProtocol) .defaultEncoding(this.mailDefaultEncoding))) .wireTap(sf -> sf // payload の内容をファイルに出力する内容に変更する .handle((p, h) -> { StringBuilder sb = new StringBuilder(); sb.append("To: " + h.get(MailHeaders.TO) + CRLF); sb.append("Subject: " + h.get(MailHeaders.SUBJECT) + CRLF); sb.append(CRLF); sb.append(p); return MessageBuilder.withPayload(sb.toString()) .build(); }) // /out ディレクトリにファイルを生成する // ファイル名は header 内の FileHeaders.FILENAME のキー名の文字列が使用される .handleWithAdapter(a -> a.file(new File(OUT_DIR)))) .channel("nullChannel")) .wireTap(f -> f // payload の File クラスを /out ディレクトリのファイルに変更する .handle((p, h) -> Paths.get(OUT_DIR, (String) h.get(FileHeaders.FILENAME)).toFile()) // SFTP サーバにファイルをアップロードする .handleWithAdapter(a -> a.sftp(sftpSessionFactory()) .remoteDirectory(SFTP_UPLOAD_DIR) , e -> e.advice(sftpUploadRetryAdvice()))) // /in, /out ディレクトリのファイルを削除する .<File>handle((p, h) -> { try { Files.delete(Paths.get(p.getAbsolutePath())); Files.delete(Paths.get(OUT_DIR, p.getName())); } catch (IOException e) { throw new RuntimeException(e); } return null; }) .get(); } /** * リトライは最大5回 ( SimpleRetryPolicy で指定 )、 * リトライ間隔は初期値2秒、最大10秒、倍数2.0 ( ExponentialBackOffPolicy で指定 ) * の RequestHandlerRetryAdvice オブジェクトを生成する * * @return RequestHandlerRetryAdvice オブジェクト */ @Bean public Advice sftpUploadRetryAdvice() { RetryTemplate retryTemplate = new RetryTemplate(); retryTemplate.setRetryPolicy( new SimpleRetryPolicy(5, singletonMap(Exception.class, true))); ExponentialBackOffPolicy backOffPolicy = new ExponentialBackOffPolicy(); backOffPolicy.setInitialInterval(2000); backOffPolicy.setMaxInterval(10000); backOffPolicy.setMultiplier(2.0); retryTemplate.setBackOffPolicy(backOffPolicy); RequestHandlerRetryAdvice advice = new RequestHandlerRetryAdvice(); advice.setRetryTemplate(retryTemplate); return advice; } }
履歴
2017/03/12
初版発行。
Spring Boot 1.3.x の Web アプリを 1.4.x へバージョンアップする ( その11 )( Error Prone を 2.0.15 → 2.0.18 へバージョンアップ。。。できませんでした )
概要
記事一覧はこちらです。
- 今回の手順で確認できるのは以下の内容です。
- Error Prone を 2.0.15 → 2.0.18 へバージョンアップします。
- gradle-errorprone-plugin も 0.0.8 → 0.0.9 へバージョンアップします。
- 。。。と思いましたが、Error Prone の 2.0.15 → 2.0.18 へのバージョンアップはできなかったというお話です。
参照したサイト・書籍
目次
- build.gradle を変更する
- clean タスク → Rebuild Project → build タスクを実行する
java.lang.NoSuchMethodError: com.google.common.base.Preconditions.checkArgument(ZLjava/lang/String;Ljava/lang/Object;)V
の原因は?- build.gradle の resolutionStrategy.force で Guava の 21.0 を強制してみる
- なぜ Guava のバージョンが 17.0 にバージョンダウンされるのか?
- BOM で指定されているライブラリのバージョンを変更する
java.lang.ClassCastException: lombok.javac.apt.Javac7BaseFileObjectWrapper cannot be cast to javax.tools.FileObject
の原因は?- 結局、どう対応するのか?
- 最後に
手順
build.gradle を変更する
build.gradle を リンク先のその1の内容 へ変更します。
Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。
clean タスク → Rebuild Project → build タスクを実行する
clean タスク → Rebuild Project → build タスクを実行します。。。が、compileJava でエラーが出て止まりました。
ログを見た感じでは com.google.common.base.Preconditions.checkArgument(ZLjava/lang/String;Ljava/lang/Object;)V
が問題のような気がしますが、これだけではエラーの内容がよく分かりません。
コマンドプロンプトを開いて、プロジェクトのルートディレクトリに移動した
後 gradlew --stackstrace --debug build
コマンドを実行してみます。
以下のログが出力されました。java.lang.NoSuchMethodError: com.google.common.base.Preconditions.checkArgument(ZLjava/lang/String;Ljava/lang/Object;)V
と出ており、com.google.errorprone.BugCheckerInfo.create(BugCheckerInfo.java:98)
からこのメソッドが呼び出されていますが、メソッドがないようです。
07:37:32.768 [ERROR] [org.gradle.BuildExceptionReporter] 07:37:32.771 [ERROR] [org.gradle.BuildExceptionReporter] FAILURE: Build failed with an exception. 07:37:32.772 [ERROR] [org.gradle.BuildExceptionReporter] 07:37:32.772 [ERROR] [org.gradle.BuildExceptionReporter] * What went wrong: 07:37:32.772 [ERROR] [org.gradle.BuildExceptionReporter] Execution failed for task ':compileJava'. 07:37:32.772 [ERROR] [org.gradle.BuildExceptionReporter] > java.lang.reflect.InvocationTargetException 07:37:32.773 [ERROR] [org.gradle.BuildExceptionReporter] 07:37:32.773 [ERROR] [org.gradle.BuildExceptionReporter] * Exception is: 07:37:32.774 [ERROR] [org.gradle.BuildExceptionReporter] org.gradle.api.tasks.TaskExecutionException: Execution failed for task ':compileJava'. .......... 07:37:32.787 [ERROR] [org.gradle.BuildExceptionReporter] Caused by: java.lang.NoSuchMethodError: com.google.common.base.Preconditions.checkArgument(ZLjava/lang/String;Ljava/lang/Object;)V 07:37:32.787 [ERROR] [org.gradle.BuildExceptionReporter] at com.google.errorprone.BugCheckerInfo.create(BugCheckerInfo.java:98) 07:37:32.787 [ERROR] [org.gradle.BuildExceptionReporter] at com.google.errorprone.scanner.BuiltInCheckerSuppliers.getSuppliers(BuiltInCheckerSuppliers.java:239) 07:37:32.788 [ERROR] [org.gradle.BuildExceptionReporter] at com.google.errorprone.scanner.BuiltInCheckerSuppliers.<clinit>(BuiltInCheckerSuppliers.java:268) 07:37:32.788 [ERROR] [org.gradle.BuildExceptionReporter] at com.google.errorprone.ErrorProneCompiler$Builder.<init>(ErrorProneCompiler.java:96) 07:37:32.788 [ERROR] [org.gradle.BuildExceptionReporter] ... 77 more 07:37:32.788 [ERROR] [org.gradle.BuildExceptionReporter] 07:37:32.788 [LIFECYCLE] [org.gradle.BuildResultLogger] 07:37:32.789 [LIFECYCLE] [org.gradle.BuildResultLogger] BUILD FAILED 07:37:32.789 [LIFECYCLE] [org.gradle.BuildResultLogger]
java.lang.NoSuchMethodError: com.google.common.base.Preconditions.checkArgument(ZLjava/lang/String;Ljava/lang/Object;)V
の原因は?
IntelliJ IDEA で com.google.common.base.Preconditions クラスのソースを表示して checkArgument メソッドを見てみましたが、引数が (String, Object)
のメソッドは存在しませんでした。必ず第1引数は boolean です。
checkArgument を呼び出している com.google.errorprone.BugCheckerInfo.create(BugCheckerInfo.java:98)
を見てみましたが、どう見ても引数を3つ渡しています。エラーメッセージが出る意味がよく分かりません。。。
gradlew dependencies
コマンドを実行して依存関係を確認してみます。
出力された結果を見たのですが、なぜか com.google.guava:guava:21.0 -> 17.0
と guava が 17.0 へバージョンダウンされています。出力結果を “com.google.guava:guava” で検索してもどこにも 17.0 が依存関係になっているものが見当たらず、なぜバージョンダウンされるのかが分かりません。。。
errorprone \--- com.google.errorprone:error_prone_core:latest.release -> 2.0.18 +--- com.google.errorprone:error_prone_annotation:2.0.18 | \--- com.google.guava:guava:21.0 -> 17.0 +--- com.google.errorprone:error_prone_check_api:2.0.18 | +--- com.google.errorprone:error_prone_annotation:2.0.18 (*) | +--- com.google.code.findbugs:jsr305:3.0.0 -> 3.0.1 | +--- org.checkerframework:dataflow:1.8.10 | | \--- org.checkerframework:javacutil:1.8.10 | +--- com.google.errorprone:javac:9-dev-r3297-4 | +--- com.googlecode.java-diff-utils:diffutils:1.3.0 | \--- com.google.errorprone:error_prone_annotations:2.0.18 +--- com.github.stephenc.jcip:jcip-annotations:1.0-1 +--- org.pcollections:pcollections:2.1.2 +--- com.google.guava:guava:21.0 -> 17.0 +--- com.google.auto:auto-common:0.7 | \--- com.google.guava:guava:19.0 -> 17.0 +--- com.google.code.findbugs:jFormatString:3.0.0 +--- com.google.code.findbugs:jsr305:3.0.0 -> 3.0.1 +--- org.checkerframework:dataflow:1.8.10 (*) +--- com.google.errorprone:javac:9-dev-r3297-4 \--- com.google.errorprone:error_prone_annotations:2.0.18
build.gradle の resolutionStrategy.force で Guava の 21.0 を強制してみる
resolutionStrategy.force
を記述することでライブラリのバージョンを強制指定することができるはずなので、build.gradle の configuration に resolutionStrategy.force "com.google.guava:guava:21.0"
を追加してみます。
configurations { // for Doma 2 domaGenRuntime // for Error Prone ( http://errorprone.info/ ) errorprone { resolutionStrategy.force "com.google.errorprone:error_prone_core:${errorproneVersion}" resolutionStrategy.force "com.google.guava:guava:21.0" } }
追加後、Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。
gradlew dependencies
コマンドを実行してみましたが com.google.guava:guava:21.0 -> 17.0
のままでした。。。
errorprone \--- com.google.errorprone:error_prone_core:latest.release -> 2.0.18 +--- com.google.errorprone:error_prone_annotation:2.0.18 | \--- com.google.guava:guava:21.0 -> 17.0 +--- com.google.errorprone:error_prone_check_api:2.0.18 | +--- com.google.errorprone:error_prone_annotation:2.0.18 (*) | +--- com.google.code.findbugs:jsr305:3.0.0 -> 3.0.1 | +--- org.checkerframework:dataflow:1.8.10 | | \--- org.checkerframework:javacutil:1.8.10 | +--- com.google.errorprone:javac:9-dev-r3297-4 | +--- com.googlecode.java-diff-utils:diffutils:1.3.0 | \--- com.google.errorprone:error_prone_annotations:2.0.18 +--- com.github.stephenc.jcip:jcip-annotations:1.0-1 +--- org.pcollections:pcollections:2.1.2 +--- com.google.guava:guava:21.0 -> 17.0 +--- com.google.auto:auto-common:0.7 | \--- com.google.guava:guava:19.0 -> 17.0 +--- com.google.code.findbugs:jFormatString:3.0.0 +--- com.google.code.findbugs:jsr305:3.0.0 -> 3.0.1 +--- org.checkerframework:dataflow:1.8.10 (*) +--- com.google.errorprone:javac:9-dev-r3297-4 \--- com.google.errorprone:error_prone_annotations:2.0.18
clean タスク → Rebuild Project → build タスクを実行してみましたが、結果は同じでした。
効果がなかったので build.gradle は元に戻します。
なぜ Guava のバージョンが 17.0 にバージョンダウンされるのか?
build.gradle に Guava の 17.0 なんて記述していないのに Guava がバージョンダウンされる原因となりそうなものに1つ心あたりがありました。それは Spring IO Platform の BOM です。以前マニュアルの Web ページを見て Guava のバージョンが妙に低いな、と思った記憶があります。
現在使用中の Spring IO Platform の Athens-SR3 の Spring IO Platform Reference Guide を開き “guava” で検索してみると、思ったとおり 17.0 でした。
また gradlew dependencies
コマンドの出力結果を見ていて気づきましたが、Spring IO Platform の BOM は gradle plugin の依存関係のライブラリだけ強制的に BOM に記述されているバージョンを利用させるようです。
例えば compile の場合には Guava は build.gradle で明示している 21.0 が使用されていますが、
compile - Dependencies for source set 'main'. +--- org.springframework.boot:spring-boot-starter-web: -> 1.4.4.RELEASE | +--- org.springframework.boot:spring-boot-starter:1.4.4.RELEASE .......... +--- com.google.guava:guava:21.0 ..........
checkstyle プラグインでは 17.0 にバージョンダウンされており、
checkstyle - The Checkstyle libraries to be used for this project. \--- com.puppycrawl.tools:checkstyle:7.5.1 +--- antlr:antlr:2.7.7 +--- org.antlr:antlr4-runtime:4.6 +--- commons-beanutils:commons-beanutils:1.9.3 | \--- commons-collections:commons-collections:3.2.2 +--- commons-cli:commons-cli:1.3.1 \--- com.google.guava:guava:19.0 -> 17.0
errorprone プラグインでも同様に 17.0 にバージョンダウンされています。
errorprone \--- com.google.errorprone:error_prone_core:2.0.18 +--- com.google.errorprone:error_prone_annotation:2.0.18 | \--- com.google.guava:guava:21.0 -> 17.0 +--- com.google.errorprone:error_prone_check_api:2.0.18 | +--- com.google.errorprone:error_prone_annotation:2.0.18 (*) | +--- com.google.code.findbugs:jsr305:3.0.0 -> 3.0.1 | +--- org.checkerframework:dataflow:1.8.10 | | \--- org.checkerframework:javacutil:1.8.10 | +--- com.google.errorprone:javac:9-dev-r3297-4 | +--- com.googlecode.java-diff-utils:diffutils:1.3.0 | \--- com.google.errorprone:error_prone_annotations:2.0.18 +--- com.github.stephenc.jcip:jcip-annotations:1.0-1 +--- org.pcollections:pcollections:2.1.2 +--- com.google.guava:guava:21.0 -> 17.0 +--- com.google.auto:auto-common:0.7 | \--- com.google.guava:guava:19.0 -> 17.0 +--- com.google.code.findbugs:jFormatString:3.0.0 +--- com.google.code.findbugs:jsr305:3.0.0 -> 3.0.1 +--- org.checkerframework:dataflow:1.8.10 (*) +--- com.google.errorprone:javac:9-dev-r3297-4 \--- com.google.errorprone:error_prone_annotations:2.0.18
BOM で指定されているライブラリのバージョンを変更する
BOM で指定されているライブラリのバージョンを指定できる方法がないか Dependency management plugin のマニュアルを見てみたところ、Changing the value of a version property に bomProperty
で変更できることが書いてありました。
http://repo.spring.io/repo/io/spring/platform/platform-bom/Athens-SR3/platform-bom-Athens-SR3.pom を見ると <guava.version>17.0</guava.version>
という記述がありましたので、guava.version
を 21.0
に変更すればよさそうです。
build.gradle を リンク先のその2の内容 に変更します。変更後、Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。
gradlew dependencies
コマンドを実行すると、今度は checkstyle plugin も errorprone plugin も Guava の 21.0 が使用されるようになりました。
checkstyle - The Checkstyle libraries to be used for this project. \--- com.puppycrawl.tools:checkstyle:7.5.1 +--- antlr:antlr:2.7.7 +--- org.antlr:antlr4-runtime:4.6 +--- commons-beanutils:commons-beanutils:1.9.3 | \--- commons-collections:commons-collections:3.2.2 +--- commons-cli:commons-cli:1.3.1 \--- com.google.guava:guava:19.0 -> 21.0 .......... errorprone \--- com.google.errorprone:error_prone_core:2.0.18 +--- com.google.errorprone:error_prone_annotation:2.0.18 | \--- com.google.guava:guava:21.0 +--- com.google.errorprone:error_prone_check_api:2.0.18 | +--- com.google.errorprone:error_prone_annotation:2.0.18 (*) | +--- com.google.code.findbugs:jsr305:3.0.0 -> 3.0.1 | +--- org.checkerframework:dataflow:1.8.10 | | \--- org.checkerframework:javacutil:1.8.10 | +--- com.google.errorprone:javac:9-dev-r3297-4 | +--- com.googlecode.java-diff-utils:diffutils:1.3.0 | \--- com.google.errorprone:error_prone_annotations:2.0.18 +--- com.github.stephenc.jcip:jcip-annotations:1.0-1 +--- org.pcollections:pcollections:2.1.2 +--- com.google.guava:guava:21.0 +--- com.google.auto:auto-common:0.7 | \--- com.google.guava:guava:19.0 -> 21.0 +--- com.google.code.findbugs:jFormatString:3.0.0 +--- com.google.code.findbugs:jsr305:3.0.0 -> 3.0.1 +--- org.checkerframework:dataflow:1.8.10 (*) +--- com.google.errorprone:javac:9-dev-r3297-4 \--- com.google.errorprone:error_prone_annotations:2.0.18
clean タスク → Rebuild Project → build タスクを実行してみます。
今度は lombok 関連と思われるエラーが出て BUILD FAILED になりました。
java.lang.ClassCastException: lombok.javac.apt.Javac7BaseFileObjectWrapper cannot be cast to javax.tools.FileObject
の原因は?
エラーの原因を1つずつ調べていきます。まずは java.lang.ClassCastException: lombok.javac.apt.Javac7BaseFileObjectWrapper cannot be cast to javax.tools.FileObject
からです。
lombok.javac.apt.Javac7BaseFileObjectWrapper クラスのソースを表示させてみたところ、継承元の com.sun.tools.javac.file.BaseFileObject
の javac
が赤字で表示されています。com.sun.tools.javac.file.BaseFileObject
クラスが含まれているライブラリが gradle により自動でロードされていないためのようです。
com.sun.tools.javac.file.BaseFileObject
クラスが含まれるライブラリを調べたところ、com.sun.tools.javac.file.BaseFileObject - snacktrace のページがヒットしました。このライブラリを build.gradle に追記してみます。
build.gradle を リンク先のその3の内容 に変更します。変更後、Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。
lombok.javac.apt.Javac7BaseFileObjectWrapper クラスのソースを表示させてみると、com.sun.tools.javac.file.BaseFileObject
の java
の赤字は解消されていましたが、今度は class の行に赤波線が表示されていました。また LombokFileObject
が赤字です。
IntelliJ IDEA の画面上部のパッケージ階層表示から lombok.javac.apt の下にある一覧を表示させてみましたが LombokFileObject
は存在します。中のソースも見てみましたが、特におかしいところは見られませんでした。
Project Tool Window で org.projectlombok:lombok:1.16.12 の内容を表示させてみたところ、LombokFileObject
は LombokFileObject.SCL.lombok というファイル名になっていてインターフェースとして表示されません。
通常は以下のようにインターフェースのアイコンが表示されます。
この状態で clean タスク → Rebuild Project → build タスクを実行しても以前と同じエラーが出ます。
どうすればよいのか分からなくなったので Web でいろいろ検索してみたところ、以下の GitHub の Issue が見つかりました。
- java.lang.LinkageError: loader constraint violation: when resolving overridden method “lombok.javac.apt.Javac7BaseFileObjectWrapper.getKind()Ljavax/tools/JavaFileObject$Kind;” the class loader (instance of lombok/launch/ShadowClassLoader) of the current class,
- Lombok + error-prone compatibility issues - An annotation processor threw an uncaught exception.
Issue を呼んだ感じでは、
- どうやらこれは lombok + Error Prone を組み合わせた時のエラーらしい。
- Error Prone のバグではなく lombok が javac 9 に対応していないためのようだ。Error Prone の出すメッセージは JDK 9 で問題となる箇所を指摘していたし、IntelliJ IDEA で使用する Java Compiler も「Javac」→「Javac with error-prone」へ変更したので、Error Prone を入れた後のコンパイルは素の JDK 8 のコンパイルとは違って JDK 9 用に何かしているようだ。
- lombok は一旦対応して Error Prone の 2.0.15 ではエラーが出なくなったようだが ( 確かに 2.0.15 ではエラーは出ていなかった! )、2.0.18 でまた別のエラーが出ている模様。
という訳で、lombok が対応しないと Error Prone は 2.0.18 にバージョンアップできなさそうです。Error Prone の 2.0.16 以降でコンパイルエラーにならないバージョンがあるのか確認してみましたが、2.0.16 からダメでした。
結局、どう対応するのか?
以下のように対応することにします。
- net.ltgt.gradle:gradle-errorprone-plugin は 0.0.8 → 0.0.9 へバージョンアップします。
- com.google.errorprone:error_prone_core はバージョンアップせずに 2.0.15 のままとします。
- com.google.errorprone:error_prone_core のバージョンの指定を configurations.errorprone に記述していましたが、Gradle error-prone plugin を見てみたら dependencies に書けることが分かったので、dependencies に書くようにします。
- Guava のように Spring IO Platform の BOM に記述があるものは dependencyManagement の方に bomProperty でバージョンを指定して、dependencies にはバージョン番号を記述しないようにします。以下のライブラリを変更します。
- com.google.errorprone:error_prone_core を 2.0.15 のままにすることにしたので
compileOnly("org.checkerframework:compiler:2.1.9")
は削除します。
build.gradle を リンク先の最終形の内容 の内容に変更します。変更後、Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。
clean タスク → Rebuild Project → build タスクを実行すると “BUILD SUCCESSFUL” が出力されました。
最後に
Error Prone ですが、JDK 9 対応のためなのか 2.0.16 以降はどうも素直にバージョンアップできないようです。以外に癖のあるライブラリでした。。。
普通に Web アプリケーションを作るだけならば入れない方が無難かもしれません。自分はもうしばらく入れたままにして様子を見たいと思います。
ソースコード
build.gradle
■その1
buildscript { .......... dependencies { classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") classpath("io.spring.gradle:dependency-management-plugin:0.6.1.RELEASE") // for Error Prone ( http://errorprone.info/ ) classpath("net.ltgt.gradle:gradle-errorprone-plugin:0.0.9") // for Grgit classpath("org.ajoberstar:grgit:1.8.0") // Gradle Download Task classpath("de.undercouch:gradle-download-task:3.2.0") } } .......... ext { errorproneVersion = '2.0.18' } configurations { // for Doma 2 domaGenRuntime // for Error Prone ( http://errorprone.info/ ) errorprone { resolutionStrategy.force "com.google.errorprone:error_prone_core:${errorproneVersion}" } } .......... dependencies { .......... // dependency-management-plugin によりバージョン番号が自動で設定されないもの、あるいは最新バージョンを指定したいもの .......... compileOnly("com.google.errorprone:error_prone_annotations:${errorproneVersion}") .......... }
- buildscript の
classpath("net.ltgt.gradle:gradle-errorprone-plugin:0.0.8")
→classpath("net.ltgt.gradle:gradle-errorprone-plugin:0.0.9")
へ変更します。 ext { errorproneVersion = '2.0.18' }
を追加します。configurations と dependencies それぞれに Error Prone のバージョンを記述していましたが、1箇所修正すればよいようにします。- configurations の
resolutionStrategy.force 'com.google.errorprone:error_prone_core:2.0.15'
→resolutionStrategy.force "com.google.errorprone:error_prone_core:${errorproneVersion}"
へ変更します。 - dependencies の
compileOnly("com.google.errorprone:error_prone_annotations:2.0.15")
→compileOnly("com.google.errorprone:error_prone_annotations:${errorproneVersion}")
へ変更します。
■その2
dependencyManagement { imports { mavenBom("io.spring.platform:platform-bom:Athens-SR3") { bomProperty 'guava.version', '21.0' } } }
- mavenBom を () を付けて
io.spring.platform:platform-bom:Athens-SR3
を渡すように変更した後、{ bomProperty 'guava.version', '21.0' }
を追加します。
■その3
dependencies { .......... // dependency-management-plugin によりバージョン番号が自動で設定されないもの、あるいは最新バージョンを指定したいもの .......... compile("com.univocity:univocity-parsers:2.3.1") compileOnly("org.checkerframework:compiler:2.1.9") testCompile("org.dbunit:dbunit:2.5.3") ..........
compileOnly("org.checkerframework:compiler:2.1.9")
を追加します。
■最終形
buildscript { ext { springBootVersion = '1.4.4.RELEASE' } repositories { jcenter() maven { url "http://repo.spring.io/repo/" } maven { url "https://plugins.gradle.org/m2/" } } dependencies { classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") classpath("io.spring.gradle:dependency-management-plugin:0.6.1.RELEASE") // for Error Prone ( http://errorprone.info/ ) classpath("net.ltgt.gradle:gradle-errorprone-plugin:0.0.9") // for Grgit classpath("org.ajoberstar:grgit:1.8.0") // Gradle Download Task classpath("de.undercouch:gradle-download-task:3.2.0") } } apply plugin: 'java' apply plugin: 'eclipse' apply plugin: 'idea' apply plugin: 'org.springframework.boot' apply plugin: 'io.spring.dependency-management' apply plugin: 'de.undercouch.download' apply plugin: 'groovy' apply plugin: 'net.ltgt.errorprone' apply plugin: 'checkstyle' apply plugin: 'findbugs' sourceCompatibility = 1.8 targetCompatibility = 1.8 task wrapper(type: Wrapper) { gradleVersion = '2.13' } [compileJava, compileTestGroovy, compileTestJava]*.options*.compilerArgs = ['-Xlint:all,-options,-processing,-path'] // for Doma 2 // JavaクラスとSQLファイルの出力先ディレクトリを同じにする processResources.destinationDir = compileJava.destinationDir // コンパイルより前にSQLファイルを出力先ディレクトリにコピーするために依存関係を逆転する compileJava.dependsOn processResources jar { baseName = 'ksbysample-webapp-lending' version = '1.1.0-RELEASE' } idea { module { inheritOutputDirs = false outputDir = file("$buildDir/classes/main/") } } configurations { // for Doma 2 domaGenRuntime } checkstyle { configFile = file("${rootProject.projectDir}/config/checkstyle/google_checks.xml") toolVersion = '7.5.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 } } repositories { jcenter() } dependencyManagement { imports { mavenBom("io.spring.platform:platform-bom:Athens-SR3") { bomProperty 'commons-lang3.version', '3.5' bomProperty 'guava.version', '21.0' } } } bootRepackage { mainClass = 'ksbysample.webapp.lending.Application' excludeDevtools = true } dependencies { def jdbcDriver = "org.postgresql:postgresql:9.4.1212" def spockVersion = "1.1-groovy-2.4-rc-3" def domaVersion = "2.15.0" def lombokVersion = "1.16.12" def errorproneVersion = '2.0.15' // dependency-management-plugin によりバージョン番号が自動で設定されるもの // Appendix A. Dependency versions ( http://docs.spring.io/platform/docs/current/reference/htmlsingle/#appendix-dependency-versions ) 参照 compile("org.springframework.boot:spring-boot-starter-web") compile("org.springframework.boot:spring-boot-starter-thymeleaf") compile("org.thymeleaf.extras:thymeleaf-extras-springsecurity4") compile("org.thymeleaf.extras:thymeleaf-extras-java8time") compile("org.springframework.boot:spring-boot-starter-data-jpa") 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-data-redis") compile("org.springframework.boot:spring-boot-starter-amqp") compile("org.springframework.boot:spring-boot-devtools") compile("org.springframework.session:spring-session") compile("com.fasterxml.jackson.datatype:jackson-datatype-jsr310") compile("com.fasterxml.jackson.dataformat:jackson-dataformat-xml") compile("com.google.guava:guava") compile("org.apache.commons:commons-lang3") compile("org.codehaus.janino:janino") testCompile("org.springframework.boot:spring-boot-starter-test") testCompile("org.springframework.security:spring-security-test") testCompile("org.yaml:snakeyaml") // dependency-management-plugin によりバージョン番号が自動で設定されないもの、あるいは最新バージョンを指定したいもの runtime("${jdbcDriver}") compile("org.seasar.doma:doma:${domaVersion}") compile("org.bgee.log4jdbc-log4j2:log4jdbc-log4j2-jdbc4.1:1.16") compile("org.simpleframework:simple-xml:2.7.1") compile("com.univocity:univocity-parsers:2.3.1") testCompile("org.dbunit:dbunit:2.5.3") testCompile("com.icegreen:greenmail:1.5.3") testCompile("org.assertj:assertj-core:3.6.2") testCompile("com.jayway.jsonpath:json-path:2.2.0") testCompile("org.jmockit:jmockit:1.30") testCompile("org.spockframework:spock-core:${spockVersion}") { exclude module: "groovy-all" } testCompile("org.spockframework:spock-spring:${spockVersion}") { exclude module: "groovy-all" } testCompile("com.google.code.findbugs:jsr305:3.0.1") // for lombok compileOnly("org.projectlombok:lombok:${lombokVersion}") testCompileOnly("org.projectlombok:lombok:${lombokVersion}") // for Doma-Gen 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}") } ..........
履歴
2017/03/06
初版発行。
2017/03/07
* com.google.common.base.Preconditions#checkArgument に関する記述でメソッドがないのにあると書いていたので、見直して修正しました。
Spring Boot 1.3.x の Web アプリを 1.4.x へバージョンアップする ( その10 )( インジェクションの方法を @Autowired によるフィールドインジェクション → コンストラクタインジェクションへ変更する )
概要
記事一覧はこちらです。
Spring Boot 1.3.x の Web アプリを 1.4.x へバージョンアップする ( その9 )( 1.3系 → 1.4系で実装方法が変更された点を修正する ) の続きです。
- 今回の手順で確認できるのは以下の内容です。
- これまでは DI 対象のフィールドに @Autowired アノテーションを付加してインジェクションしていましたが、1.4 系からコンストラクタインジェクションが主流になったと聞いたので、コンストラクタインジェクションに変更します。
参照したサイト・書籍
Spring 4.3 DIコンテナ関連の主な変更点
http://qiita.com/kazuki43zoo/items/172d132ff8f4ba098888SpringでField InjectionよりConstructor Injectionが推奨される理由
http://pppurple.hatenablog.com/entry/2016/12/29/233141- なぜコンストラクタインジェクションがよいのか、についてはこちらの記事が分かりやすかったです。
Does JUnit4 testclasses require a public no arg constructor?
http://stackoverflow.com/questions/1451496/does-junit4-testclasses-require-a-public-no-arg-constructor
目次
- コンストラクタインジェクションって面倒なのでは? と思ったが IntelliJ IDEA がきちんと補完してくれる!
- コンストラクタインジェクションに変更する
- clean タスク → Rebuild Project → build タスクを実行してみる
- Mail001Helper, Mail002Helper, Mail003Helper の3クラス全てに同じような修正をしたのに Mail003Helper だけ checkstyle のチェックで警告が出る理由は?
- 次回は。。。
手順
コンストラクタインジェクションって面倒なのでは? と思ったが IntelliJ IDEA がきちんと補完してくれる!
コンストラクタインジェクションが主流になったと聞いた時は、フィールドに @Autowired アノテーションを付けるだけと比較して、わざわざコンストラクタに引数を追加して、かつコンストラクタ内でフィールドにセットするのは書くのが面倒そうだなと思ったのですが、さすがは IntelliJ IDEA、サポートは万全でした。
例えばクラス内に final が付いたフィールドを記述すると、以下の画像のようにフィールドの下に赤波線が表示されます。
赤波線が表示されたフィールドにカーソルを移動して Alt+Enter を押すとコンテキストメニューが表示されます。
「Add constructor parameter」を選択するとコンストラクタが自動生成されて、コンストラクタインジェクションの処理が記述されます。
この補完はフィールドの出現順通りにコンストラクタの引数にセットしてくれます。例えば先程記述したフィールドの上に別のフィールドを記述して、
Alt+Enter を押して「Add constructor parameter」を選択すると、
コンストラクタの引数も先程記述された引数の前に追加されます。コンストラクタ内の記述もフィールドと同じ順に記述されます。
Javadoc も自動的に補完してくれます。コンストラクタに Javadoc を記述した後、記述済のフィールドの間に別のフィールドを記述します。
Alt+Enter を押して「Add constructor parameter」を選択すると、
引数やコンストラクタ内の記述の位置を中間にしてくれるだけでなく、Javadoc の記述の位置も中間になります。
また最初にフィールドだけ全て書いて、後からコンストラクタを生成することもできます。まずフィールドだけ書きます。
赤波線にカーソルを移動してから Alt+Enter を押します。コンテキストメニューが表示されますので、「Add constructor parameters」を選択します。複数だと parameter ではなく parameters になるとは。。。
「Choose Fields to Generate Constructor Parameters for」ダイアログが表示されます。デフォルトではフィールドは1つしか選択されていませんので、全て選択してから「OK」ボタンをクリックします。
コンストラクタが生成されます。
Lombok の @RequiredArgsConstructor アノテーションを付ければ final のフィールドだけを引数に持つコンストラクタを自動生成してくれるのでコンストラクタ自体を省略することが可能ですが、自分はソースを見たときの分かりやすさや、インジェクション対象のフィールドが多い時にはそのことに気付いた方がよい(依存関係が多くクラスの作りを見直すべき)という点を考慮すると、コンストラクタは書いた方が良さそうな気がします。
尚、削除する時はフィールドを削除して何かキーを押せばコンストラクタの方も自動で削除してくれるということはありませんでした。1つずつ削除しましょう。
コンストラクタインジェクションに変更する
以下の方法・方針で変更します。
- 変更は以下の手順で行います。
- @Autowired アノテーションを削除する。
- フィールドの宣言に final を追加する。
- コンストラクタインジェクションを記述する。
- @Autowired によるフィールドインジェクションの数が多いクラスがあってもコンストラクタインジェクションに変更するだけで、クラスの実装は見直しません。
- テストクラスは @Autowired によるフィールドインジェクションのままとします。JUnit4 のテストクラスには引数なしのコンストラクタが必ず必要らしいからです ( 実際にコンストラクタインジェクションに修正したら引数なしのコンストラクタがないというエラーメッセージが出ました )。
以下のソースを修正しました。
- src/main/java/ksbysample/webapp/lending/config/ApplicationConfig.java
- src/main/java/ksbysample/webapp/lending/config/WebSecurityConfig.java
- src/main/java/ksbysample/webapp/lending/helper/library/LibraryHelper.java
- src/main/java/ksbysample/webapp/lending/helper/mail/EmailHelper.java
- src/main/java/ksbysample/webapp/lending/helper/mail/Mail001Helper.java
- src/main/java/ksbysample/webapp/lending/helper/mail/Mail002Helper.java
- src/main/java/ksbysample/webapp/lending/helper/mail/Mail003Helper.java
- src/main/java/ksbysample/webapp/lending/helper/message/MessagesPropertiesHelper.java
- src/main/java/ksbysample/webapp/lending/helper/user/UserHelper.java
- src/main/java/ksbysample/webapp/lending/listener/rabbitmq/InquiringStatusOfBookQueueListener.java
- src/main/java/ksbysample/webapp/lending/security/AuthenticationFailureBadCredentialsEventListener.java
- src/main/java/ksbysample/webapp/lending/security/AuthenticationSuccessEventListener.java
- src/main/java/ksbysample/webapp/lending/security/LendingUserDetailsService.java
- src/main/java/ksbysample/webapp/lending/service/file/BooklistCsvFileService.java
- src/main/java/ksbysample/webapp/lending/service/queue/InquiringStatusOfBookQueueService.java
- src/main/java/ksbysample/webapp/lending/service/UserInfoService.java
- src/main/java/ksbysample/webapp/lending/web/admin/library/AdminLibraryController.java
- src/main/java/ksbysample/webapp/lending/web/admin/library/AdminLibraryService.java
- src/main/java/ksbysample/webapp/lending/web/booklist/BooklistController.java
- src/main/java/ksbysample/webapp/lending/web/booklist/BooklistService.java
- src/main/java/ksbysample/webapp/lending/web/booklist/UploadBooklistFormValidator.java
- src/main/java/ksbysample/webapp/lending/web/confirmresult/ConfirmresultController.java
- src/main/java/ksbysample/webapp/lending/web/confirmresult/ConfirmresultService.java
- src/main/java/ksbysample/webapp/lending/web/lendingapp/LendingappController.java
- src/main/java/ksbysample/webapp/lending/web/lendingapp/LendingappService.java
- src/main/java/ksbysample/webapp/lending/web/lendingapproval/LendingapprovalController.java
- src/main/java/ksbysample/webapp/lending/web/lendingapproval/LendingapprovalService.java
- src/main/java/ksbysample/webapp/lending/web/springmvcmemo/BeanValidationGroupController.java
- src/main/java/ksbysample/webapp/lending/web/LoginController.java
- src/main/java/ksbysample/webapp/lending/web/WebappErrorController.java
- src/main/java/ksbysample/webapp/lending/webapi/library/LibraryController.java
- src/main/java/ksbysample/webapp/lending/webapi/weather/WeatherController.java
clean タスク → Rebuild Project → build タスクを実行してみる
よく考えたら前回 clean タスク → Rebuild Project → build タスクを実行していませんでしたね。。。と思いつつ、実行してみます。
Error Prone の 2.0.18 がダウンロードされています。
その後で checkstyle の JavadocMethod の警告が出ています。追加したコンストラクタに Javadoc のコメントを付けていないからでしょう。でも警告が出力されたクラスを見ると Mail001Helper, Mail002Helper, Mail003Helper の3クラスに同じような修正をしたにも関わらず Mail003Helper しか出ていないのが気になりました。
あとテストが1つ失敗して “BUILD FAILED” の文字が出力されました。
Mail001Helper, Mail002Helper, Mail003Helper の3クラス全てに同じような修正をしたのに Mail003Helper だけ checkstyle のチェックで警告が出る理由は?
追加したコンストラクタを見ると、Mail001Helper クラスは、
public Mail001Helper(FreeMarkerUtils freeMarkerUtils , JavaMailSender mailSender) { this.freeMarkerUtils = freeMarkerUtils; this.mailSender = mailSender; }
Mail003Helper クラスは、
public Mail003Helper(FreeMarkerUtils freeMarkerUtils , JavaMailSender mailSender , ValuesHelper vh) { this.freeMarkerUtils = freeMarkerUtils; this.mailSender = mailSender; this.vh = vh; }
google_checks.xml の JavadocMethod の設定を見ると以下のように定義されていました。
<module name="JavadocMethod"> <property name="scope" value="public"/> <property name="allowMissingParamTags" value="true"/> <property name="allowMissingThrowsTags" value="true"/> <property name="allowMissingReturnTag" value="true"/> <property name="minLineCount" value="2"/> <property name="allowedAnnotations" value="Override, Test"/> <property name="allowThrowsTagsForSubclasses" value="true"/> </module>
おそらく原因は <property name="minLineCount" value="2"/>
ですね。コンストラクタ内の行数により警告を出すか否かを判断しているようです。
<property name="minLineCount" value="3"/>
に変更すると Mail003Helper クラスも警告されなくなりました。
<property name="minLineCount" value="1"/>
に変更すると Mail001Helper, Mail002Helper, Mail003Helper の全てのクラスが警告されます。
さて、どうするかですが、
- public, protected, private 問わず、メソッドには全てコメントは欲しい気がする。
- Google Java Style Guide を見てみると、7.3 Where Javadoc is used に “At the minimum, Javadoc is present for every public class” と記載されていた。google_checks.xml に
<property name="scope" value="public"/>
の定義が書かれているのは、このためだろう。これは Google Java Style Guide 通りでいいかな、と思った。 - setter, getter のようなメソッド内に通常1行しか書かないものを対象外にしたいので
<property name="minLineCount" value="2"/>
のようなルールを設定しているような気がするが、自分は lombok を使うのでこの点は気にしなくていいはず。 - そういえば Doma-Gen で自動生成した Entity クラスは lombok を使用せず独自に setter, getter を記述していたはずと思ったが、自動生成された Entity クラスのソースを見ると全てコメントが入っていた。
<property name="minLineCount" value="2"/>
のルールのままにしておけば、依存関係が多くないコストラクタインジェクションが目的のコンストラクタは Javadoc がなくてもよくなるのか。。。 その方がいいかな、という気がする。
などと考えてみましたが、やっぱり public は全てコメントを付けるルールでいいかなと思ったので <property name="minLineCount" value="2"/>
の設定は削除します。
google_checks.xml から <property name="minLineCount" value="2"/>
の行を削除した後、警告が出ているメソッドに Javadoc を記述します。
次回は。。。
テストクラスのアノテーションの変更の前に以下2つの作業を行います。
- Error Prone を 2.0.15 → 2.0.18 へバージョンアップします。
- clean タスク → Rebuild Project → build タスク実行時に失敗したテストを見直します。
ソースコード
履歴
2017/03/05
初版発行。
Spring Boot + Spring Integration でいろいろ試してみる ( その18 )( @MessagingGateway でメソッド呼び出しのインターフェースで MessageChannel へ Message を送信する2 )
概要
記事一覧はこちらです。
- Spring Boot + Spring Integration でいろいろ試してみる ( その17 )( @MessagingGateway でメソッド呼び出しのインターフェースで MessageChannel へ Message を送信する ) の続きです。
- 以下の内容を記載します。
- FTP アップロード処理へのリトライ処理の追加
- ErrorChannel の処理の実装
- 動作確認
参照したサイト・書籍
目次
手順
FTP アップロード処理にリトライ処理を追加する
前回作成した FTP サーバにアップロードする処理にリトライ処理を入れるのを忘れていたので、追加します。
- src/main/java/ksbysample/eipapp/messaginggateway の下の FlowConfig.java を リンク先のその1の内容 に変更します。
ErrorChannel の処理を作成する
Flow の処理中に例外が throw されると “errorChannel” という名前の MessageChannel に throw された例外がセットされた Message が送信されます。
“errorChannel” の MessageChannel は Spring Integration が自動的に生成します。生成しているのは org.springframework.integration.config.DefaultConfiguringBeanFactoryPostProcessor #postProcessBeanFactory です。
まず throw された例外の stacktrace をログに出力されるのと同じ適宜インデントされたフォーマットの文字列で取得したいので、Guava の Throwables#getStackTraceAsString を使用できるようにします。build.gradle を リンク先の文字列 に変更します。
変更後、Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。
src/main/java/ksbysample/eipapp/messaginggateway の下の FlowConfig.java を リンク先のその2の内容 に変更します。
動作確認
事前準備
まずは clean タスク → Rebuild Project → build タスクを実行して正常終了することを確認します。
次に Xlight FTP Server, FreeFTPd, smtp4dev, zipkin を起動します。zipkin は最新の zipkin-server-1.20.1-exec.jar をダウンロードしています。
起動していて vagrant か docker で必要なサーバが起動する仮想サーバを作った方がいいな、と思いました。そのうち気が向いたらやってみます。
ログが見にくくなるので JavaMail の debug 機能を一旦 OFF にします。src/main/java/ksbysample/eipapp/messaginggateway の下の MailHelperConfig.java の sendMailFlow メソッドを以下のように変更します。
@Bean public IntegrationFlow sendMailFlow() { return f -> f .handle(Mail.outboundAdapter(this.mailHost) .port(this.mailPort) .protocol(this.mailProtocol) .defaultEncoding(this.mailDefaultEncoding) .javaMailProperties(p -> p.put("mail.debug", "false"))); }
.javaMailProperties(p -> p.put("mail.debug", "true")));
→.javaMailProperties(p -> p.put("mail.debug", "false")));
に変更します。
bootRun を実行して ksbysample-eipapp-messaginggateway を起動します。
1ファイルを SFTP サーバの /in ディレクトリに置いてみる
まずは testdata01.txt を SFTP サーバの send01 ユーザの /in ディレクトリに置いてみます。ファイルの文字コードは UTF-8、改行コードは CRLF で以下の内容です。
これはテストです。 今日は晴れています。
SFTP サーバからファイルがダウンロードされて処理が行われ、FTP サーバの recv01 ユーザの /out ディレクトリにファイルがアップロードされました。
アップロードされた testdata01.txt の内容は以下のようになっていました。文字コードは UTF-8、改行コードは CRLF です。
To: download@test.co.jp Subject: testdata01.txt これはテストです。 今日は晴れています。
IntelliJ IDEA のコンソールに出力されたログは以下のようになっており、特にエラーは出ていません。
smtp4dev に送信されたメールは以下の内容でした。こちらも特に問題ありませんでした。
Zipkin で処理状況を見ると以下のようになっていました。
2ファイルを SFTP サーバの /in ディレクトリに置いてみる
次は testdata01.txt, testdata02.txt の2ファイルを SFTP サーバの send01 ユーザの /in ディレクトリに置いてみます。testdata02.txt の内容は以下のものです。
緊急警報です。 明日は、 雨ですね。
2ファイルとも SFTP サーバからファイルがダウンロードされて処理が行われ、FTP サーバの recv01 ユーザの /out ディレクトリにファイルがアップロードされました。
アップロードされた testdata01.txt, testdata02.txt の内容は以下のようになっていました。
■testdata01.txt
To: download@test.co.jp Subject: testdata01.txt これはテストです。 今日は晴れています。
■testdata02.txt
To: download@test.co.jp Subject: testdata02.txt 緊急警報です。 明日は、 雨ですね。
IntelliJ IDEA のコンソールに出力されたログは以下のようになっており、特にエラーは出ていません。traceId は1ファイル毎に 1 ID 発行されています。
smtp4dev に送信されたメールは以下の内容でした。こちらも特に問題ありませんでした。
FTP サーバを停止して errorChannel に Message が送信されるようにしてみる
Xlight FTP Server を停止します。
testdata01.txt を SFTP サーバの send01 ユーザの /in ディレクトリに置きます。
今度は FTP アップロードできなかったため、C:\eipapp\ksbysample-eipapp-messaginggateway\send ディレクトリにファイルが残ったままになりました。
IntelliJ IDEA のコンソールに出力されたログは以下のようになっていました。例外が throw されています。
smtp4dev に送信されたメールは以下の内容でした。今回は「エラーが発生しました」メールが送信されており、送信されたメールの本文を確認すると例外の内容が出力されています。
想定通りの動作になっていたので、動作確認を終わります。
メモ書き
.handle(Mail.outboundAdapter(...).~)
と .handleWithAdapter(a -> a.mail(...).~)
は同じです&使い方の説明や感想など
src/main/java/ksbysample/eipapp/messaginggateway の MailHelperConfig.java の中で以下のように記述していますが、
@Bean public IntegrationFlow sendMailFlow() { return f -> f .handle(Mail.outboundAdapter(this.mailHost) .port(this.mailPort) .protocol(this.mailProtocol) .defaultEncoding(this.mailDefaultEncoding) .javaMailProperties(p -> p.put("mail.debug", "false"))); }
これは以下と同じです。
@Bean public IntegrationFlow sendMailFlow() { return f -> f .handleWithAdapter(a -> a.mail(this.mailHost) .port(this.mailPort) .protocol(this.mailProtocol) .defaultEncoding(this.mailDefaultEncoding) .javaMailProperties(p -> p.put("mail.debug", "false"))); }
Mail 以外には Files, Ftp 等があり、以下の Web ページに説明があります。
Spring Integration Java DSL Reference - Using Protocol Adapters https://github.com/spring-projects/spring-integration-java-dsl/wiki/spring-integration-java-dsl-reference#using-protocol-adapters
.handleWithAdapter(a -> a.~)
の “a” は “adapters” の略です。.handleWithAdapter(a -> a.~)
の書き方の方が IDE の補完により使える Adapter が一覧表示されるので使いやすいと思います。
使い方ですが、Message に特定の header をセットして送信すると、その header と payload のデータを利用してメール送信したりファイル出力したりしてくれます。
例えば .handle(Mail.outboundAdapter(...).~)
あるいは .handleWithAdapter(a -> a.mail(...).~)
の場合ですが、
- メール本文は payload にセットする。
- From は header に “mail_from” をキーにしてセットする。
- To は header に “mail_to” をキーにしてセットする。
- Subject は header に “mail_subject” をキーにしてセットする。
- “mail_from”, “mail_to”, “mail_subject” 等は org.springframework.integration.mail の MailHeaders クラスに定義されている。通常は MailHeaders.FROM, MailHeaders.TO, MailHeaders.SUBJECT を使用する。
という Message を送信すると header にセットされた From, To, Subject で payload の内容でメール送信します。
どのような header が使用できるのかは、
Spring Integration Reference Manual
http://docs.spring.io/spring-integration/reference/html/
の中の「V. Integration Endpoints」の下にある 21. Mail Support のような「~ Adapters」「~ Support」というタイトルのページの中に記述があります(たぶん)。
また .handleWithAdapter(a -> a.file(...).~)
と .handleWithAdapter(a -> a.fileGateway(...).~)
のように “Gateway” という文字列が付くものと付かないものが表示される Adapter がありますが、これはその次の処理に Message を送信するか否かで分けます。"Gateway" は次に Message を送信しますが、付かないものは送信しません ( outboundAdapter なので次に Message は送信されません )。
付かないものは .handle(...)
の中で return null;
を返しているのと同じです。
.handle((p, h) -> { .....(ここに処理を記述する)..... return null; })
Adapter 関連は使いこなせれば便利なのかもしれませんが、まだ以下のような感想を抱いていて使いこなせていない感があります。もう少しいろいろ強制的に使ってみて慣れないとダメかな。
- outboundAdapter だとそこで処理が止まってしまうが、その後に処理は続けたい場合にどうしたらよいのか分かりづらい。
- outboundGateway にすればよいのかもしれないが、outboundAdapter の処理だけやって次に処理を流してくれればよいだけなのに outboundGateway だと追加で指定をしないといけなかったりして何か使いずらい印象がある。gateway の考え方にまだ慣れていないだけなのか?
.handle(Mail.outboundAdapter(...).~)
のメール送信処理は spring.mail.~
の設定が反映されない。。。と思って調べたら MailSendingMessageHandler って何?
今回メール送信用の共通処理では以下のように実装していますが、これは Spring Integration Java DSL Reference のサンプルを見て書いています。
.handle(Mail.outboundAdapter(this.mailHost) .port(this.mailPort) .protocol(this.mailProtocol) .defaultEncoding(this.mailDefaultEncoding) .javaMailProperties(p -> p.put("mail.debug", "false")));
Spring Boot を使っていて、かつ compile("org.springframework.boot:spring-boot-starter-mail")
を入れているのだから、自動生成されている javaMailSender Bean が利用されていて application.properties の spring.mail.~ の設定が反映されないのかな? と思い、
- 上の
.defaultEncoding(this.mailDefaultEncoding)
の部分を削除する。 - application.properties に
spring.mail.default-encoding=UTF-8
を追加する。
としてみたのですが全く設定が反映されません。ソースを追ってみると、org.springframework.integration.dsl.mail.MailSendingMessageHandlerSpec の中で private final JavaMailSenderImpl sender = new JavaMailSenderImpl();
と実装されていて javaMailSender Bean は利用されていませんでした。
MailSendingMessageHandlerSpec クラスは MessageHandlerSpec<MailSendingMessageHandlerSpec, MailSendingMessageHandler>
を継承していて、MailSendingMessageHandler クラスを見るとこちらはフィールドに private final MailSender mailSender;
と書かれていて、かつコンストラクタに渡された MailSender インターフェースの実装クラスのインスタンスをセットしています。MailSendingMessageHandler クラスは javaMailSender Bean を DI しているんじゃ。。。と思いましたが、よく見たら MailSendingMessageHandler クラスには @Component アノテーションは付いていませんでした。こちらも javaMailSender Bean は使っていませんね。
なんか不便だな。。。 と思いましたが、javaMailSender Bean をコンストラクタに渡して MailSendingMessageHandler Bean を作成すればよいのでは?と思い、以下のテストコードを作成して動かしてみるとメールを送信することができました。
package ksbysample.eipapp.messaginggateway; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.integration.config.EnableIntegration; import org.springframework.integration.dsl.IntegrationFlow; import org.springframework.integration.mail.MailSendingMessageHandler; import org.springframework.integration.support.MessageBuilder; import org.springframework.mail.javamail.JavaMailSender; import org.springframework.mail.javamail.MimeMessageHelper; import org.springframework.messaging.MessageChannel; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.junit4.SpringRunner; import javax.mail.internet.MimeMessage; @ContextConfiguration @RunWith(SpringRunner.class) @DirtiesContext @TestPropertySource(properties = { "spring.mail.host=localhost" , "spring.mail.default-encoding=UTF-8" }) public class MailHelperConfigTest { @Autowired private JavaMailSender mailSender; @Autowired @Qualifier("testFlow.input") private MessageChannel testFlowInput; @Test public void sendMailFlow() throws Exception { MimeMessage mimeMessage = this.mailSender.createMimeMessage(); MimeMessageHelper message = new MimeMessageHelper(mimeMessage, false, "UTF-8"); message.setFrom("from@test.co.jp"); message.setTo("to@sample.com"); message.setSubject("これはテストです"); message.setText("本文です。\r\n改行してみます。"); this.testFlowInput.send(MessageBuilder.withPayload(message.getMimeMessage()).build()); } @Configuration @EnableIntegration @ComponentScan public static class ContextConfiguration { @Autowired private JavaMailSender mailSender; @Bean public MailSendingMessageHandler mailSendingMessageHandler() { return new MailSendingMessageHandler(this.mailSender); } @Bean public IntegrationFlow testFlow() { return f -> f .handle(mailSendingMessageHandler()); } } }
メール送信には Mail.outboundAdapter(...)
だけでなく MailSendingMessageHandler クラスが使えますね。発見です。このクラスだと SimpleMailMessage や MimeMailMessage クラスのインスタンスを payload にセットして Message を送信すればメール送信してくれるので添付ファイルがあるメールでも送れそうです。でもこのクラスって Spring Integration Reference Manual には記載されていないので、Spring Integration のソースを見ていないとさすがに分かりません。
またおそらくこれと同じように “~MessageHandler” というクラス名の AbstractMessageHandler インターフェースの実装クラスが他にも存在して、たぶん知っているとかなり便利なのでは?という気がしてきました。いつか調べてみたいと思います。
ソースコード
FlowConfig.java
■その1
@Configuration public class FlowConfig { .......... /** * Message の payload にセットされた File オブジェクトが指し示すファイルを FTP サーバにアップロードする * * @return IntegrationFlow オブジェクト */ @Bean public IntegrationFlow ftpUploadFlow() { return f -> f .handleWithAdapter(a -> a.ftp(ftpSessionFactory()).remoteDirectory(PATH_FTP_UPLOAD_DIR) , e -> e.advice(ftpUploadRetryAdvice())); } .......... /** * リトライは最大5回 ( SimpleRetryPolicy で指定 )、 * リトライ間隔は初期値2秒、最大10秒、倍数2.0 ( ExponentialBackOffPolicy で指定 ) * の RequestHandlerRetryAdvice オブジェクトを生成する * * @return RequestHandlerRetryAdvice オブジェクト */ @Bean public Advice ftpUploadRetryAdvice() { RetryTemplate retryTemplate = new RetryTemplate(); retryTemplate.setRetryPolicy( new SimpleRetryPolicy(5, singletonMap(Exception.class, true))); ExponentialBackOffPolicy backOffPolicy = new ExponentialBackOffPolicy(); backOffPolicy.setInitialInterval(2000); backOffPolicy.setMaxInterval(10000); backOffPolicy.setMultiplier(2.0); retryTemplate.setBackOffPolicy(backOffPolicy); RequestHandlerRetryAdvice advice = new RequestHandlerRetryAdvice(); advice.setRetryTemplate(retryTemplate); return advice; } }
- ftpUploadRetryAdvice メソッドを追加します。
- ftpUploadFlow メソッドで
, e -> e.advice(ftpUploadRetryAdvice())
を追加します。
■その2
@Configuration public class FlowConfig { .......... private static final String ERRORMAIL_FROM = "system@sample.com"; private static final String ERRORMAIL_TO = "alert@test.co.jp"; private static final String ERRORMAIL_SUBJECT = "エラーが発生しました"; .......... /** * errorChannel に送信された Message からエラーメッセージを取得してメールする * * @return IntegrationFlow オブジェクト */ @Bean public IntegrationFlow errorChannelFlow() { return IntegrationFlows.from("errorChannel") .<Exception>handle((p, h) -> { String stacktrace = Throwables.getStackTraceAsString(p); this.mailHelper.send(stacktrace , new MapBuilder<>() .put(MailHeaders.FROM, ERRORMAIL_FROM) .put(MailHeaders.TO, ERRORMAIL_TO) .put(MailHeaders.SUBJECT, ERRORMAIL_SUBJECT) .get()); return null; }) .get(); } }
- errorChannelFlow メソッドを追加します。
■完成形
package ksbysample.eipapp.messaginggateway; import com.google.common.base.Throwables; import com.jcraft.jsch.ChannelSftp; import lombok.extern.slf4j.Slf4j; import org.aopalliance.aop.Advice; import org.apache.commons.net.ftp.FTPFile; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.integration.dsl.IntegrationFlow; import org.springframework.integration.dsl.IntegrationFlows; import org.springframework.integration.dsl.core.Pollers; import org.springframework.integration.dsl.support.MapBuilder; import org.springframework.integration.dsl.support.Transformers; import org.springframework.integration.file.FileHeaders; import org.springframework.integration.file.filters.AcceptAllFileListFilter; import org.springframework.integration.file.filters.IgnoreHiddenFileListFilter; 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.advice.ExpressionEvaluatingRequestHandlerAdvice; import org.springframework.integration.handler.advice.RequestHandlerRetryAdvice; import org.springframework.integration.mail.MailHeaders; import org.springframework.integration.sftp.session.DefaultSftpSessionFactory; import org.springframework.integration.support.MessageBuilder; import org.springframework.retry.backoff.ExponentialBackOffPolicy; import org.springframework.retry.policy.SimpleRetryPolicy; import org.springframework.retry.support.RetryTemplate; import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Paths; import static java.util.Collections.singletonMap; @Slf4j @Configuration public class FlowConfig { private static final String PATH_SFTP_DOWNLOAD_DIR = "/in"; private static final String PATH_LOCAL_DOWNLOAD_DIR = "C:/eipapp/ksbysample-eipapp-messaginggateway/recv"; private static final String PATH_LOCAL_UPLOAD_DIR = "C:/eipapp/ksbysample-eipapp-messaginggateway/send"; private static final String PATH_FTP_UPLOAD_DIR = "/out"; private static final String DOWNLOADFILEMAIL_FROM = "system@sample.com"; private static final String DOWNLOADFILEMAIL_TO = "download@test.co.jp"; private static final String ERRORMAIL_FROM = "system@sample.com"; private static final String ERRORMAIL_TO = "alert@test.co.jp"; private static final String ERRORMAIL_SUBJECT = "エラーが発生しました"; private static final String CRLF = "\r\n"; private final MailHelperConfig.MailHelper mailHelper; public FlowConfig(MailHelperConfig.MailHelper mailHelper) { this.mailHelper = mailHelper; } /** * SFTP サーバに接続するための SessionFactory オブジェクトを生成する * * @return SFTP サーバ接続用の SessionFactory オブジェクト */ @Bean public SessionFactory<ChannelSftp.LsEntry> sftpSessionFactory() { DefaultSftpSessionFactory factory = new DefaultSftpSessionFactory(true); factory.setHost("localhost"); factory.setPort(22); factory.setUser("send01"); factory.setPassword("send01"); factory.setAllowUnknownKeys(true); return new CachingSessionFactory<>(factory); } /** * FTP サーバに接続するための SessionFactory オブジェクトを生成する * * @return FTP サーバ接続用の SessionFactory オブジェクト */ @Bean public SessionFactory<FTPFile> ftpSessionFactory() { DefaultFtpSessionFactory factory = new DefaultFtpSessionFactory(); factory.setHost("localhost"); factory.setPort(21); factory.setUsername("recv01"); factory.setPassword("recv01"); return new CachingSessionFactory<>(factory); } /** * SFTP サーバにあるファイルをダウンロードした後、ファイルの内容をメールで送信して、 * 送信したメールの内容をファイルに出力して FTP サーバにアップロードする * * @return IntegrationFlow オブジェクト */ @Bean public IntegrationFlow sftpToMailToFtpFlow() { return IntegrationFlows // SFTP サーバの /in ディレクトリにファイルがあるか 5秒間隔でチェックする .from(s -> s.sftp(sftpSessionFactory()) .preserveTimestamp(true) .deleteRemoteFiles(true) .remoteDirectory(PATH_SFTP_DOWNLOAD_DIR) .localDirectory(new File(PATH_LOCAL_DOWNLOAD_DIR)) .localFilter(new AcceptAllFileListFilter<>()) .localFilter(new IgnoreHiddenFileListFilter()) , e -> e.poller(Pollers.fixedDelay(5000) .maxMessagesPerPoll(100))) .log() // ファイル名とファイルの絶対パスを Message の header にセットする .enrichHeaders(h -> h .headerExpression(FileHeaders.FILENAME, "payload.name") .headerExpression(FileHeaders.ORIGINAL_FILE, "payload.absolutePath")) // File の内容を読み込んで payload へセットする .transform(Transformers.fileToString()) // ファイルの内容をメール本文とするメールを送信する .<String>handle((p, h) -> { this.mailHelper.send(p , new MapBuilder<>() .put(MailHeaders.FROM, DOWNLOADFILEMAIL_FROM) .put(MailHeaders.TO, DOWNLOADFILEMAIL_TO) .put(MailHeaders.SUBJECT, h.get(FileHeaders.FILENAME)) .get()); return MessageBuilder.withPayload(p) .setHeader(MailHeaders.TO, DOWNLOADFILEMAIL_TO) .setHeader(MailHeaders.SUBJECT, h.get(FileHeaders.FILENAME)) .build(); }) .log() // メール送信した内容を payload にセットする .<String>handle((p, h) -> { StringBuilder sb = new StringBuilder(); sb.append("To: " + h.get(MailHeaders.TO) + CRLF); sb.append("Subject: " + h.get(MailHeaders.SUBJECT) + CRLF); sb.append(CRLF); sb.append(p); return MessageBuilder.withPayload(sb.toString()) .build(); }) // /send ディレクトリの下に payload の内容を出力したファイルを生成する .handleWithAdapter(a -> a.fileGateway(new File(PATH_LOCAL_UPLOAD_DIR))) .log() // /recv ディレクトリの下のファイルを削除し、payload には /send ディレクトリの下の // ファイルを指す File オブジェクトをセットする .handle((p, h) -> { try { Files.delete(Paths.get((String) h.get(FileHeaders.ORIGINAL_FILE))); } catch (IOException e) { throw new RuntimeException(e); } return MessageBuilder .withPayload(Paths.get(PATH_LOCAL_UPLOAD_DIR , (String) h.get(FileHeaders.FILENAME)).toFile()) .build(); }) // /send ディレクトリの下に作成したファイルを FTP サーバにアップロードする .bridge(e -> e.advice(ftpUploadAdvice())) .log() // /send ディレクトリの下に作成したファイルを削除する .<File>handle((p, h) -> { try { Files.delete(Paths.get(p.getAbsolutePath())); } catch (IOException e) { throw new RuntimeException(e); } return null; }) .log() .get(); } /** * Message の payload にセットされた File オブジェクトが指し示すファイルを FTP サーバにアップロードする * * @return IntegrationFlow オブジェクト */ @Bean public IntegrationFlow ftpUploadFlow() { return f -> f .handleWithAdapter(a -> a.ftp(ftpSessionFactory()).remoteDirectory(PATH_FTP_UPLOAD_DIR) , e -> e.advice(ftpUploadRetryAdvice())); } /** * ftpUploadFlow へ Message を送信する ExpressionEvaluatingRequestHandlerAdvice Bean * * @return ExpressionEvaluatingRequestHandlerAdvice オブジェクト */ @Bean public Advice ftpUploadAdvice() { ExpressionEvaluatingRequestHandlerAdvice advice = new ExpressionEvaluatingRequestHandlerAdvice(); advice.setOnSuccessExpressionString("payload"); advice.setSuccessChannelName("ftpUploadFlow.input"); return advice; } /** * リトライは最大5回 ( SimpleRetryPolicy で指定 )、 * リトライ間隔は初期値2秒、最大10秒、倍数2.0 ( ExponentialBackOffPolicy で指定 ) * の RequestHandlerRetryAdvice オブジェクトを生成する * * @return RequestHandlerRetryAdvice オブジェクト */ @Bean public Advice ftpUploadRetryAdvice() { RetryTemplate retryTemplate = new RetryTemplate(); retryTemplate.setRetryPolicy( new SimpleRetryPolicy(5, singletonMap(Exception.class, true))); ExponentialBackOffPolicy backOffPolicy = new ExponentialBackOffPolicy(); backOffPolicy.setInitialInterval(2000); backOffPolicy.setMaxInterval(10000); backOffPolicy.setMultiplier(2.0); retryTemplate.setBackOffPolicy(backOffPolicy); RequestHandlerRetryAdvice advice = new RequestHandlerRetryAdvice(); advice.setRetryTemplate(retryTemplate); return advice; } /** * errorChannel に送信された Message からエラーメッセージを取得してメールする * * @return IntegrationFlow オブジェクト */ @Bean public IntegrationFlow errorChannelFlow() { return IntegrationFlows.from("errorChannel") .<Exception>handle((p, h) -> { String stacktrace = Throwables.getStackTraceAsString(p); this.mailHelper.send(stacktrace , new MapBuilder<>() .put(MailHeaders.FROM, ERRORMAIL_FROM) .put(MailHeaders.TO, ERRORMAIL_TO) .put(MailHeaders.SUBJECT, ERRORMAIL_SUBJECT) .get()); return null; }) .get(); } }
build.gradle
dependencies { .......... // dependency-management-plugin によりバージョン番号が自動で設定されないもの、あるいは最新バージョンを指定したいもの compile("org.springframework.integration:spring-integration-java-dsl:1.2.1.RELEASE") compile("org.projectlombok:lombok:1.16.14") compile("com.google.guava:guava:21.0") testCompile("org.assertj:assertj-core:3.6.2") }
- dependencies に
compile("com.google.guava:guava:21.0")
を追加します。
履歴
2017/02/26
初版発行。
Spring Boot + Spring Integration でいろいろ試してみる ( その17 )( @MessagingGateway でメソッド呼び出しのインターフェースで MessageChannel へ Message を送信する )
概要
記事一覧はこちらです。
- Spring Integration DSL のサンプルを作成します。今回は長くなったため2回に分けています。
- 以下の処理を行う常駐型アプリケーションを作成します。
- SFTPサーバに send01 ユーザでログインし /in ディレクトリにファイルがあるかチェックします。ファイルがあれば C:\eipapp\ksbysample-eipapp-messaginggateway\recv ディレクトリにダウンロードします。
- ダウンロードしたファイルの内容をメール本文とするメールを送信します。From は system@sample.com、To は download@test.co.jp、Subject はファイル名にします。
- メールした時の To, Subject, メール本文を出力したファイルを C:\eipapp\ksbysample-eipapp-messaginggateway\send ディレクトリに生成して FTP サーバへアップロードします。FTP サーバには recv01 ユーザでログインし /out ディレクトリにアップロードします。
- 今回は ErrorChannel の処理を実装します。ErrorChannel に送信されたエラーメッセージをメール本文とするメールを送信します。From は system@sample.com、To は alert@test.co.jp、Subject は “エラーが発生しました” にします。
- メール送信処理は共通の IntegrationFlow として作成します。
- FTP サーバは Xlight ftp server を、SFTP サーバは FreeFTPd を使用します。今回この記事を書いている途中で Xlight ftp server の試用期間が終了したのですが、「Personal edition is free for personal use.」と書かれていたので問題なく使えるものと思っていたら SFTP サーバは使用できなくなりました。。。 ( https://www.xlightftpd.com/purchase.htm に記載されていました )
- SMTPサーバは smtp4dev を使用します。
参照したサイト・書籍
Spring Integration Reference Manual - 8.4 Messaging Gateways
http://docs.spring.io/spring-integration/reference/html/messaging-endpoints-chapter.html#gatewayHow to access custom headers in Spring Integration after receiving http error
http://stackoverflow.com/questions/38482418/how-to-access-custom-headers-in-spring-integration-after-receiving-http-error- タイトルとは全然関係なくて、
.transform(...)
の中で header にアクセスする方法を調べた時に参考にしました。
- タイトルとは全然関係なくて、
目次
- @MessagingGateway とは?
- recv, send ディレクトリを作成する
- ksbysample-eipapp-messaginggateway プロジェクトを作成する
- メール送信用の共通処理を作成する
- SFTP サーバからファイルをダウンロードする処理を作成する
- ファイルの内容をメール送信する処理を作成する
- メールの内容をファイルに出力する処理を作成する
- FTP サーバにアップロードする処理を作成する
- 続く。。。
手順
@MessagingGateway とは?
Spring Integration の Message 送信・受信を意識させずに、通常のメソッドを呼び出す方式で Message を送信・受信できるようにする仕組みです。
例えば以下の IntegrationFlow に Message を送信するには、
@Bean public IntegrationFlow printFlow() { return f -> f .handle((p, h) -> { System.out.println(p); return null; }); }
以下のように MessageChannel#send で Message を送信する必要があります。
@Autowired @Qualifier("printFlow.input") private MessageChannel printFlowInput; @Test public void printFlow() throws Exception { this.printFlowInput.send(new GenericMessage<>("テストです")); }
これを以下のように @MessagingGateway アノテーションを付加した interface を用意することで、
@MessagingGateway public interface PrintHelper { @Gateway(requestChannel = "printFlow.input") void print(String payload); } @Bean public IntegrationFlow printFlow() { return f -> f .handle((p, h) -> { System.out.println(p); return null; }); }
以下のように通常のメソッドのように呼び出すことができるようになります。
@Autowired private PrintHelper printHelper; @Test public void printFlow() throws Exception { this.printHelper.print("テストです"); }
また別ファイルに定義した return f -> f.~
形式の IntegrationFlow へ Message を送信しようとした時に、実行時に “~.input” の MessageChannel をうまく DI 出来ずエラーになる場合があります。この時も @MessagingGateway で inteface を定義すれば回避できます。
1点注意です。Spring Boot の 1.4 までは @IntegrationComponentScan を付加しないと ( 例えば @SpringBootApplication の下に記述します ) @MessagingGateway を付加した interface が Bean の生成対象として認識されません。1.5 からは省略できるようになりました。
recv, send ディレクトリを作成する
サンプルアプリケーションを作成します。常駐型アプリケーション用として以下の構成のディレクトリを作成します。
C:\eipapp\ksbysample-eipapp-messaginggateway ├ recv └ send
ksbysample-eipapp-messaginggateway プロジェクトを作成する
IntelliJ IDEA で Gradle プロジェクトを作成し、build.gradle を リンク先の内容 に変更します。
ksbysample-eipapp-messaginggateway プロジェクトのルート直下に config/checkstyle, config/findbugs ディレクトリを作成します。
config/checkstyle の下に Spring Boot 1.3.x の Web アプリを 1.4.x へバージョンアップする ( その8 )( build.gradle への checkstyle, findbugs の導入+CheckStyle-IDEA, FindBugs-IDEA Plugin の導入 ) で作成した google_checks.xml を置きます。
config/findbugs の下に findbugs-exclude.xml を新規作成し、リンク先の内容 の内容に変更します。
src/main/java の下に ksbysample.eipapp.messaginggateway パッケージを作成します。
src/main/java/ksbysample/eipapp/messaginggateway の下に Application.java を作成し、リンク先の内容 を記述します。
src/main/resources の下に application.properties を作成し、リンク先の内容 を記述します。
src/main/resources の下に logback-spring.xml を作成し、リンク先の内容 を記述します。
メール送信用の共通処理を作成する
SFTP サーバからファイルをダウンロードする処理を作成する
- src/main/java/ksbysample/eipapp/messaginggateway の下に FlowConfig.java を新規作成し、リンク先のその1の内容 を記述します。
ファイルの内容をメール送信する処理を作成する
- src/main/java/ksbysample/eipapp/messaginggateway の下の FlowConfig.java を リンク先のその2の内容 に変更します。
メールの内容をファイルに出力する処理を作成する
- src/main/java/ksbysample/eipapp/messaginggateway の下の FlowConfig.java を リンク先のその3の内容 に変更します。
FTP サーバにアップロードする処理を作成する
- src/main/java/ksbysample/eipapp/messaginggateway の下の FlowConfig.java を リンク先のその4の内容 に変更します。
続く。。。
長くなったので続きます。次回は以下の予定です。
- ErrorChannel の処理を実装します。
- 動作確認します。
- FlowConfig.java の完成形は次回載せます。
ソースコード
build.gradle
group 'ksbysample' version '1.0.0-RELEASE' buildscript { ext { springBootVersion = '1.4.4.RELEASE' } repositories { mavenCentral() maven { url "http://repo.spring.io/repo/" } maven { url "https://plugins.gradle.org/m2/" } } dependencies { classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") classpath("io.spring.gradle:dependency-management-plugin:0.6.1.RELEASE") // for Error Prone ( http://errorprone.info/ ) classpath("net.ltgt.gradle:gradle-errorprone-plugin:0.0.9") } } apply plugin: 'java' apply plugin: 'eclipse' apply plugin: 'idea' apply plugin: 'org.springframework.boot' apply plugin: 'io.spring.dependency-management' apply plugin: 'groovy' apply plugin: 'net.ltgt.errorprone' apply plugin: 'checkstyle' apply plugin: 'findbugs' sourceCompatibility = 1.8 targetCompatibility = 1.8 [compileJava, compileTestGroovy, compileTestJava]*.options*.compilerArgs = ['-Xlint:all,-options,-processing,-path'] idea { module { inheritOutputDirs = false outputDir = file("$buildDir/classes/main/") } } configurations { // for Error Prone ( http://errorprone.info/ ) errorprone { resolutionStrategy.force 'com.google.errorprone:error_prone_core:2.0.15' } } checkstyle { configFile = file("${rootProject.projectDir}/config/checkstyle/google_checks.xml") toolVersion = '7.5.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 } } repositories { mavenCentral() maven { url "http://repo.spring.io/repo/" } } dependencyManagement { imports { mavenBom 'io.spring.platform:platform-bom:Athens-SR3' mavenBom 'org.springframework.cloud:spring-cloud-dependencies:Camden.RELEASE' } } dependencies { // dependency-management-plugin によりバージョン番号が自動で設定されるもの // Appendix A. Dependency versions ( http://docs.spring.io/platform/docs/current/reference/htmlsingle/#appendix-dependency-versions ) 参照 compile("org.springframework.boot:spring-boot-starter-integration") compile("org.springframework.boot:spring-boot-starter-mail") compile("org.springframework.integration:spring-integration-ftp") compile("org.springframework.integration:spring-integration-mail") compile("org.springframework.integration:spring-integration-sftp") compile("org.codehaus.janino:janino") testCompile("org.springframework.boot:spring-boot-starter-test") testCompile("org.spockframework:spock-core") testCompile("org.spockframework:spock-spring") // org.springframework.cloud:spring-cloud-dependencies によりバージョン番号が自動で設定されるもの // http://projects.spring.io/spring-cloud/ の「Release Trains」参照 compile("org.springframework.cloud:spring-cloud-starter-zipkin") { exclude module: 'spring-boot-starter-web' } // dependency-management-plugin によりバージョン番号が自動で設定されないもの、あるいは最新バージョンを指定したいもの compile("org.springframework.integration:spring-integration-java-dsl:1.2.1.RELEASE") compile("org.projectlombok:lombok:1.16.14") testCompile("org.assertj:assertj-core:3.6.2") }
findbugs-exclude.xml
<?xml version="1.0" encoding="UTF-8"?> <FindBugsFilter> </FindBugsFilter>
Application.java
package ksbysample.eipapp.messaginggateway; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.integration.annotation.IntegrationComponentScan; @SpringBootApplication @IntegrationComponentScan public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }
application.properties
spring.application.name=messaginggateway spring.zipkin.base-url=http://localhost:9411/ spring.sleuth.sampler.percentage=1.0
logback-spring.xml
<?xml version="1.0" encoding="UTF-8"?> <configuration> <include resource="org/springframework/boot/logging/logback/defaults.xml"/> <springProperty scope="context" name="springAppName" source="spring.application.name"/> <property name="CONSOLE_LOG_PATTERN" value="%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(${level:-%5p}) %clr([${springAppName:-},%X{X-B3-TraceId:-},%X{X-B3-SpanId:-},%X{X-Span-Export:-}]){yellow} %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}"/> <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern>${CONSOLE_LOG_PATTERN}</pattern> <charset>UTF-8</charset> </encoder> </appender> <root level="INFO"> <appender-ref ref="CONSOLE"/> </root> <logger name="org.springframework.integration.expression.ExpressionUtils" level="ERROR"/> <logger name="com.jcraft.jsch" level="ERROR"/> </configuration>
MailHelperConfig.java
package ksbysample.eipapp.messaginggateway; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.integration.annotation.Gateway; import org.springframework.integration.annotation.MessagingGateway; import org.springframework.integration.dsl.IntegrationFlow; import org.springframework.integration.dsl.mail.Mail; import java.util.Map; @Configuration public class MailHelperConfig { @Value("${spring.mail.default-encoding:UTF-8}") private String mailDefaultEncoding; @Value("${spring.mail.host:localhost}") private String mailHost; @Value("${spring.mail.port:25}") private int mailPort; @Value("${spring.mail.protocol:smtp}") private String mailProtocol; @MessagingGateway public interface MailHelper { @Gateway(requestChannel = "sendMailFlow.input") void send(String payload, Map<Object, Object> headers); } /** * メールを送信する * メールの From, To, Subject 等は Message の header に、メール本文は payload にセットする * ヘッダーにセットする時の key 文字列は org.springframework.integration.mail.MailHeaders クラス参照 * * @return IntegrationFlow オブジェクト */ @Bean public IntegrationFlow sendMailFlow() { return f -> f .handle(Mail.outboundAdapter(this.mailHost) .port(this.mailPort) .protocol(this.mailProtocol) .defaultEncoding(this.mailDefaultEncoding) .javaMailProperties(p -> p.put("mail.debug", "true"))); } }
FlowConfig.java
■その1
package ksbysample.eipapp.messaginggateway; import com.jcraft.jsch.ChannelSftp; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.integration.dsl.IntegrationFlow; import org.springframework.integration.dsl.IntegrationFlows; import org.springframework.integration.dsl.core.Pollers; import org.springframework.integration.file.filters.AcceptAllFileListFilter; import org.springframework.integration.file.filters.IgnoreHiddenFileListFilter; import org.springframework.integration.file.remote.session.CachingSessionFactory; import org.springframework.integration.file.remote.session.SessionFactory; import org.springframework.integration.sftp.session.DefaultSftpSessionFactory; import java.io.File; @Configuration public class FlowConfig { private static final String PATH_SFTP_DOWNLOAD_DIR = "/in"; private static final String PATH_LOCAL_DOWNLOAD_DIR = "C:/eipapp/ksbysample-eipapp-messaginggateway/recv"; /** * SFTP サーバに接続するための SessionFactory オブジェクトを生成する * * @return SFTP サーバ接続用の SessionFactory オブジェクト */ @Bean public SessionFactory<ChannelSftp.LsEntry> sftpSessionFactory() { DefaultSftpSessionFactory factory = new DefaultSftpSessionFactory(true); factory.setHost("localhost"); factory.setPort(22); factory.setUser("send01"); factory.setPassword("send01"); factory.setAllowUnknownKeys(true); return new CachingSessionFactory<>(factory); } /** * SFTP サーバにあるファイルをダウンロードした後、ファイルの内容をメールで送信して、 * 送信したメールの内容をファイルに出力して FTP サーバにアップロードする * * @return IntegrationFlow オブジェクト */ @Bean public IntegrationFlow sftpToMailToFtpFlow() { return IntegrationFlows // SFTP サーバの /in ディレクトリにファイルがあるか 5秒間隔でチェックする .from(s -> s.sftp(sftpSessionFactory()) .preserveTimestamp(true) .deleteRemoteFiles(true) .remoteDirectory(PATH_SFTP_DOWNLOAD_DIR) .localDirectory(new File(PATH_LOCAL_DOWNLOAD_DIR)) .localFilter(new AcceptAllFileListFilter<>()) .localFilter(new IgnoreHiddenFileListFilter()) , e -> e.poller(Pollers.fixedDelay(5000) .maxMessagesPerPoll(100))) .log() .<File>handle((p, h) -> { p.delete(); return null; }) .get(); } }
■その2
@Configuration public class FlowConfig { .......... private static final String DOWNLOADFILEMAIL_FROM = "system@sample.com"; private static final String DOWNLOADFILEMAIL_TO = "download@test.co.jp"; private final MailHelperConfig.MailHelper mailHelper; public FlowConfig(MailHelperConfig.MailHelper mailHelper) { this.mailHelper = mailHelper; } .......... /** * SFTP サーバにあるファイルをダウンロードした後、ファイルの内容をメールで送信して、 * 送信したメールの内容をファイルに出力して FTP サーバにアップロードする * * @return IntegrationFlow オブジェクト */ @Bean public IntegrationFlow sftpToMailToFtpFlow() { return IntegrationFlows .......... // ファイル名とファイルの絶対パスを Message の header にセットする .enrichHeaders(h -> h .headerExpression(FileHeaders.FILENAME, "payload.name") .headerExpression(FileHeaders.ORIGINAL_FILE, "payload.absolutePath")) // File の内容を読み込んで payload へセットする .transform(Transformers.fileToString()) // ファイルの内容をメール本文とするメールを送信する .<String>handle((p, h) -> { this.mailHelper.send(p , new MapBuilder<>() .put(MailHeaders.FROM, DOWNLOADFILEMAIL_FROM) .put(MailHeaders.TO, DOWNLOADFILEMAIL_TO) .put(MailHeaders.SUBJECT, h.get(FileHeaders.FILENAME)) .get()); return MessageBuilder.withPayload(new File((String) h.get(FileHeaders.ORIGINAL_FILE))) .setHeader(MailHeaders.TO, DOWNLOADFILEMAIL_TO) .setHeader(MailHeaders.SUBJECT, h.get(FileHeaders.FILENAME)) .build(); }) .log() .......... .get(); } }
- MapBuilder は org.springframework.integration.dsl.support パッケージにあるクラスです。
■その3
@Configuration public class FlowConfig { .......... private static final String PATH_LOCAL_UPLOAD_DIR = "C:/eipapp/ksbysample-eipapp-messaginggateway/send"; .......... private static final String CRLF = "\r\n"; .......... /** * SFTP サーバにあるファイルをダウンロードした後、ファイルの内容をメールで送信して、 * 送信したメールの内容をファイルに出力して FTP サーバにアップロードする * * @return IntegrationFlow オブジェクト */ @Bean public IntegrationFlow sftpToMailToFtpFlow() { return IntegrationFlows .......... // メール送信した内容を payload にセットする .<String>handle((p, h) -> { StringBuilder sb = new StringBuilder(); sb.append("To: " + h.get(MailHeaders.TO) + CRLF); sb.append("Subject: " + h.get(MailHeaders.SUBJECT) + CRLF); sb.append(CRLF); sb.append(p); return MessageBuilder.withPayload(sb.toString()) .build(); }) // /send ディレクトリの下に payload の内容を出力したファイルを生成する .handleWithAdapter(a -> a.fileGateway(new File(PATH_LOCAL_UPLOAD_DIR))) .log() // /recv ディレクトリの下のファイルを削除し、payload には /send ディレクトリの下の // ファイルを指す File オブジェクトをセットする .handle((p, h) -> { try { Files.delete(Paths.get((String) h.get(FileHeaders.ORIGINAL_FILE))); } catch (IOException e) { throw new RuntimeException(e); } return MessageBuilder .withPayload(Paths.get(PATH_LOCAL_UPLOAD_DIR , (String) h.get(FileHeaders.FILENAME)).toFile()) .build(); }) .channel("nullChannel") .get(); } }
■その4
@Configuration public class FlowConfig { .......... private static final String PATH_FTP_UPLOAD_DIR = "/out"; .......... /** * FTP サーバに接続するための SessionFactory オブジェクトを生成する * * @return FTP サーバ接続用の SessionFactory オブジェクト */ @Bean public SessionFactory<FTPFile> ftpSessionFactory() { DefaultFtpSessionFactory factory = new DefaultFtpSessionFactory(); factory.setHost("localhost"); factory.setPort(21); factory.setUsername("recv01"); factory.setPassword("recv01"); return new CachingSessionFactory<>(factory); } /** * SFTP サーバにあるファイルをダウンロードした後、ファイルの内容をメールで送信して、 * 送信したメールの内容をファイルに出力して FTP サーバにアップロードする * * @return IntegrationFlow オブジェクト */ @Bean public IntegrationFlow sftpToMailToFtpFlow() { return IntegrationFlows .......... // /send ディレクトリの下に作成したファイルを FTP サーバにアップロードする .bridge(e -> e.advice(ftpUploadAdvice())) .log() // /send ディレクトリの下に作成したファイルを削除する .<File>handle((p, h) -> { try { Files.delete(Paths.get(p.getAbsolutePath())); } catch (IOException e) { throw new RuntimeException(e); } return null; }) .log() .get(); } /** * Message の payload にセットされた File オブジェクトが指し示すファイルを FTP サーバにアップロードする * * @return IntegrationFlow オブジェクト */ @Bean public IntegrationFlow ftpUploadFlow() { return f -> f .handleWithAdapter(a -> a.ftp(ftpSessionFactory()).remoteDirectory(PATH_FTP_UPLOAD_DIR)); } /** * ftpUploadFlow へ Message を送信する ExpressionEvaluatingRequestHandlerAdvice Bean * * @return ExpressionEvaluatingRequestHandlerAdvice オブジェクト */ @Bean public Advice ftpUploadAdvice() { ExpressionEvaluatingRequestHandlerAdvice advice = new ExpressionEvaluatingRequestHandlerAdvice(); advice.setOnSuccessExpressionString("payload"); advice.setSuccessChannelName("ftpUploadFlow.input"); return advice; } }
履歴
2017/02/25
初版発行。
Spring Boot 1.3.x の Web アプリを 1.4.x へバージョンアップする ( その9 )( 1.3系 → 1.4系で実装方法が変更された点を修正する )
概要
記事一覧はこちらです。
- 今回の手順で確認できるのは以下の内容です。
- 1.3系 → 1.4系で実装方法が変更された点を修正します。
- 今回は変更が軽微なものだけで、フィールドへの @Autowired 付加 → コンストラクタインジェクションへの変更、及びテストクラスのアノテーションの変更は次回以降にやります。
参照したサイト・書籍
Spring Boot 1.4 Release Notes
https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-1.4-Release-NotesSpring Boot 1.4+でRestTemplate(HTTPクライアント)を使う
http://qiita.com/kazuki43zoo/items/7cf3c8ca4f6f2283cefb
目次
- spring-boot-starter で名前が変更されたものを反映する
- application.properties に spring.session.store-type の設定を追加する
- application.properties の spring.datasource の項目名を変更する
- @RequestMapping を変更可能なところは @GetMapping, @PostMapping に変更する
- RestTemplate オブジェクトを生成する処理を
new RestTemplate(...)
→RestTemplateBuilder#build
へ変更する - 次回は。。。
手順
spring-boot-starter で名前が変更されたものを反映する
1.4 から spring-boot-starter-redis
→ spring-boot-starter-data-redis
へ変更されたので反映します。
build.gradle を リンク先の内容 に変更します。
Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。
Project Tool Window の External Libraries を見ると
spring-boot-starter-redis
が消えてspring-boot-starter-data-redis
が入ったことが確認できます。
application.properties に spring.session.store-type の設定を追加する
Spring Session のデータ保存先を application.properties で設定できるようになったので、その設定を追加します。
application.properties を リンク先の内容 に変更します。
設定する時に IntelliJ IDEA で候補を表示させてみたのですが、
hash_map
やnone
といった選択肢もありました。一時的に Spring Session を試したいだけならサーバを立てずにhash_map
を選択するのもありなのかもしれません。none
は何だろう?と思ったら、39. Spring Session に Spring Session を無効化したい時に設定するよう記述がありました。
application.properties の spring.datasource の項目名を変更する
使用しているコネクションプーリングのライブラリに応じた namespace が増えました。Spring Boot では特に指定をしていなければ Tomcat JDBC Connection Pool が使用されるので、spring.datasource
→ spring.datasource.tomcat
へ変更します。
Appendix A. Common application properties には「spring.datasource.tomcat.*= # Tomcat datasource specific settings」としか記述がありませんが、IntelliJ IDEA で設定項目を表示させてみたところ、以下の項目が表示されました。
知らない設定がいろいろ表示されています。次回以降で設定しておいた方がよいものがあるのか調べてみたいと思います。Tomcat JDBC Connection Pool の URL もメモしておきます。
The Tomcat JDBC Connection Pool
https://tomcat.apache.org/tomcat-8.5-doc/jdbc-pool.html
application-develop.properties, application-product.properties, application-unittest.properties を リンク先の内容 に変更します。
src/main/java/ksbysample/webapp/lending/config の下の ApplicationConfig.java を リンク先のその1の内容 に変更します。
※org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration クラスが dataSource Bean を生成して spring.datasource.tomcat の設定を反映してくれると思ったのですが、なぜかうまく行きませんでした。。。 ので、自分で dataSource Bean を定義しています。
@RequestMapping を変更可能なところは @GetMapping, @PostMapping に変更する
ksbysample-webapp-lending のソースを見てみると @RequestMapping(value = "...", method = RequestMethod.POST)
のように POST のみに制限しているところはあっても、GET のみで制限しているところはありませんでした。今回は @RequestMapping(value = "...", method = RequestMethod.POST)
→ @PostMapping("...")
にのみ変更することにします。
以下のクラスのメソッドで
@RequestMapping(value = "...", method = RequestMethod.POST)
→@PostMapping("...")
へ変更します。- ksbysample.webapp.lending.web.confirmresult.ConfirmresultController#filedownloadByResponse
- ksbysample.webapp.lending.web.confirmresult.ConfirmresultController#filedownloadByView
- ksbysample.webapp.lending.web.lendingapp.LendingappController#apply
- ksbysample.webapp.lending.web.lendingapp.LendingappController#temporarySave
- ksbysample.webapp.lending.web.lendingapproval.LendingapprovalController#complete
RestTemplate オブジェクトを生成する処理を new RestTemplate(...)
→ RestTemplateBuilder#build
へ変更する
RestTemplate オブジェクトを生成するための RestTemplateBuilder クラスが提供されています。タイムアウトを設定するのも RestTemplateBuilder クラスを使用した方が簡単で分かりやすいので、RestTemplateBuilder クラスを利用する方法に変更します。
MappingJackson2XmlHttpMessageConverter を生成する部分は Bean にします。src/main/java/ksbysample/webapp/lending/config の下の ApplicationConfig.java を リンク先のその2の内容 に変更します。
src/main/java/ksbysample/webapp/lending/service/calilapi の下の CalilApiService.java を リンク先の内容 に変更します。
※src/main/java/ksbysample/webapp/lending/service/openweathermapapi の下の OpenWeatherMapApiService.java は使用していないのとテストも動かないので、変更しません。
次回は。。。
引き続き 1.3系 → 1.4系で実装方法が変更された点を修正します。
次回はフィールドへの @Autowired 付加 → コンストラクタインジェクションへの変更、その次にテストクラスのアノテーションの変更をやる予定です。
ソースコード
build.gradle
dependencies { .......... // dependency-management-plugin によりバージョン番号が自動で設定されるもの // Appendix A. Dependency versions ( http://docs.spring.io/platform/docs/current/reference/htmlsingle/#appendix-dependency-versions ) 参照 .......... compile("org.springframework.boot:spring-boot-starter-security") compile("org.springframework.boot:spring-boot-starter-data-redis") compile("org.springframework.boot:spring-boot-starter-amqp") ..........
spring-boot-starter-redis
→spring-boot-starter-data-redis
へ変更します。
application.properties
hibernate.dialect=org.hibernate.dialect.PostgreSQL9Dialect doma.dialect=org.seasar.doma.jdbc.dialect.PostgresDialect spring.jpa.hibernate.ddl-auto=none spring.jpa.hibernate.naming_strategy=org.hibernate.cfg.ImprovedNamingStrategy spring.session.store-type=redis spring.freemarker.cache=true spring.freemarker.charset=UTF-8 spring.freemarker.enabled=false spring.freemarker.prefer-file-system-access=false
spring.session.store-type=redis
を追加します。
application-develop.properties, application-product.properties, application-unittest.properties
■application-develop.properties
spring.datasource.tomcat.url=jdbc:log4jdbc:postgresql://localhost/ksbylending spring.datasource.tomcat.username=ksbylending_user spring.datasource.tomcat.password=xxxxxxxx spring.datasource.tomcat.driverClassName=net.sf.log4jdbc.sql.jdbcapi.DriverSpy
■application-product.properties
spring.datasource.tomcat.url=jdbc:postgresql://localhost/ksbylending spring.datasource.tomcat.username=ksbylending_user spring.datasource.tomcat.password=xxxxxxxx spring.datasource.tomcat.driverClassName=org.postgresql.Driver
■application-unittest.properties
spring.datasource.tomcat.url=jdbc:postgresql://localhost/ksbylending spring.datasource.tomcat.username=ksbylending_user spring.datasource.tomcat.password=xxxxxxxx spring.datasource.tomcat.driverClassName=org.postgresql.Driver
- 全て
spring.datasource
→spring.datasource.tomcat
へ変更します。尚、上で設定している url, username, password, driverClassName のような全てのコネクションプーリングで共通の設定の場合にはspring.datasource
のままでも構いません。
ApplicationConfig.java
■その1
.......... import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder; .......... import javax.sql.DataSource; @Configuration public class ApplicationConfig { .......... /** * @return Tomcat JDBC Connection Pool の DataSource オブジェクト */ @Bean @ConfigurationProperties("spring.datasource.tomcat") public DataSource dataSource() { return DataSourceBuilder.create() .type(org.apache.tomcat.jdbc.pool.DataSource.class) .build(); } }
- dataSource メソッドを追加します。この時
@ConfigurationProperties("spring.datasource.tomcat")
アノテーションを付加してspring.datasource.tomcat
の設定が DataSource オブジェクトに設定されるようにします。
■その2
@Configuration public class ApplicationConfig { .......... /** * 外部のWebAPIとXMLフォーマットで通信するために使用する MessageConverter * build.gralde に compile("com.fasterxml.jackson.dataformat:jackson-dataformat-xml:...") を記述して * jackson-dataformat-xml が使用できるように設定しないと Bean は生成されない * * @return MappingJackson2XmlHttpMessageConverter オブジェクト */ @Bean @ConditionalOnClass(com.fasterxml.jackson.dataformat.xml.XmlMapper.class) public MappingJackson2XmlHttpMessageConverter mappingJackson2XmlHttpMessageConverter() { // findAndRegisterModules メソッドを呼び出して jackson-dataformat-xml が機能するようにする return new MappingJackson2XmlHttpMessageConverter(new XmlMapper().findAndRegisterModules()); } }
- mappingJackson2XmlHttpMessageConverter Bean を追加します。
- 以前 ksbysample.webapp.lending.service.calilapi.CalilApiService#getMessageConvertersforJackson2Xml に実装していた内容を持ってきて、以下の点を変更しました。
assert(...)
は削除して@ConditionalOnClass(com.fasterxml.jackson.dataformat.xml.XmlMapper.class)
に変更しました。- MappingJackson2XmlHttpMessageConverter#setObjectMapper を呼び出すのではなく、
new MappingJackson2XmlHttpMessageConverter(...)
の引数で渡すように変更しました。 - spring-framework/spring-web/src/main/java/org/springframework/http/converter/xml/MappingJackson2XmlHttpMessageConverter.java を見ると、デフォルトで必要な MediaType がセットされていたので、MappingJackson2XmlHttpMessageConverter#setSupportedMediaTypes でセットしていた処理を削除しました。
- 以前 ksbysample.webapp.lending.service.calilapi.CalilApiService#getMessageConvertersforJackson2Xml に実装していた内容を持ってきて、以下の点を変更しました。
CalilApiService.java
package ksbysample.webapp.lending.service.calilapi; import com.google.common.base.Joiner; import ksbysample.webapp.lending.service.calilapi.response.Book; import ksbysample.webapp.lending.service.calilapi.response.CheckApiResponse; import ksbysample.webapp.lending.service.calilapi.response.Libraries; import ksbysample.webapp.lending.service.calilapi.response.LibrariesForJackson2Xml; import org.simpleframework.xml.Serializer; import org.simpleframework.xml.core.Persister; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; import org.springframework.http.ResponseEntity; import org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; import java.util.HashMap; import java.util.List; import java.util.Map; @Service @PropertySource("classpath:calilapi.properties") public class CalilApiService { private final Logger logger = LoggerFactory.getLogger(this.getClass()); private static final int RETRY_MAX_CNT = 5; private static final long RETRY_SLEEP_MILLS = 3000; private static final String URL_CALILAPI_ROOT = "http://api.calil.jp"; private static final String URL_CALILAPI_LIBRALY = URL_CALILAPI_ROOT + "/library?appkey={appkey}&pref={pref}"; private static final String URL_CALILAPI_CHECK = URL_CALILAPI_ROOT + "/check?appkey={appkey}&systemid={systemid}&isbn={isbn}&format=xml"; private static final String URL_CALILAPI_CHECK_FOR_RETRY = URL_CALILAPI_ROOT + "/check?session={session}&format=xml"; @Value("${calil.apikey}") private String calilApiKey; private final RestTemplate restTemplateForCalilApi; private final RestTemplate restTemplateForCalilApiByXml; public CalilApiService(@Qualifier("restTemplateForCalilApi") RestTemplate restTemplateForCalilApi , @Qualifier("restTemplateForCalilApiByXml") RestTemplate restTemplateForCalilApiByXml) { this.restTemplateForCalilApi = restTemplateForCalilApi; this.restTemplateForCalilApiByXml = restTemplateForCalilApiByXml; } /** * @param pref ??? * @return ??? * @throws Exception */ public Libraries getLibraryList(String pref) throws Exception { // 図書館データベースAPIを呼び出して XMLレスポンスを受信する ResponseEntity<String> response = this.restTemplateForCalilApi.getForEntity(URL_CALILAPI_LIBRALY , String.class, this.calilApiKey, pref); // 受信した XMLレスポンスを Javaオブジェクトに変換する Serializer serializer = new Persister(); Libraries libraries = serializer.read(Libraries.class, response.getBody()); return libraries; } /** * @param pref ??? * @return ??? * @throws Exception */ public LibrariesForJackson2Xml getLibraryListByJackson2Xml(String pref) throws Exception { // 図書館データベースAPIを呼び出して XMLレスポンスを受信する ResponseEntity<LibrariesForJackson2Xml> response = this.restTemplateForCalilApiByXml.getForEntity(URL_CALILAPI_LIBRALY , LibrariesForJackson2Xml.class, this.calilApiKey, pref); return response.getBody(); } /** * @param systemid ??? * @param isbnList ??? * @return ??? */ public List<Book> check(String systemid, List<String> isbnList) { Map<String, String> vars = new HashMap<>(); vars.put("appkey", this.calilApiKey); vars.put("systemid", systemid); vars.put("isbn", Joiner.on(",").join(isbnList)); ResponseEntity<CheckApiResponse> response = null; String url = URL_CALILAPI_CHECK; for (int retry = 0; retry < RETRY_MAX_CNT; retry++) { // 蔵書検索APIを呼び出して蔵書の有無と貸出状況を取得する response = this.restTemplateForCalilApiByXml.getForEntity(url, CheckApiResponse.class, vars); logger.info("カーリルの蔵書検索API を呼び出し、レスポンスを取得しました。{}", response.getBody().toString()); if (response.getBody().getContinueValue() == 0) { break; } // continue の値が 0 でない場合には2秒以上待機した後、URLパラメータを session に変更して再度リクエストを送信する try { Thread.sleep(RETRY_SLEEP_MILLS); } catch (InterruptedException e) { logger.warn("カーリルの蔵書検索APIのsleep中にInterruptedExceptionが発生しましたが、処理は継続します。", e); } url = URL_CALILAPI_CHECK_FOR_RETRY; vars.clear(); vars.put("session", response.getBody().getSession()); } return response.getBody().getBookList(); } @Configuration public static class CalilApiConfig { private static int CONNECT_TIMEOUT = 5000; private static int READ_TIMEOUT = 5000; private final RestTemplateBuilder restTemplateBuilder; private final MappingJackson2XmlHttpMessageConverter mappingJackson2XmlHttpMessageConverter; /** * コンストラクタ * * @param restTemplateBuilder restTemplateBuilder Bean * @param mappingJackson2XmlHttpMessageConverter mappingJackson2XmlHttpMessageConverter Bean */ public CalilApiConfig(RestTemplateBuilder restTemplateBuilder , MappingJackson2XmlHttpMessageConverter mappingJackson2XmlHttpMessageConverter) { this.restTemplateBuilder = restTemplateBuilder; this.mappingJackson2XmlHttpMessageConverter = mappingJackson2XmlHttpMessageConverter; } /** * カーリルの図書館API呼び出し用 RestTemplate * JSON フォーマットで結果を受信する * * @return RestTemplate オブジェクト */ @Bean public RestTemplate restTemplateForCalilApi() { return this.restTemplateBuilder .setConnectTimeout(CONNECT_TIMEOUT) .setReadTimeout(READ_TIMEOUT) .rootUri(URL_CALILAPI_ROOT) .build(); } /** * カーリルの図書館API呼び出し用 RestTemplate * XML フォーマットで結果を受信する * * @return RestTemplate オブジェクト */ @Bean public RestTemplate restTemplateForCalilApiByXml() { return this.restTemplateBuilder .setConnectTimeout(CONNECT_TIMEOUT) .setReadTimeout(READ_TIMEOUT) .rootUri(URL_CALILAPI_ROOT) .messageConverters(this.mappingJackson2XmlHttpMessageConverter) .build(); } } }
@Configuration public static class CalilApiConfig { ... }
を追加して、その中で restTemplateForCalilApi Bean, restTemplateForCalilApiByXml Bean を定義するようにします。- RETRY_MAX_CNT, RETRY_SLEEP_MILLS の定義は CalilApiConfig クラス内へ移動します。
- URL_CALILAPI_ROOT の定義を追加し、URL_CALILAPI_LIBRALY, URL_CALILAPI_CHECK, URL_CALILAPI_CHECK_FOR_RETRY の URL の定義を URL_CALILAPI_ROOT を使うように変更しました。URL_CALILAPI_ROOT は restTemplateForCalilApi Bean, restTemplateForCalilApiByXml Bean の生成時に RestTemplateBuilder#rootUri を呼び出す時に使用します。
- DI 用の以下のフィールドと、インジェクションするためのコンストラクタを追加します。
private final RestTemplate restTemplateForCalilApi;
private final RestTemplate restTemplateForCalilApiByXml;
- 各メソッド内で
new RestTemplate(...)
を呼び出して RestTemplate オブジェクトを生成している処理を削除しました。
履歴
2017/02/22
初版発行。
Spring Boot 1.3.x の Web アプリを 1.4.x へバージョンアップする ( その8 )( build.gradle への checkstyle, findbugs の導入+CheckStyle-IDEA, FindBugs-IDEA Plugin の導入 )
概要
記事一覧はこちらです。
- 今回の手順で確認できるのは以下の内容です。
- 前回 Error Prone を導入してみてコードチェックツールはいれた方が良さそうに感じたので、有名な checkstyle と FindBugs を入れてみます。
- 最初に build.gradle に checkstyle, findbugs を導入してみます。
- その後で IntelliJ IDEA の Plugin である CheckStyle-IDEA, FindBugs-IDEA を導入してみます。
参照したサイト・書籍
Google’s Java Style の CheckStyle を使う
http://create-something.hatenadiary.jp/entry/2015/01/31/121332checkstyle/src/main/resources/google_checks.xml
https://github.com/checkstyle/checkstyle/blob/master/src/main/resources/google_checks.xmlGoogle Java Style Guide
https://google.github.io/styleguide/javaguide.htmlHow can I disable checkstyle rules for tests in gradle?
http://stackoverflow.com/questions/32772872/how-can-i-disable-checkstyle-rules-for-tests-in-gradleFindBugsExtension http://gradle.monochromeroad.com/docs/dsl/org.gradle.api.plugins.quality.FindBugsExtension.html
- build.gradle へ findbugs を導入する時の設定はこちらを参照しました。
JavaのバグをFindBugsで見つける
https://internetcom.jp/developer/20081017/26.htmlJenkins+GradleでJavaのCIのための基本build.gradle設定 (JUnit,PMD,FindBugs,CPD,JaCoCo)
http://qiita.com/mychaelstyle/items/74baa62b7bf2fe81e309How to generate HTML output using Gradle FindBugs Plugin
http://stackoverflow.com/questions/15406469/how-to-generate-html-output-using-gradle-findbugs-plugin第8章 フィルターファイル
http://findbugs.sourceforge.net/ja/manual/filter.html
目次
- build.gradle に checkstyle を導入してみる
- Google Java Style の checkstyle 用XMLファイル google_checks.xml をダウンロードする
- build.gradle を変更する
- build タスクを実行する
のための間違った辞書式順序 '...' インポート。 ... [CustomImportOrder]
、のインポート文 '...' は、間違った順序です。 ... [CustomImportOrder]
行が 100 文字を超えています。 [LineLength]
インデント階層 ... の ... が正しいインデント ... にありません [Indentation]
Javadoc コメントがありません。 [JavadocMethod]
',' は前の行にあるべきです。 [SeparatorWrap]
Javadocのの??最初の文は、(期間が欠落している)、または存在しない不完全です。 [SummaryJavadoc]
アット節非空の記述を持っている必要があります。 [NonEmptyAtclauseDescription]
'METHOD_DEF' 前の文から分離する必要があります。 [EmptyLineSeparator]
オーバーロードメソッドは、分割すべきではありません。 ... [OverloadMethodsDeclarationOrder]
- 再び build タスクを実行するも、まだエラーが出ます
'.*' 形式のインポートの使用は避けるべきです ... [AvoidStarImport]
名前に略語は '...' 以下にする必要があります ... [AbbreviationAsWordInName]
Member name '...' must match pattern '^[a-z][a-z0-9][a-zA-Z0-9]*$'. [MemberName]
'}' 欄で ... はマルチブロック文を直接複数のブロックが含まれています ... [RightCurly]
変数 '...' の宣言と、その変数の使用開始位置までの距離が 7 です。許可された距離 は 3 です。... [VariableDeclarationUsageDistance]
- 再び build タスクを実行し警告もエラーも出なくなりました
- build.gradle に findbugs を導入してみる
- CheckStyle-IDEA Plugin を導入してみる
- FindBugs-IDEA Plugin を導入してみる
- 次回は。。。
手順
build.gradle に checkstyle を導入してみる
Google Java Style の checkstyle 用XMLファイル google_checks.xml をダウンロードする
Google Java Style 用の chekstyle の XMLファイルがダウンロードできますので、それをベースに自分向けの変更を加えて導入してみます。
プロジェクトのルートから config/checkstyle ディレクトリを作成します。
checkstyle/src/main/resources/google_checks.xml から google_checks.xml をダウンロードし、config/checkstyle の下に配置します。
build.gradle を変更する
build.gradle を リンク先のその1の内容 に変更します。
変更後、Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。
build タスクを実行する
clean タスク → Rebuild Project → build タスク の順に実行します。
build タスクが実行されるといろいろダウンロードされます。
その後に大量の WARN メッセージが出力されました。
出力されているのは以下の WARN メッセージです。1つずつ対応していきます。
- インポートの順番間違いに関するメッセージ
- のための間違った辞書式順序 ‘…’ インポート。の前にすべきである … [CustomImportOrder]
- のインポート文 ‘…’ は、間違った順序です。 … [CustomImportOrder]
- 行が 100 文字を超えています。 [LineLength]
- インデント階層 … の … が正しいインデント … にありません [Indentation]
- Javadoc コメントがありません。 [JavadocMethod]
- ‘,’ は前の行にあるべきです。 [SeparatorWrap]
- Javadocのの??最初の文は、(期間が欠落している)、または存在しない不完全です。 [SummaryJavadoc]
- アット節非空の記述を持っている必要があります。 [NonEmptyAtclauseDescription]
- ‘METHOD_DEF’ 前の文から分離する必要があります。 [EmptyLineSeparator]
- オーバーロードメソッドは、分割すべきではありません。 … [OverloadMethodsDeclarationOrder]
- インポートの順番間違いに関するメッセージ
のための間違った辞書式順序 '...' インポート。 ... [CustomImportOrder]
、のインポート文 '...' は、間違った順序です。 ... [CustomImportOrder]
import 文の順序は IntelliJ IDEA 任せにしているので checkstyle ではチェックしないようにします。以下の部分をコメントアウトします。
<!-- import 文の順序は IntelliJ IDEA 任せにしているのでチェックしない --> <!--<module name="CustomImportOrder">--> <!--<property name="sortImportsInGroupAlphabetically" value="true"/>--> <!--<property name="separateLineBetweenGroups" value="true"/>--> <!--<property name="customImportOrderRules" value="STATIC###THIRD_PARTY_PACKAGE"/>--> <!--</module>-->
行が 100 文字を超えています。 [LineLength]
IntelliJ IDEA のエディタの右側に縦線が引かれていますが、これは 120 文字の位置に引かれています。
この設定を変える気はありませんので、checkstyle によるチェックも 120 文字に変更します。
<module name="LineLength"> <property name="max" value="120"/> <property name="ignorePattern" value="^package.*|^import.*|a href|href|http://|https://|ftp://"/> </module>
<property name="max" value="100"/>
→<property name="max" value="120"/>
に変更します。
また 120 文字を超えている行がある場合にはソースを変更します。
インデント階層 ... の ... が正しいインデント ... にありません [Indentation]
Google Java Style のインデントのデフォルトは 2 ですが、IntelliJ IDEA のフォーマットではインデントは 4 なので 4 に変更します。
<module name="Indentation"> <property name="basicOffset" value="4"/> <property name="braceAdjustment" value="0"/> <property name="caseIndent" value="4"/> <property name="throwsIndent" value="4"/> <property name="lineWrappingIndentation" value="4"/> <property name="arrayInitIndent" value="4"/> </module>
value="2"
→value="4"
に変更します。
Javadoc コメントがありません。 [JavadocMethod]
これは checkstyle のルールは変更せずにソースに Javadoc を付ける方針とします。今回は詳細なコメントは書かずに単に Javadoc のヘッダだけ書いておきます。
',' は前の行にあるべきです。 [SeparatorWrap]
自分は “,” を前に付ける方が好みなので checkstyle のルールを変更します。
<module name="SeparatorWrap"> <property name="id" value="SeparatorWrapComma"/> <property name="tokens" value="COMMA"/> <property name="option" value="nl"/> </module>
- SeparatorWrapComma の option の
value="EOL"
→value="nl"
へ変更します。
Javadocのの??最初の文は、(期間が欠落している)、または存在しない不完全です。 [SummaryJavadoc]
Google Java Style だと Javadoc のコメントの1行目には、このメソッドが何を返すのか、あるいは何をするメソッドなのかを記述することを強制するルールのようです。あとから真似るかもしれませんが、今はコメントアウトしておきます。
<!-- Javadoc の1行目のフォーマットを指定するルールだが、今はそこまで真似られないのでコメントアウトする --> <!--<module name="SummaryJavadoc">--> <!--<property name="forbiddenSummaryFragments" value="^@return the *|^This method returns |^A [{]@code [a-zA-Z0-9]+[}]( is a )"/>--> <!--</module>-->
アット節非空の記述を持っている必要があります。 [NonEmptyAtclauseDescription]
最初に日本語のメッセージを見た時は何を言っているのか分かりませんでしたが、Javadoc で @param, @return 等の @ から始まるタグの説明が空の場合に警告を出すルールでした。
ルールとしては入れておきたいので checkstyle の XMLファイルはこのままにしますが、今からソースの Javadoc に説明を追加するのはさすがに手間なので今は “???” だけ書いておくことにします。
。。。と思ったのですが、"???“ を書いていて @throws だけこの対象から外しておきたくなったので checkstyle のルールを以下のように変更します。
<module name="NonEmptyAtclauseDescription"> <!-- @throws はチェックの対象から外す --> <property name="javadocTokens" value="PARAM_LITERAL,RETURN_LITERAL,DEPRECATED_LITERAL"/> </module>
<property name="javadocTokens" value="PARAM_LITERAL,RETURN_LITERAL,DEPRECATED_LITERAL"/>
を追加します。
'METHOD_DEF' 前の文から分離する必要があります。 [EmptyLineSeparator]
この警告はここまで修正したら消えてしまったので無視します。
オーバーロードメソッドは、分割すべきではありません。 ... [OverloadMethodsDeclarationOrder]
ksbysample.webapp.lending.dao.LendingAppDao クラス等 Doma 2 の interface で出力されていて対応のしようがないので、ルールをコメントアウトすることにします。
<!-- Doma2 の Dao interface で警告が出て対応のしようがないのでコメントアウトする --> <!--<module name="OverloadMethodsDeclarationOrder"/>-->
再び build タスクを実行するも、まだエラーが出ます
出ていた警告には対応したので、再び clean タスク → Rebuild Project → build タスク の順に実行します。
まだ警告が出力されており、出力されたのは以下の WARN メッセージでした。1つずつ対応していきます。
- ‘.*’ 形式のインポートの使用は避けるべきです … [AvoidStarImport]
- 名前に略語は ‘…’ 以下にする必要があります … [AbbreviationAsWordInName]
- Member name ‘…’ must match pattern ‘^[a-z][a-z0-9][a-zA-Z0-9]*$’. [MemberName]
- ‘}’ 欄で … はマルチブロック文を直接複数のブロックが含まれています … [RightCurly]
- 変数 ‘…’ の宣言と、その変数の使用開始位置までの距離が 7 です。許可された距離 は 3 です。… [VariableDeclarationUsageDistance]
'.*' 形式のインポートの使用は避けるべきです ... [AvoidStarImport]
import 文の記述は IntelliJ IDEA 任せにしているので checkstyle ではチェックしないようにします。以下の部分をコメントアウトします。
<!-- import 文の記述は IntelliJ IDEA 任せにしているのでチェックしない --> <!--<module name="AvoidStarImport"/>-->
名前に略語は '...' 以下にする必要があります ... [AbbreviationAsWordInName]
2種類の意味で出力されていました。
まず1種類目は 名前に略語は 'TEMPLATE_LOCATION_TEXTMAIL' 以下にする必要があります '1' 大文字を。 [AbbreviationAsWordInName]
のように定数文字列で出力されているもので、これはソースの記述で private static final ...
ではなく private final ...
にしていたためでした。。。 ( 定数でないのでキャメルケースを使用しているのが NG と判断されています ) static
を追加します。
2種類目は 名前に略語は 'BooklistCSVRecord' 以下にする必要があります '1' 大文字を。 [AbbreviationAsWordInName]
のようにクラス名等で “CSV” のように大文字続きの名称にしていたためでした。CSV
→ Csv
にように全てスネークケースになるよう変更します。
この警告については checkstyle のルールは変更しません。
Member name '...' must match pattern '^[a-z][a-z0-9][a-zA-Z0-9]*$'. [MemberName]
これは上の 名前に略語は '...' 以下にする必要があります ... [AbbreviationAsWordInName]
の警告を修正したら出なくなりましたので checkstyle のルールもソースも変更しません。
'}' 欄で ... はマルチブロック文を直接複数のブロックが含まれています ... [RightCurly]
これはソースのオートフォーマット忘れで、
}
else {
と書かれていたためでした。オートフォーマットするとこうなります。
} else {
checkstyle のルールは変更しません。
変数 '...' の宣言と、その変数の使用開始位置までの距離が 7 です。許可された距離 は 3 です。... [VariableDeclarationUsageDistance]
変数が宣言された位置と実際に使用されている位置が 4 行以上離れているため出ている警告でした。ただし実際に指摘を受けた箇所を見てみたのですが、位置を修正したくなかったので以下のようにチェックされてる行数を少し拡大することにします ( 実際にはこのルール通り書けた方がよいと思うのですが、サンプルとして作っているソースでこのルールを厳密に適用しようとするとちょっと辛いかなと思ったので緩くします )。
<module name="VariableDeclarationUsageDistance"> <!-- allowedDistance のデフォルト値は 3 だが、少し短すぎるので 10 に変更する --> <property name="allowedDistance" value="10"/> </module>
再び build タスクを実行し警告もエラーも出なくなりました
再び clean タスク → Rebuild Project → build タスク の順に実行します。
今度は1つも警告、エラーが出ずに “BUILD SUCCESSFUL” の文字が出力されました。
checkstyle も Error Prone 同様に修正した方がよいと思われる箇所を結構いろいろ指摘してくれるので気に入りました。こちらも今後は積極的に入れていきたいと思います。
build.gradle に findbugs を導入してみる
build.gradle を変更する
build.gradle を リンク先のその2の内容 に変更します。
変更後、Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。
build タスクを実行する
clean タスク → Rebuild Project → build タスク の順に実行します。
build タスクが実行されるといろいろダウンロードされます。
build タスク自体は “BUILD SUCCESSFUL” のメッセージが出て終了しましたが、FindBugs が何か検知して
FindBugs rule violations were found.
のメッセージも出力されていました。デフォルトの設定では build/reports/findbugs ディレクトリにレポートの XML ファイルが生成されます。生成されたレポートファイル main.xml を開いてみたのですが、Error Prone や checkstyle のメッセージと比較するとすごく見にくいですね。findbugs を導入している人って毎回このレポートを見ているのでしょうか。。。?
さすがに毎回このレポートファイルを見るのはちょっとつらそうな気がする&絶対見やすくする方法があるはず、と思ったので調べてみます。
レポートの出力形式を xml ではなく html に変更する
古い記事ですが JavaのバグをFindBugsで見つける にレポートを XML 形式ではなく HTML 形式で出力できるという記述を見かけました。stackoverflow で gradle での設定方法を探したところ How to generate HTML output using Gradle FindBugs Plugin という QA を見つけましたので、これで試してみたいと思います。
build.gradle を リンク先のその3の内容 に変更します。
変更後、Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。
clean タスク → Rebuild Project → build タスク の順に実行します。
build/reports/findbugs ディレクトリの下に main.html が生成されました。
main.html を開くと警告が4件あることが分かります。html だと結果が分かりやすいです。
ただしこの HTML 形式のレポートファイルですが、meta タグで文字コードが出力されていないため Chrome で開くと文字化けしますね。上のは IE で開いています。自動で meta タグを追加する方法は探してみましたが見つからなかったので、IE で開くことにします。
出力された4件は以下の内容でした。種類としては2種類のようです。確認してみます。
... は、static 内部クラスにすべきです。
メッセージ通り static の付け忘れでした。修正します。
- src/main/java/ksbysample/webapp/lending/web/booklist の下の RegisterBooklistForm.java を リンク先の内容 に変更します。
... への無効な代入です。
メッセージが出力されたのはデータロックのために実行している部分の処理で、確かに変数へ代入するだけで使用していません。ただし削除する訳にもいかないので、FindBugs の フィルターファイル を作成してチェックの対象にならないようにします。
config/findbugs の下に findbugs-exclude.xml を作成し、リンク先の内容 の内容を記載します。
build.gradle を リンク先のその4の内容 に変更します。
再び build タスクを実行し何も警告が出なくなりました
再び clean タスク → Rebuild Project → build タスク の順に実行します。
今度は
FindBugs rule violations were found.
のメッセージが出ずに “BUILD SUCCESSFUL” の文字が出力されました。findbugs もこのまま入れておくことにします。また XML 形式のレポートファイルについて調べてみたところ、Jenkins で実行する際に Plugin で警告数を取得する時には HTML 形式ではなく XML 形式で出力する必要があるようです。覚えておきます。
CheckStyle-IDEA Plugin を導入してみる
CheckStyle-IDEA Plugin をインストールする
IntelliJ IDEA のメインメニューから「File」-「Settings…」を選択します。
「Settings」ダイアログが表示されます。画面左側のリストから「Plugins」を選択した後、画面中央下の「Browse repositories…」ボタンをクリックします。
「Browse Repositories」ダイアログが表示されます。画面左上の検索フィールドに “checkstyle” と入力すると「CheckStyle-IDEA」が表示されますので、選択して「Install」ボタンをクリックします。
プラグインがダウンロードされて「Install」ボタンが「Restart IntelliJ IDEA」ボタンに切り替わりますのでクリックします。
「Settings」ダイアログに戻りますので「OK」ボタンをクリックします。
「Platform and Plugin Updates」ダイアログが表示されますので「Restart」ボタンをクリックします。
IntelliJ IDEA が再起動します。再起動しただけではまだ設定が不足しているので設定します。
再度 IntelliJ IDEA のメインメニューから「File」-「Settings…」を選択します。
「Settings」ダイアログが表示されます。画面左側のリストから「Other Settings」-「Checkstyle」を選択した後、画面右側の「Configuration File」の「+」ボタンをクリックします。
設定用のダイアログが表示されますので、以下の値を入力した後「Next」ボタンをクリックします。
- 「Description」に
google_checks.xml
と入力します。 - 「Use a local Checkstyle file」の下の「File」に
C:\project-springboot\ksbysample-webapp-lending\config\checkstyle\google_checks.xml
と入力します。 - 「Store relative to project location」をチェックします。
次の画面が表示されたら「Finish」ボタンをクリックします。
- 「Description」に
「Settings」ダイアログに戻ります。「Configuration File」に登録した google_checks.xml が表示されていますので、選択してから左側のチェックボックスをチェックします。チェック後「OK」ボタンをクリックしてダイアログを閉じます。
以上でインストール、設定は完了です。
Plugin から checkstyle を実行する
CheckStyle-IDEA Plugin をインストールすると IntelliJ IDEA のメイン画面の左下に「CheckStyle」メニューが表示されます。
「CheckStyle」メニューをクリックします。
CheckStyle-IDEA Plugin の画面が表示されます。画面左側の「Check Project」ボタンをクリックします。
チェックが実行された後、結果が表示されます。今は gradle で checkstyle を実行して警告を取り除いているので何も引っかかりませんでした。
以前 checkstyle で指摘を受けた箇所を元に戻して「Check Project」ボタンをクリックしてみます。
以下のようにファイルとメッセージが表示されました。
使ってみた感想としては、
- 表示されているメッセージをダブルクリックするとソースの該当箇所にジャンプします。gradle で checkstyle を実行した時には警告が出た箇所を表示するのにちょっと面倒だったので、これはかなり便利です。
- 画面左側に緑の矢印が表示されていますが、これは「Check Current File」ボタンで現在表示しているファイルだけチェックできます。
- 同じく画面左側の下矢印があるボタンですが、これは「Autoscroll to Source」ボタンでクリックした状態にしておけばメッセージを選択するとすぐにソースの該当箇所が表示されます。たぶんクリックしておいた方が便利だと思います。
CheckStyle-IDEA Plugin の方がチェック&修正がやりやすいので、gradle の checkstyle は最終チェックとして実行することにして、通常は CheckStyle-IDEA Plugin を使った方が便利ですね。
FindBugs-IDEA Plugin を導入してみる
FindBugs-IDEA Plugin をインストールする
IntelliJ IDEA のメインメニューから「File」-「Settings…」を選択します。
「Settings」ダイアログが表示されます。画面左側のリストから「Plugins」を選択した後、画面中央下の「Browse repositories…」ボタンをクリックします。
「Browse Repositories」ダイアログが表示されます。画面左上の検索フィールドに “findbugs” と入力すると「FindBugs-IDEA」が表示されますので、選択して「Install」ボタンをクリックします。
プラグインがダウンロードされて「Install」ボタンが「Restart IntelliJ IDEA」ボタンに切り替わりますのでクリックします。
「Settings」ダイアログに戻りますので「OK」ボタンをクリックします。
「Platform and Plugin Updates」ダイアログが表示されますので「Restart」ボタンをクリックします。
IntelliJ IDEA が再起動します。FindBugs-IDEA Plugin はこれだけでも使用可能ですが、gradle に findbugs を設定した時に作成した findbugs-exclude.xml を設定します。
再度 IntelliJ IDEA のメインメニューから「File」-「Settings…」を選択します。
「Settings」ダイアログが表示されます。画面左側のリストから「Other Settings」-「FindBugs-IDEA」を選択した後、画面右側で「Filter」タブをクリックして中央の「Exclude filter files」の「+」ボタンをクリックします。
「Exclude Filter Files」ダイアログが表示されますので、
C:\project-springboot\ksbysample-webapp-lending\config\findbugs\findbugs-exclude.xml
を選択して「OK」ボタンをクリックします。「Settings」ダイアログに戻ります。「OK」ボタンをクリックしてダイアログを閉じます。
以上でインストール、設定は完了です。
Plugin から FindBugs を実行する
FindBugs-IDEA Plugin をインストールすると IntelliJ IDEA のメイン画面の左下に「FindBugs-IDEA」メニューが表示されます。
「FindBugs-IDEA」メニューをクリックします。
FindBugs-IDEA Plugin の画面が表示されます。画面左側の「Analyze Project Files」ボタンをクリックします。
「Test Sources」ダイアログが表示されます。テストは FindBugs の対象には入れないので「No」ボタンをクリックします。
チェックが実行された後、結果が表示されます。今は gradle で findbugs を実行して警告を取り除いているので何も引っかかりませんでした。
以前 findbugs で指摘を受けた箇所を元に戻して「Analyze Project Files」ボタンをクリックしてみます。
以下のようにファイルとメッセージが表示されました。
使ってみた感想としては、
- 指摘を受けている部分、内容が分かりやすいです。gradle の findbugs が出力するレポートファイルと比較すると圧倒的にこちらの方がいいですね。
- 画面左側の下矢印がある「Autoscroll to Source」ボタンが最初からクリックされている状態になっており、Plugin の画面に表示されているファイル名をクリックするとエディタで対象ファイルが開き該当箇所にジャンプします。
- 画面左側の一番左上にある「Analyze Current File」ボタンを押せば CheckStyle-IDEA Plugin と同じく現在表示しているファイルだけチェックできます。
こちらも CheckStyle-IDEA Plugin と同様に gradle の findbugs は最終チェックとして実行することにして、通常は FindBugs-IDEA Plugin を使った方が便利ですね。
次回は。。。
1.4 系ではこう書くべきという点があるか確認し、変更した方がよいところを修正します。
最後に。後からコードチェックツールを導入してたくさん指摘を受けると結構堪えます。出来れば最初から導入しましょう。今回本当にそう思いました。。。
ソースコード
build.gradle
■その1
apply plugin: 'java' apply plugin: 'eclipse' apply plugin: 'idea' apply plugin: 'org.springframework.boot' apply plugin: 'io.spring.dependency-management' apply plugin: 'de.undercouch.download' apply plugin: 'groovy' apply plugin: 'net.ltgt.errorprone' apply plugin: 'checkstyle' .......... configurations { // for Doma 2 domaGenRuntime // for Error Prone ( http://errorprone.info/ ) errorprone { resolutionStrategy.force 'com.google.errorprone:error_prone_core:2.0.15' } } checkstyle { configFile = file("${rootProject.projectDir}/config/checkstyle/google_checks.xml") toolVersion = '7.5.1' sourceSets = [project.sourceSets.main] } repositories { jcenter() }
apply plugin: 'checkstyle'
を追加します。checkstyle { ... }
を追加します。checkstyle によるチェックは main だけとし、test は対象外にします ( JUnit の日本語のメソッド名等で警告が出たり、Javadoc がないので警告が出たりしますが、出ても対応しないからです )。
■その2
apply plugin: 'java' apply plugin: 'eclipse' apply plugin: 'idea' apply plugin: 'org.springframework.boot' apply plugin: 'io.spring.dependency-management' apply plugin: 'de.undercouch.download' apply plugin: 'groovy' apply plugin: 'net.ltgt.errorprone' apply plugin: 'checkstyle' apply plugin: 'findbugs' .......... findbugs { toolVersion = '3.0.1' sourceSets = [project.sourceSets.main] ignoreFailures = true effort = "max" } repositories { jcenter() }
apply plugin: 'findbugs'
を追加します。findbugs { ... }
を追加します。
■その3
findbugs { toolVersion = '3.0.1' sourceSets = [project.sourceSets.main] ignoreFailures = true effort = "max" } tasks.withType(FindBugs) { reports { xml.enabled = false html.enabled = true } } repositories { jcenter() }
tasks.withType(FindBugs) { ... }
を追加します。
■その4
findbugs { toolVersion = '3.0.1' sourceSets = [project.sourceSets.main] ignoreFailures = true effort = "max" excludeFilter = file("${rootProject.projectDir}/config/findbugs/findbugs-exclude.xml") }
excludeFilter = file("${rootProject.projectDir}/config/findbugs/findbugs-exclude.xml")
を追加します。
RegisterBooklistForm.java
public class RegisterBooklistForm { .......... @Data public static class RegisterBooklistRow {
public class RegisterBooklistRow {
→public static class RegisterBooklistRow {
へ変更します。
findbugs-exclude.xml
<?xml version="1.0" encoding="UTF-8"?> <FindBugsFilter> <Match> <Class name="ksbysample.webapp.lending.web.lendingapp.LendingappService"/> <Bug pattern="DLS_DEAD_LOCAL_STORE"/> </Match> </FindBugsFilter>
履歴
2017/02/19
初版発行。
2017/03/23
* Plugin の名前を間違えていました。CheckStyle-IDE, FindBugs-IDE → CheckStyle-IDEA, FindBugs-IDEA へ修正しました。