かんがるーさんの日記

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

Spring Boot + Spring Integration でいろいろ試してみる ( その19 )( Flow の途中で一時的に別の Flow を実行したいなら wireTap! )

概要

記事一覧はこちらです。

参照したサイト・書籍

  1. 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

目次

  1. wireTap になぜ気づいたのか?
  2. in, out ディレクトリを作成する
  3. ksbysample-eipapp-wiretap プロジェクトを作成する
  4. サンプルの Flow を作成する
  5. 動作確認
  6. 最後に

手順

wireTap になぜ気づいたのか?

  • まず Spring Integration Reference Manual を初めに読んだ時には全く頭に残っていませんでした。
  • Spring Integration DSL に興味を持つようになって、DSL が書いてあるところはざっと見直したのですが、Wire TapDSL のサンプルは以下のようなコードで、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 版で購入。
    Apache Camel Developer's Cookbook (Solve Common Integration Tasks With Over 100 Easily Accessible Apache Camel Recipes)

    Apache Camel Developer's Cookbook (Solve Common Integration Tasks With Over 100 Easily Accessible Apache Camel Recipes)

    Mastering Apache Camel

    Mastering Apache Camel

  • 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 プロジェクトを作成する

  1. IntelliJ IDEA で Gradle プロジェクトを作成し、build.gradle を リンク先の内容 に変更します。

  2. ksbysample-eipapp-wiretap プロジェクトのルート直下に config/checkstyle, config/findbugs ディレクトリを作成します。

  3. config/checkstyle の下に ksbysample-eipapp-messaginggateway プロジェクトの config/checkstyle の下にある google_checks.xml をコピーします。

  4. config/findbugs の下に findbugs-exclude.xml を新規作成し、リンク先の内容 の内容に変更します。

  5. src/main/java の下に ksbysample.eipapp.wiretap パッケージを作成します。

  6. src/main/java/ksbysample/eipapp/wiretap の下に Application.java を作成し、リンク先の内容 を記述します。

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

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

サンプルの Flow を作成する

  1. サンプルの Flow は以下の仕様で作成します。

    • /in ディレクトリにファイルがあるかチェックします。
    • ファイルの内容をメールします。
    • /out ディレクトリに To, Subject, メール本文を出力したファイルを作成します。
    • /out ディレクトリに作成したファイルを SFTPサーバにアップロードします。
  2. 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
  1. smtp4dev, freeFTPd を起動します。

    f:id:ksby:20170311231446p:plain f:id:ksby:20170311231600p:plain

  2. ZipkinServer を起動します。今回は起動するだけです。

  3. bootRun を実行して ksbysample-eipapp-wiretap を起動します。

  4. C:\eipapp\ksbysample-eipapp-wiretap\in の下に 2014.txt を置きます。

    f:id:ksby:20170311233054p:plain

  5. SFTP サーバに 2014.txt がアップロードされて /in ディレクトリからはファイルが削除されます。

    f:id:ksby:20170311233527p:plain

    /out ディレクトリにもファイルは残っていません。

    f:id:ksby:20170311233743p:plain

  6. メールも届いており、ファイルの中身がメール本文になっています。

    f:id:ksby:20170311234105p:plain f:id:ksby:20170311234216p:plain f:id:ksby:20170311234311p:plain

  7. SFTP サーバにアップロードされた 2014.txt は以下の内容です。メールで送信した To, Subject が出力されています。

    f:id:ksby:20170311234551p:plain

  8. 起動したサーバと 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 へバージョンアップ。。。できませんでした )

概要

記事一覧はこちらです。

Spring Boot 1.3.x の Web アプリを 1.4.x へバージョンアップする ( その10 )( インジェクションの方法を @Autowired によるフィールドインジェクション → コンストラクタインジェクションへ変更する ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • 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 へのバージョンアップはできなかったというお話です。

参照したサイト・書籍

目次

  1. build.gradle を変更する
  2. clean タスク → Rebuild Project → build タスクを実行する
  3. java.lang.NoSuchMethodError: com.google.common.base.Preconditions.checkArgument(ZLjava/lang/String;Ljava/lang/Object;)V の原因は?
  4. build.gradle の resolutionStrategy.force で Guava の 21.0 を強制してみる
  5. なぜ Guava のバージョンが 17.0 にバージョンダウンされるのか?
  6. BOM で指定されているライブラリのバージョンを変更する
  7. java.lang.ClassCastException: lombok.javac.apt.Javac7BaseFileObjectWrapper cannot be cast to javax.tools.FileObject の原因は?
  8. 結局、どう対応するのか?
  9. 最後に

手順

build.gradle を変更する

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

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

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

clean タスク → Rebuild Project → build タスクを実行します。。。が、compileJava でエラーが出て止まりました。

f:id:ksby:20170305073102p:plain

ログを見た感じでは com.google.common.base.Preconditions.checkArgument(ZLjava/lang/String;Ljava/lang/Object;)V が問題のような気がしますが、これだけではエラーの内容がよく分かりません。

コマンドプロンプトを開いて、プロジェクトのルートディレクトリに移動した 後 gradlew --stackstrace --debug build コマンドを実行してみます。

f:id:ksby:20170305073640p:plain

以下のログが出力されました。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 です。

f:id:ksby:20170307001538p:plain

checkArgument を呼び出している com.google.errorprone.BugCheckerInfo.create(BugCheckerInfo.java:98) を見てみましたが、どう見ても引数を3つ渡しています。エラーメッセージが出る意味がよく分かりません。。。

f:id:ksby:20170307001830p:plain

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 タスクを実行してみましたが、結果は同じでした。

f:id:ksby:20170305082427p:plain

効果がなかったので 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 でした。

f:id:ksby:20170305140121p:plain

また 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 propertybomProperty で変更できることが書いてありました。

http://repo.spring.io/repo/io/spring/platform/platform-bom/Athens-SR3/platform-bom-Athens-SR3.pom を見ると <guava.version>17.0</guava.version> という記述がありましたので、guava.version21.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 になりました。

f:id:ksby:20170305145738p:plain f:id:ksby:20170305145845p:plain f:id:ksby:20170305145954p:plain

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.BaseFileObjectjavac が赤字で表示されています。com.sun.tools.javac.file.BaseFileObject クラスが含まれているライブラリが gradle により自動でロードされていないためのようです。

f:id:ksby:20170305175249p:plain

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.BaseFileObjectjava の赤字は解消されていましたが、今度は class の行に赤波線が表示されていました。また LombokFileObject が赤字です。

f:id:ksby:20170305201258p:plain

IntelliJ IDEA の画面上部のパッケージ階層表示から lombok.javac.apt の下にある一覧を表示させてみましたが LombokFileObject は存在します。中のソースも見てみましたが、特におかしいところは見られませんでした。

f:id:ksby:20170305201607p:plain

Project Tool Window で org.projectlombok:lombok:1.16.12 の内容を表示させてみたところ、LombokFileObject は LombokFileObject.SCL.lombok というファイル名になっていてインターフェースとして表示されません。

f:id:ksby:20170305201926p:plain

通常は以下のようにインターフェースのアイコンが表示されます。

f:id:ksby:20170305202452p:plain

この状態で clean タスク → Rebuild Project → build タスクを実行しても以前と同じエラーが出ます。

f:id:ksby:20170305203000p:plain f:id:ksby:20170305203115p:plain

どうすればよいのか分からなくなったので Web でいろいろ検索してみたところ、以下の GitHub の Issue が見つかりました。

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.guava:guava ( bomProperty で 21.0 を指定します )
    • org.apache.commons:commons-lang3 ( bomProperty で 3.5 を指定します )
  • 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” が出力されました。

f:id:ksby:20170306020126p:plain

最後に

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 系からコンストラクタインジェクションが主流になったと聞いたので、コンストラクタインジェクションに変更します。

参照したサイト・書籍

  1. Spring 4.3 DIコンテナ関連の主な変更点
    http://qiita.com/kazuki43zoo/items/172d132ff8f4ba098888

  2. SpringでField InjectionよりConstructor Injectionが推奨される理由
    http://pppurple.hatenablog.com/entry/2016/12/29/233141

    • なぜコンストラクタインジェクションがよいのか、についてはこちらの記事が分かりやすかったです。
  3. Does JUnit4 testclasses require a public no arg constructor?
    http://stackoverflow.com/questions/1451496/does-junit4-testclasses-require-a-public-no-arg-constructor

目次

  1. コンストラクタインジェクションって面倒なのでは? と思ったが IntelliJ IDEA がきちんと補完してくれる!
  2. コンストラクタインジェクションに変更する
  3. clean タスク → Rebuild Project → build タスクを実行してみる
  4. Mail001Helper, Mail002Helper, Mail003Helper の3クラス全てに同じような修正をしたのに Mail003Helper だけ checkstyle のチェックで警告が出る理由は?
  5. 次回は。。。

手順

コンストラクタインジェクションって面倒なのでは? と思ったが IntelliJ IDEA がきちんと補完してくれる!

コンストラクタインジェクションが主流になったと聞いた時は、フィールドに @Autowired アノテーションを付けるだけと比較して、わざわざコンストラクタに引数を追加して、かつコンストラクタ内でフィールドにセットするのは書くのが面倒そうだなと思ったのですが、さすがは IntelliJ IDEA、サポートは万全でした。

例えばクラス内に final が付いたフィールドを記述すると、以下の画像のようにフィールドの下に赤波線が表示されます。

f:id:ksby:20170301014351p:plain

赤波線が表示されたフィールドにカーソルを移動して Alt+Enter を押すとコンテキストメニューが表示されます。

f:id:ksby:20170301014604p:plain

「Add constructor parameter」を選択するとコンストラクタが自動生成されて、コンストラクタインジェクションの処理が記述されます。

f:id:ksby:20170301020240p:plain

この補完はフィールドの出現順通りにコンストラクタの引数にセットしてくれます。例えば先程記述したフィールドの上に別のフィールドを記述して、

f:id:ksby:20170301021218p:plain

Alt+Enter を押して「Add constructor parameter」を選択すると、

f:id:ksby:20170301021325p:plain

コンストラクタの引数も先程記述された引数の前に追加されます。コンストラクタ内の記述もフィールドと同じ順に記述されます。

f:id:ksby:20170301021445p:plain

Javadoc も自動的に補完してくれます。コンストラクタに Javadoc を記述した後、記述済のフィールドの間に別のフィールドを記述します。

f:id:ksby:20170301021832p:plain

Alt+Enter を押して「Add constructor parameter」を選択すると、

f:id:ksby:20170301022006p:plain

引数やコンストラクタ内の記述の位置を中間にしてくれるだけでなく、Javadoc の記述の位置も中間になります。

f:id:ksby:20170301022109p:plain

また最初にフィールドだけ全て書いて、後からコンストラクタを生成することもできます。まずフィールドだけ書きます。

f:id:ksby:20170301023734p:plain

赤波線にカーソルを移動してから Alt+Enter を押します。コンテキストメニューが表示されますので、「Add constructor parameters」を選択します。複数だと parameter ではなく parameters になるとは。。。

f:id:ksby:20170304002702p:plain

「Choose Fields to Generate Constructor Parameters for」ダイアログが表示されます。デフォルトではフィールドは1つしか選択されていませんので、全て選択してから「OK」ボタンをクリックします。

f:id:ksby:20170304003032p:plain

コンストラクタが生成されます。

f:id:ksby:20170304003146p:plain

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 がダウンロードされています。

f:id:ksby:20170304005232p:plain

その後で checkstyle の JavadocMethod の警告が出ています。追加したコンストラクタに Javadoc のコメントを付けていないからでしょう。でも警告が出力されたクラスを見ると Mail001Helper, Mail002Helper, Mail003Helper の3クラスに同じような修正をしたにも関わらず Mail003Helper しか出ていないのが気になりました。

f:id:ksby:20170304005629p:plain

あとテストが1つ失敗して “BUILD FAILED” の文字が出力されました。

f:id:ksby:20170304005743p:plain

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 クラスも警告されなくなりました。

f:id:ksby:20170304222035p:plain

<property name="minLineCount" value="1"/> に変更すると Mail001Helper, Mail002Helper, Mail003Helper の全てのクラスが警告されます。

f:id:ksby:20170304222346p:plain

さて、どうするかですが、

  • 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 )

概要

記事一覧はこちらです。

参照したサイト・書籍

目次

  1. FTP アップロード処理にリトライ処理を追加する
  2. ErrorChannel の処理を作成する
  3. 動作確認
    1. 事前準備
    2. 1ファイルを SFTP サーバの /in ディレクトリに置いてみる
    3. 2ファイルを SFTP サーバの /in ディレクトリに置いてみる
    4. FTP サーバを停止して errorChannel に Message が送信されるようにしてみる
  4. メモ書き
    1. .handle(Mail.outboundAdapter(...).~).handleWithAdapter(a -> a.mail(...).~) は同じです&使い方の説明や感想など
    2. .handle(Mail.outboundAdapter(...).~) のメール送信処理は spring.mail.~ の設定が反映されない。。。と思って調べたら MailSendingMessageHandler って何?

手順

FTP アップロード処理にリトライ処理を追加する

前回作成した FTP サーバにアップロードする処理にリトライ処理を入れるのを忘れていたので、追加します。

  1. 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 です。

  1. まず throw された例外の stacktrace をログに出力されるのと同じ適宜インデントされたフォーマットの文字列で取得したいので、Guava の Throwables#getStackTraceAsString を使用できるようにします。build.gradle を リンク先の文字列 に変更します。

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

  2. src/main/java/ksbysample/eipapp/messaginggateway の下の FlowConfig.javaリンク先のその2の内容 に変更します。

動作確認

事前準備

まずは clean タスク → Rebuild Project → build タスクを実行して正常終了することを確認します。

f:id:ksby:20170226105416p:plain

次に Xlight FTP Server, FreeFTPd, smtp4dev, zipkin を起動します。zipkin は最新の zipkin-server-1.20.1-exec.jar をダウンロードしています。

f:id:ksby:20170226101713p:plain f:id:ksby:20170226101803p:plain f:id:ksby:20170226101843p:plain f:id:ksby:20170226105630p:plain

起動していて 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 で以下の内容です。

これはテストです。
今日は晴れています。

f:id:ksby:20170226104932p:plain

SFTP サーバからファイルがダウンロードされて処理が行われ、FTP サーバの recv01 ユーザの /out ディレクトリにファイルがアップロードされました。

f:id:ksby:20170226110035p:plain

アップロードされた testdata01.txt の内容は以下のようになっていました。文字コードUTF-8、改行コードは CRLF です。

To: download@test.co.jp
Subject: testdata01.txt

これはテストです。
今日は晴れています。

IntelliJ IDEA のコンソールに出力されたログは以下のようになっており、特にエラーは出ていません。

f:id:ksby:20170226110420p:plain

smtp4dev に送信されたメールは以下の内容でした。こちらも特に問題ありませんでした。

f:id:ksby:20170226110520p:plain f:id:ksby:20170226111029p:plain f:id:ksby:20170226111120p:plain

Zipkin で処理状況を見ると以下のようになっていました。

f:id:ksby:20170226111659p:plain f:id:ksby:20170226111805p:plain

2ファイルを SFTP サーバの /in ディレクトリに置いてみる

次は testdata01.txt, testdata02.txt の2ファイルを SFTP サーバの send01 ユーザの /in ディレクトリに置いてみます。testdata02.txt の内容は以下のものです。

緊急警報です。

明日は、
雨ですね。

f:id:ksby:20170226112605p:plain

2ファイルとも SFTP サーバからファイルがダウンロードされて処理が行われ、FTP サーバの recv01 ユーザの /out ディレクトリにファイルがアップロードされました。

f:id:ksby:20170226112817p:plain

アップロードされた 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 発行されています。

f:id:ksby:20170226113148p:plain

smtp4dev に送信されたメールは以下の内容でした。こちらも特に問題ありませんでした。

f:id:ksby:20170226113402p:plain f:id:ksby:20170226113454p:plain f:id:ksby:20170226113551p:plain

FTP サーバを停止して errorChannel に Message が送信されるようにしてみる

Xlight FTP Server を停止します。

f:id:ksby:20170226114000p:plain

testdata01.txt を SFTP サーバの send01 ユーザの /in ディレクトリに置きます。

今度は FTP アップロードできなかったため、C:\eipapp\ksbysample-eipapp-messaginggateway\send ディレクトリにファイルが残ったままになりました。

f:id:ksby:20170226114237p:plain

IntelliJ IDEA のコンソールに出力されたログは以下のようになっていました。例外が throw されています。

f:id:ksby:20170226114502p:plain

smtp4dev に送信されたメールは以下の内容でした。今回は「エラーが発生しました」メールが送信されており、送信されたメールの本文を確認すると例外の内容が出力されています。

f:id:ksby:20170226115052p:plain f:id:ksby:20170226115306p:plain

想定通りの動作になっていたので、動作確認を終わります。

メモ書き

.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 が一覧表示されるので使いやすいと思います。

f:id:ksby:20170226142909p:plain

使い方ですが、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());
        }

    }

}

f:id:ksby:20170226200224p:plain f:id:ksby:20170226200325p:plain

メール送信には 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 を使用します。

参照したサイト・書籍

  1. Spring Integration Reference Manual - 8.4 Messaging Gateways
    http://docs.spring.io/spring-integration/reference/html/messaging-endpoints-chapter.html#gateway

  2. How 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 にアクセスする方法を調べた時に参考にしました。

目次

  1. @MessagingGateway とは?
  2. recv, send ディレクトリを作成する
  3. ksbysample-eipapp-messaginggateway プロジェクトを作成する
  4. メール送信用の共通処理を作成する
  5. SFTP サーバからファイルをダウンロードする処理を作成する
  6. ファイルの内容をメール送信する処理を作成する
  7. メールの内容をファイルに出力する処理を作成する
  8. FTP サーバにアップロードする処理を作成する
  9. 続く。。。

手順

@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 プロジェクトを作成する

  1. IntelliJ IDEA で Gradle プロジェクトを作成し、build.gradle を リンク先の内容 に変更します。

  2. ksbysample-eipapp-messaginggateway プロジェクトのルート直下に config/checkstyle, config/findbugs ディレクトリを作成します。

  3. config/checkstyle の下に Spring Boot 1.3.x の Web アプリを 1.4.x へバージョンアップする ( その8 )( build.gradle への checkstyle, findbugs の導入+CheckStyle-IDEA, FindBugs-IDEA Plugin の導入 ) で作成した google_checks.xml を置きます。

  4. config/findbugs の下に findbugs-exclude.xml を新規作成し、リンク先の内容 の内容に変更します。

  5. src/main/java の下に ksbysample.eipapp.messaginggateway パッケージを作成します。

  6. src/main/java/ksbysample/eipapp/messaginggateway の下に Application.java を作成し、リンク先の内容 を記述します。

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

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

メール送信用の共通処理を作成する

  1. src/main/java/ksbysample/eipapp/messaginggateway の下に MailHelperConfig.java を新規作成し、リンク先の内容 を記述します。

SFTP サーバからファイルをダウンロードする処理を作成する

  1. src/main/java/ksbysample/eipapp/messaginggateway の下に FlowConfig.java を新規作成し、リンク先のその1の内容 を記述します。

ファイルの内容をメール送信する処理を作成する

  1. src/main/java/ksbysample/eipapp/messaginggateway の下の FlowConfig.javaリンク先のその2の内容 に変更します。

メールの内容をファイルに出力する処理を作成する

  1. src/main/java/ksbysample/eipapp/messaginggateway の下の FlowConfig.javaリンク先のその3の内容 に変更します。

FTP サーバにアップロードする処理を作成する

  1. 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系で実装方法が変更された点を修正する )

概要

記事一覧はこちらです。

Spring Boot 1.3.x の Web アプリを 1.4.x へバージョンアップする ( その8 )( build.gradle への checkstyle, findbugs の導入+CheckStyle-IDEA, FindBugs-IDEA Plugin の導入 ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • 1.3系 → 1.4系で実装方法が変更された点を修正します。
    • 今回は変更が軽微なものだけで、フィールドへの @Autowired 付加 → コンストラクタインジェクションへの変更、及びテストクラスのアノテーションの変更は次回以降にやります。

参照したサイト・書籍

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

  2. Spring Boot 1.4+でRestTemplate(HTTPクライアント)を使う
    http://qiita.com/kazuki43zoo/items/7cf3c8ca4f6f2283cefb

目次

  1. spring-boot-starter で名前が変更されたものを反映する
  2. application.properties に spring.session.store-type の設定を追加する
  3. application.properties の spring.datasource の項目名を変更する
  4. @RequestMapping を変更可能なところは @GetMapping, @PostMapping に変更する
  5. RestTemplate オブジェクトを生成する処理を new RestTemplate(...)RestTemplateBuilder#build へ変更する
  6. 次回は。。。

手順

spring-boot-starter で名前が変更されたものを反映する

1.4 から spring-boot-starter-redisspring-boot-starter-data-redis へ変更されたので反映します。

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

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

  3. Project Tool Window の External Libraries を見ると spring-boot-starter-redis が消えて spring-boot-starter-data-redis が入ったことが確認できます。

    f:id:ksby:20170220230718p:plain

application.properties に spring.session.store-type の設定を追加する

Spring Session のデータ保存先を application.properties で設定できるようになったので、その設定を追加します。

  1. application.properties を リンク先の内容 に変更します。

    設定する時に IntelliJ IDEA で候補を表示させてみたのですが、hash_mapnone といった選択肢もありました。一時的に Spring Session を試したいだけならサーバを立てずに hash_map を選択するのもありなのかもしれません。none は何だろう?と思ったら、39. Spring Session に Spring Session を無効化したい時に設定するよう記述がありました。

    f:id:ksby:20170220231055p:plain

application.properties の spring.datasource の項目名を変更する

使用しているコネクションプーリングのライブラリに応じた namespace が増えました。Spring Boot では特に指定をしていなければ Tomcat JDBC Connection Pool が使用されるので、spring.datasourcespring.datasource.tomcat へ変更します。

Appendix A. Common application properties には「spring.datasource.tomcat.*= # Tomcat datasource specific settings」としか記述がありませんが、IntelliJ IDEA で設定項目を表示させてみたところ、以下の項目が表示されました。

f:id:ksby:20170221001910p:plain f:id:ksby:20170221002008p:plain

知らない設定がいろいろ表示されています。次回以降で設定しておいた方がよいものがあるのか調べてみたいと思います。Tomcat JDBC Connection Pool の URL もメモしておきます。

The Tomcat JDBC Connection Pool
https://tomcat.apache.org/tomcat-8.5-doc/jdbc-pool.html

  1. application-develop.properties, application-product.properties, application-unittest.properties を リンク先の内容 に変更します。

  2. 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("...") にのみ変更することにします。

  1. 以下のクラスのメソッドで @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 クラスを利用する方法に変更します。

  1. MappingJackson2XmlHttpMessageConverter を生成する部分は Bean にします。src/main/java/ksbysample/webapp/lending/config の下の ApplicationConfig.javaリンク先のその2の内容 に変更します。

  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-redisspring-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.datasourcespring.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 でセットしていた処理を削除しました。

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 の導入 )

概要

記事一覧はこちらです。

Spring Boot 1.3.x の Web アプリを 1.4.x へバージョンアップする ( その7 )( Google の Java コンパイル時バグチェックツール? Error Prone を試してみる ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • 前回 Error Prone を導入してみてコードチェックツールはいれた方が良さそうに感じたので、有名な checkstyleFindBugs を入れてみます。
    • 最初に build.gradle に checkstyle, findbugs を導入してみます。
    • その後で IntelliJ IDEA の Plugin である CheckStyle-IDEA, FindBugs-IDEA を導入してみます。

参照したサイト・書籍

  1. Google’s Java Style の CheckStyle を使う
    http://create-something.hatenadiary.jp/entry/2015/01/31/121332

  2. checkstyle/src/main/resources/google_checks.xml
    https://github.com/checkstyle/checkstyle/blob/master/src/main/resources/google_checks.xml

  3. Google Java Style Guide
    https://google.github.io/styleguide/javaguide.html

  4. checkstyle
    http://checkstyle.sourceforge.net/

  5. How can I disable checkstyle rules for tests in gradle?
    http://stackoverflow.com/questions/32772872/how-can-i-disable-checkstyle-rules-for-tests-in-gradle

  6. FindBugsExtension http://gradle.monochromeroad.com/docs/dsl/org.gradle.api.plugins.quality.FindBugsExtension.html

    • build.gradle へ findbugs を導入する時の設定はこちらを参照しました。
  7. JavaのバグをFindBugsで見つける
    https://internetcom.jp/developer/20081017/26.html

  8. Jenkins+GradleでJavaのCIのための基本build.gradle設定 (JUnit,PMD,FindBugs,CPD,JaCoCo)
    http://qiita.com/mychaelstyle/items/74baa62b7bf2fe81e309

  9. How to generate HTML output using Gradle FindBugs Plugin
    http://stackoverflow.com/questions/15406469/how-to-generate-html-output-using-gradle-findbugs-plugin

  10. 第8章 フィルターファイル
    http://findbugs.sourceforge.net/ja/manual/filter.html

目次

  1. build.gradle に checkstyle を導入してみる
    1. Google Java Style の checkstyle 用XMLファイル google_checks.xml をダウンロードする
    2. build.gradle を変更する
    3. build タスクを実行する
    4. のための間違った辞書式順序 '...' インポート。 ... [CustomImportOrder]のインポート文 '...' は、間違った順序です。 ... [CustomImportOrder]
    5. 行が 100 文字を超えています。 [LineLength]
    6. インデント階層 ... の ... が正しいインデント ... にありません [Indentation]
    7. Javadoc コメントがありません。 [JavadocMethod]
    8. ',' は前の行にあるべきです。 [SeparatorWrap]
    9. Javadocのの??最初の文は、(期間が欠落している)、または存在しない不完全です。 [SummaryJavadoc]
    10. アット節非空の記述を持っている必要があります。 [NonEmptyAtclauseDescription]
    11. 'METHOD_DEF' 前の文から分離する必要があります。 [EmptyLineSeparator]
    12. オーバーロードメソッドは、分割すべきではありません。 ... [OverloadMethodsDeclarationOrder]
    13. 再び build タスクを実行するも、まだエラーが出ます
    14. '.*' 形式のインポートの使用は避けるべきです ... [AvoidStarImport]
    15. 名前に略語は '...' 以下にする必要があります ... [AbbreviationAsWordInName]
    16. Member name '...' must match pattern '^[a-z][a-z0-9][a-zA-Z0-9]*$'. [MemberName]
    17. '}' 欄で ... はマルチブロック文を直接複数のブロックが含まれています ... [RightCurly]
    18. 変数 '...' の宣言と、その変数の使用開始位置までの距離が 7 です。許可された距離 は 3 です。... [VariableDeclarationUsageDistance]
    19. 再び build タスクを実行し警告もエラーも出なくなりました
  2. build.gradle に findbugs を導入してみる
    1. build.gradle を変更する
    2. build タスクを実行する
    3. レポートの出力形式を xml ではなく html に変更する
    4. ... は、static 内部クラスにすべきです。
    5. ... への無効な代入です。
    6. 再び build タスクを実行し何も警告が出なくなりました
  3. CheckStyle-IDEA Plugin を導入してみる
    1. CheckStyle-IDEA Plugin をインストールする
    2. Plugin から checkstyle を実行する
  4. FindBugs-IDEA Plugin を導入してみる
    1. FindBugs-IDEA Plugin をインストールする
    2. Plugin から FindBugs を実行する
  5. 次回は。。。

手順

build.gradle に checkstyle を導入してみる

Google Java Style の checkstyleXMLファイル google_checks.xml をダウンロードする

Google Java Style 用の chekstyle の XMLファイルがダウンロードできますので、それをベースに自分向けの変更を加えて導入してみます。

  1. プロジェクトのルートから config/checkstyle ディレクトリを作成します。

  2. checkstyle/src/main/resources/google_checks.xml から google_checks.xml をダウンロードし、config/checkstyle の下に配置します。

build.gradle を変更する

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

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

build タスクを実行する

  1. clean タスク → Rebuild Project → build タスク の順に実行します。

    build タスクが実行されるといろいろダウンロードされます。

    f:id:ksby:20170218142430p:plain

    その後に大量の WARN メッセージが出力されました。

    f:id:ksby:20170218142614p:plain

    出力されているのは以下の 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 文字の位置に引かれています。

f:id:ksby:20170218175459p:plain

この設定を変える気はありませんので、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 等の @ から始まるタグの説明が空の場合に警告を出すルールでした。

ルールとしては入れておきたいので checkstyleXMLファイルはこのままにしますが、今からソースの 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” のように大文字続きの名称にしていたためでした。CSVCsv にように全てスネークケースになるよう変更します。

この警告については 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 タスクを実行し警告もエラーも出なくなりました

  1. 再び clean タスク → Rebuild Project → build タスク の順に実行します。

    今度は1つも警告、エラーが出ずに “BUILD SUCCESSFUL” の文字が出力されました。

    f:id:ksby:20170219133342p:plain

    checkstyle も Error Prone 同様に修正した方がよいと思われる箇所を結構いろいろ指摘してくれるので気に入りました。こちらも今後は積極的に入れていきたいと思います。

build.gradle に findbugs を導入してみる

build.gradle を変更する

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

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

build タスクを実行する

  1. clean タスク → Rebuild Project → build タスク の順に実行します。

    build タスクが実行されるといろいろダウンロードされます。

    f:id:ksby:20170219164640p:plain

    build タスク自体は “BUILD SUCCESSFUL” のメッセージが出て終了しましたが、FindBugs が何か検知して FindBugs rule violations were found. のメッセージも出力されていました。

    f:id:ksby:20170219164802p:plain

    デフォルトの設定では build/reports/findbugs ディレクトリにレポートの XML ファイルが生成されます。生成されたレポートファイル main.xml を開いてみたのですが、Error Prone や checkstyle のメッセージと比較するとすごく見にくいですね。findbugs を導入している人って毎回このレポートを見ているのでしょうか。。。?

    f:id:ksby:20170219155249p:plain

    さすがに毎回このレポートファイルを見るのはちょっとつらそうな気がする&絶対見やすくする方法があるはず、と思ったので調べてみます。

レポートの出力形式を xml ではなく html に変更する

古い記事ですが JavaのバグをFindBugsで見つける にレポートを XML 形式ではなく HTML 形式で出力できるという記述を見かけました。stackoverflow で gradle での設定方法を探したところ How to generate HTML output using Gradle FindBugs Plugin という QA を見つけましたので、これで試してみたいと思います。

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

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

  3. clean タスク → Rebuild Project → build タスク の順に実行します。

    build/reports/findbugs ディレクトリの下に main.html が生成されました。

    f:id:ksby:20170219170247p:plain

    main.html を開くと警告が4件あることが分かります。html だと結果が分かりやすいです。

    f:id:ksby:20170219170546p:plain

    ただしこの HTML 形式のレポートファイルですが、meta タグで文字コードが出力されていないため Chrome で開くと文字化けしますね。上のは IE で開いています。自動で meta タグを追加する方法は探してみましたが見つからなかったので、IE で開くことにします。

    f:id:ksby:20170219172404p:plain

    出力された4件は以下の内容でした。種類としては2種類のようです。確認してみます。

    f:id:ksby:20170219174747p:plain

... は、static 内部クラスにすべきです。

メッセージ通り static の付け忘れでした。修正します。

  1. src/main/java/ksbysample/webapp/lending/web/booklist の下の RegisterBooklistForm.javaリンク先の内容 に変更します。

... への無効な代入です。

メッセージが出力されたのはデータロックのために実行している部分の処理で、確かに変数へ代入するだけで使用していません。ただし削除する訳にもいかないので、FindBugsフィルターファイル を作成してチェックの対象にならないようにします。

  1. config の下に findbugs ディレクトリを作成します。

  2. config/findbugs の下に findbugs-exclude.xml を作成し、リンク先の内容 の内容を記載します。

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

再び build タスクを実行し何も警告が出なくなりました

  1. 再び clean タスク → Rebuild Project → build タスク の順に実行します。

    今度は FindBugs rule violations were found. のメッセージが出ずに “BUILD SUCCESSFUL” の文字が出力されました。

    f:id:ksby:20170219184747p:plain

    findbugs もこのまま入れておくことにします。また XML 形式のレポートファイルについて調べてみたところ、Jenkins で実行する際に Plugin で警告数を取得する時には HTML 形式ではなく XML 形式で出力する必要があるようです。覚えておきます。

CheckStyle-IDEA Plugin を導入してみる

CheckStyle-IDEA Plugin をインストールする

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

  2. 「Settings」ダイアログが表示されます。画面左側のリストから「Plugins」を選択した後、画面中央下の「Browse repositories…」ボタンをクリックします。

  3. 「Browse Repositories」ダイアログが表示されます。画面左上の検索フィールドに “checkstyle” と入力すると「CheckStyle-IDEA」が表示されますので、選択して「Install」ボタンをクリックします。

    f:id:ksby:20170219190804p:plain

    プラグインがダウンロードされて「Install」ボタンが「Restart IntelliJ IDEA」ボタンに切り替わりますのでクリックします。

  4. 「Settings」ダイアログに戻りますので「OK」ボタンをクリックします。

    「Platform and Plugin Updates」ダイアログが表示されますので「Restart」ボタンをクリックします。

  5. IntelliJ IDEA が再起動します。再起動しただけではまだ設定が不足しているので設定します。

  6. 再度 IntelliJ IDEA のメインメニューから「File」-「Settings…」を選択します。

  7. 「Settings」ダイアログが表示されます。画面左側のリストから「Other Settings」-「Checkstyle」を選択した後、画面右側の「Configuration File」の「+」ボタンをクリックします。

    f:id:ksby:20170219193225p:plain

  8. 設定用のダイアログが表示されますので、以下の値を入力した後「Next」ボタンをクリックします。

    f:id:ksby:20170219194829p:plain

    • 「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」ボタンをクリックします。

    f:id:ksby:20170219193752p:plain

  9. 「Settings」ダイアログに戻ります。「Configuration File」に登録した google_checks.xml が表示されていますので、選択してから左側のチェックボックスをチェックします。チェック後「OK」ボタンをクリックしてダイアログを閉じます。

    f:id:ksby:20170219193913p:plain

    以上でインストール、設定は完了です。

Plugin から checkstyle を実行する

  1. CheckStyle-IDEA Plugin をインストールすると IntelliJ IDEA のメイン画面の左下に「CheckStyle」メニューが表示されます。

    f:id:ksby:20170219195529p:plain

    CheckStyle」メニューをクリックします。

  2. CheckStyle-IDEA Plugin の画面が表示されます。画面左側の「Check Project」ボタンをクリックします。

    f:id:ksby:20170219195809p:plain

    チェックが実行された後、結果が表示されます。今は gradle で checkstyle を実行して警告を取り除いているので何も引っかかりませんでした。

    f:id:ksby:20170219200030p:plain

  3. 以前 checkstyle で指摘を受けた箇所を元に戻して「Check Project」ボタンをクリックしてみます。

    以下のようにファイルとメッセージが表示されました。

    f:id:ksby:20170219200729p:plain

    使ってみた感想としては、

    • 表示されているメッセージをダブルクリックするとソースの該当箇所にジャンプします。gradle で checkstyle を実行した時には警告が出た箇所を表示するのにちょっと面倒だったので、これはかなり便利です。
    • 画面左側に緑の矢印が表示されていますが、これは「Check Current File」ボタンで現在表示しているファイルだけチェックできます。
    • 同じく画面左側の下矢印があるボタンですが、これは「Autoscroll to Source」ボタンでクリックした状態にしておけばメッセージを選択するとすぐにソースの該当箇所が表示されます。たぶんクリックしておいた方が便利だと思います。

    CheckStyle-IDEA Plugin の方がチェック&修正がやりやすいので、gradle の checkstyle は最終チェックとして実行することにして、通常は CheckStyle-IDEA Plugin を使った方が便利ですね。

FindBugs-IDEA Plugin を導入してみる

FindBugs-IDEA Plugin をインストールする

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

  2. 「Settings」ダイアログが表示されます。画面左側のリストから「Plugins」を選択した後、画面中央下の「Browse repositories…」ボタンをクリックします。

  3. 「Browse Repositories」ダイアログが表示されます。画面左上の検索フィールドに “findbugs” と入力すると「FindBugs-IDEA」が表示されますので、選択して「Install」ボタンをクリックします。

    f:id:ksby:20170219210909p:plain

    プラグインがダウンロードされて「Install」ボタンが「Restart IntelliJ IDEA」ボタンに切り替わりますのでクリックします。

  4. 「Settings」ダイアログに戻りますので「OK」ボタンをクリックします。

    「Platform and Plugin Updates」ダイアログが表示されますので「Restart」ボタンをクリックします。

  5. IntelliJ IDEA が再起動します。FindBugs-IDEA Plugin はこれだけでも使用可能ですが、gradle に findbugs を設定した時に作成した findbugs-exclude.xml を設定します。

  6. 再度 IntelliJ IDEA のメインメニューから「File」-「Settings…」を選択します。

  7. 「Settings」ダイアログが表示されます。画面左側のリストから「Other Settings」-「FindBugs-IDEA」を選択した後、画面右側で「Filter」タブをクリックして中央の「Exclude filter files」の「+」ボタンをクリックします。

    f:id:ksby:20170219212045p:plain

  8. 「Exclude Filter Files」ダイアログが表示されますので、C:\project-springboot\ksbysample-webapp-lending\config\findbugs\findbugs-exclude.xml を選択して「OK」ボタンをクリックします。

    f:id:ksby:20170219212357p:plain

  9. 「Settings」ダイアログに戻ります。「OK」ボタンをクリックしてダイアログを閉じます。

    以上でインストール、設定は完了です。

Plugin から FindBugs を実行する

  1. FindBugs-IDEA Plugin をインストールすると IntelliJ IDEA のメイン画面の左下に「FindBugs-IDEA」メニューが表示されます。

    f:id:ksby:20170219213007p:plain

    FindBugs-IDEA」メニューをクリックします。

  2. FindBugs-IDEA Plugin の画面が表示されます。画面左側の「Analyze Project Files」ボタンをクリックします。

    f:id:ksby:20170219222850p:plain

    「Test Sources」ダイアログが表示されます。テストは FindBugs の対象には入れないので「No」ボタンをクリックします。

    f:id:ksby:20170219223146p:plain

    チェックが実行された後、結果が表示されます。今は gradle で findbugs を実行して警告を取り除いているので何も引っかかりませんでした。

    f:id:ksby:20170219223348p:plain

  3. 以前 findbugs で指摘を受けた箇所を元に戻して「Analyze Project Files」ボタンをクリックしてみます。

    以下のようにファイルとメッセージが表示されました。

    f:id:ksby:20170219223822p:plain

    使ってみた感想としては、

    • 指摘を受けている部分、内容が分かりやすいです。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-IDECheckStyle-IDEA, FindBugs-IDEA へ修正しました。