かんがるーさんの日記

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

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

概要

記事一覧はこちらです。

Spring Boot 1.3.x の Web アプリを 1.4.x へバージョンアップする ( その6 )( 「Run ‘All Tests’ with Coverage」実行時のエラーを解消する+build タスク実行時の警告を解消する ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • GoogleJava コンパイル時バグチェックツール? ( 静的解析ツールではないらしい ) Error Prone の導入方法・使い方を調べて、ksbysample-webapp-lending に導入するとどのような結果が出るのかを試してみます。
    • IntelliJ IDEA の Error Prone の Plugin もあるようなのでインストールして試してみます。

参照したサイト・書籍

  1. google/error-prone
    https://github.com/google/error-prone

  2. tbroyer/gradle-errorprone-plugin
    https://github.com/tbroyer/gradle-errorprone-plugin

  3. error-prone/examples/gradle/build.gradle
    https://github.com/google/error-prone/blob/master/examples/gradle/build.gradle

    • build.gradle に Error Prone を導入する時のサンプルです。
  4. How To Find Bugs, Part 1: A Minimal Bug Detector
    https://www.lmax.com/blog/staff-blogs/2016/04/01/find-bugs-part-1-minimal-bug-detector/

  5. guava - warning: Cannot find annotation method - Warnings as errors causes builds to fail
    https://github.com/robolectric/robolectric/issues/2446

  6. Adding the gradle-errorprone-plugin causes “bad path element” warnings, need to add “-Xlint:-path” to compilerArgs
    https://github.com/tbroyer/gradle-errorprone-plugin/issues/15

目次

  1. Error Prone を導入してみる
    1. Error Prone とは?
    2. build.gradle を変更する
    3. build タスクを実行する
    4. ClassNewInstance の警告を修正する
    5. MissingOverride の警告を修正する
    6. GetClassOnClass のエラーを修正する
    7. 再び build タスクを実行するも、なぜかまだエラーが。。。
    8. MissingOverride の警告を修正する
    9. 警告: タイプ'GuardedBy'内に注釈メソッド'value()'が見つかりません: javax.annotation.concurrent.GuardedByのクラス・ファイルが見つかりません の警告を修正する
    10. Finally の警告を修正する
    11. GetClassOnAnnotation のエラーを修正する
    12. 再び build タスクを実行するもまだエラーが出ます
    13. 警告: [path] 不正なパス要素"C:\project-springboot\ksbysample-webapp-lending\build\resources\main": そのファイルまたはディレクトリはありません の警告を修正する
    14. BoxedPrimitiveConstructor の警告を修正する
    15. MissingOverride の警告を修正する
    16. 再び build タスクを実行し、やっと警告もエラーも出なくなりました
  2. Error-prone Compiler Integration Plugin を導入してみる
    1. Error-prone Compiler Integration Plugin をインストールする
    2. Rebuild Project を実行する
    3. TypeParameterUnusedInFormals の警告を修正する
  3. 次回は。。。

手順

Error Prone を導入してみる

Error Prone とは?

  • Google の バグチェックツール。
  • バージョン 2.0.6 以降は Java 1.8 以降でしか動作しません。
  • ソースコードを静的解析するのではなく、Java コンパイラの機能で抽象構文ツリー (Abstract Syntax Tree、AST) にしてチェックするらしいです。
  • 検出できるバグの種類は FindBugs と比較すると少ないですが、plugin を作成して追加できます。
  • 検出できるバグの一覧は Bug patterns のページに記載されています。詳細ページには説明以外にサンプルコードも書かれており、内容が分かりやすいです。
  • FindBugs 3.0.1 では検出できない Files.lines(...) の try-with-resources 構文使用漏れが検出できます。ちなみに FindBugs で検出させるための記事が How To Find Bugs, Part 1: A Minimal Bug Detector にありました。
  • ClassNewInstance を見ると “deprecation in JDK 9” という記述も。JDK 9 も見据えて使用すべきではないコードも検出できるようです。

build.gradle を変更する

  1. error-prone/examples/gradle/build.gradle を参考にして build.gradle を リンク先のその1の内容 に変更します。

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

build タスクを実行する

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

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

    f:id:ksby:20170217013643p:plain

    その後コンパイルエラーが出力されて、最後に “BUILD FAILED” が出力されました。結構ありますね。。。

    f:id:ksby:20170217015028p:plain f:id:ksby:20170217015152p:plain

    ソースファイルと行数、エラーか警告か、エラーの種類 ( [ClassNewInstance] 等 )、エラーと判定されたソースの位置が出力されます。

    またエラーの詳細は各エラー毎に出力されている (see http://errorprone.info/bugpattern/...) のリンクをクリックすると Web ページが表示されて確認できます。かなり分かりやすいです。

    出力されたエラーはエラー1個、警告9個で、種類は以下の3種類でした。

ClassNewInstance の警告を修正する

Class#newInstance は問題があって JDK 9 から非推奨になるらしいので修正します。

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

  2. src/main/java/ksbysample/webapp/lending/util/cookie の下の CookieUtils.javaリンク先の内容 に変更します。

MissingOverride の警告を修正する

Values オブジェクトはかなりトリッキーなことをしているので @SuppressWarnings("MissingOverride") を付けて警告を回避します。

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

  2. src/main/java/ksbysample/webapp/lending/values/lendingbook の下の LendingBookApprovalResultValues.javaリンク先の内容 に変更します。

  3. src/main/java/ksbysample/webapp/lending/values/lendingbook の下の LendingBookLendingAppFlgValues.javaリンク先の内容 に変更します。

GetClassOnClass のエラーを修正する

こちらは単純なミスでした。

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

再び build タスクを実行するも、なぜかまだエラーが。。。

  1. これでエラーが全て解消されたはずなので、再び clean タスク → Rebuild Project → build タスク の順に実行します。

    が、なぜかまた大量にエラーが出ました。エラーの数が一定数を超えるとそれ以上のエラーは出なくなるのでしょうか?

    f:id:ksby:20170217233647p:plain

    出力されたエラーはエラー3個、警告12個で、種類は以下の4種類でした。

MissingOverride の警告を修正する

単純な @Override つけ忘れでした。

  1. src/main/java/ksbysample/common/test/helper の下の SimpleRequestBuilder.javaリンク先の内容 に変更します。

警告: タイプ'GuardedBy'内に注釈メソッド'value()'が見つかりません: javax.annotation.concurrent.GuardedByのクラス・ファイルが見つかりません の警告を修正する

Web で検索したら guava - warning: Cannot find annotation method - Warnings as errors causes builds to fail という GitHub の Issue が見つかりました。testCompile 'com.google.code.findbugs:jsr305:1.3.9' を付ければ警告が消えると書いてありますので、同じように対応します。。

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

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

Finally の警告を修正する

finally 内で throw していることが原因による警告です。テスト用のクラスでエラーを検出する方を優先したいので @SuppressWarnings("Finally") を付けて警告が出ないようにします。

  1. src/test/java/ksbysample/common/test/rule/db の下の TestDataResource.javaリンク先の内容 に変更します。

GetClassOnAnnotation のエラーを修正する

getClass() ではなく annotationType() を使え、というエラーでした。annotationType() は知りませんでしたね。

  1. src/test/java/ksbysample/common/test/rule/db の下の TestSqlExecutor.javaリンク先の内容 に変更します。

再び build タスクを実行するもまだエラーが出ます

  1. 出ていたエラーを解消したので、再び clean タスク → Rebuild Project → build タスク の順に実行します。

    が、まだエラーが出ますね。見た感じ、軽微そうな警告のみになってきました。

    f:id:ksby:20170218023841p:plain

  2. 出力されたエラーは警告6個で、種類は以下の3種類でした。最初の “[path] 不正なパス要素…” は最初から出ていたのですが、"(see http://errorprone.info/bugpattern/…)“ が出力されていなかったのでスルーしていました。今回はこちらも修正します。

警告: [path] 不正なパス要素"C:\project-springboot\ksbysample-webapp-lending\build\resources\main": そのファイルまたはディレクトリはありません の警告を修正する

Web で検索したら Adding the gradle-errorprone-plugin causes “bad path element” warnings, need to add “-Xlint:-path” to compilerArgs という GitHub の Issue が見つかりました。Java コンパイル時の -Xlint オプションに -path を付ければよいようです。

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

BoxedPrimitiveConstructor の警告を修正する

new Long(...)JDK 9 から非推奨になるので Long.valueOf(...) を使った方がよいという警告です。

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

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

MissingOverride の警告を修正する

こちらは前と同じでした。@SuppressWarnings("MissingOverride") を付けて警告を回避します。

  1. src/test/java/ksbysample/webapp/lending/values/validation の下の ValuesEnumValidatorTest.javaリンク先の内容 に変更します。

再び build タスクを実行し、やっと警告もエラーも出なくなりました

  1. 出ていたエラーを解消したので、再び clean タスク → Rebuild Project → build タスク の順に実行します。

    今度は1つも警告、エラーが出ずに “BUILD SUCCESSFUL” の文字が出力されました。FindBugs より検出できるバグは少ないと聞いていましたが、結構検出されましたね。。。 また JDK 9 から非推奨になる部分に警告が出て、修正内容も Bug patterns のページで分かるのはかなり良さそうな感触でした。

    f:id:ksby:20170218082334p:plain

    Error Prone は個人的にはかなり気に入りましたので、今後は積極的に入れていきたいと思います。

Error-prone Compiler Integration Plugin を導入してみる

Error-prone Compiler Integration Plugin をインストールする

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

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

    f:id:ksby:20170218095447p:plain

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

    f:id:ksby:20170218095954p:plain

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

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

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

    f:id:ksby:20170218100155p:plain

  5. IntelliJ IDEA が再起動します。再起動しただけではまだ有効になっていません。設定を変更します。

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

  7. 「Settings」ダイアログが表示されます。画面左上の検索フィールドに “java compiler” と入力した後、画面左側のリストから「Java Compiler」を選択して、画面中央上の「Use compiler」で “Javac” → “Javac with error-prone” へ変更します。変更後「OK」ボタンをクリックしてダイアログを閉じます。

    f:id:ksby:20170218101044p:plain

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

Rebuild Project を実行する

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

    gradle からのコンパイル時エラーは全て解消したので大丈夫だろうと思っていたら、1件だけ警告が出ました。

    f:id:ksby:20170218103157p:plain

    出た警告は以下のものでした。

TypeParameterUnusedInFormals の警告を修正する

警告が出たのは src/main/java/ksbysample/webapp/lending/webapi/common の下の CommonWebApiResponse.java で、

@Data
public class CommonWebApiResponse<T> {

    private int errcode = 0;

    private String errmsg = "";

    private T content;

}

という書き方だと private T content; の T は型チェックが機能しないらしいです。きちんと型チェックを機能させるためには以下のように書くべきらしいですが、このクラスは Jackson が JSON に自動変換する際に使用するためのもので以下の実装にしても public <T> T getContent(Class<T> clazz) メソッドは呼び出されないので、@SuppressWarnings("TypeParameterUnusedInFormals") を付加して回避することにします。

@Data
public class CommonWebApiResponse<T> {

    private int errcode = 0;

    private String errmsg = "";

    private T content;

    public <T> T getContent(Class<T> clazz) {
        return clazz.cast(this.content);
    }

}
  1. src/main/java/ksbysample/webapp/lending/webapi/common の下の CommonWebApiResponse.javaリンク先の内容 に変更します。

  2. IntelliJ IDEA のメインメニューから「Build」-「Rebuild Project」を選択して実行すると今度はエラー、警告は1件も出ませんでした。

次回は。。。

コードチェックツールに興味が湧いたので、IntelliJ IDEA の Plugin である CheckStyle-IDEA, FindBugs-IDEA の導入、及び build.gradle への checkstyle の導入をしてみます。

ソースコード

build.gradle

■その1

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.8")
        // 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'

..........

configurations {
    // for Doma 2
    domaGenRuntime

    // for Error Prone ( http://errorprone.info/ )
    errorprone {
        resolutionStrategy.force 'com.google.errorprone:error_prone_core:2.0.15'
    }
}

repositories {
    jcenter()
}
  • buildscript の以下の点を変更します。
    • repositories に maven { url "https://plugins.gradle.org/m2/" } を追加します。
    • dependencies に classpath("net.ltgt.gradle:gradle-errorprone-plugin:0.0.8") を追加します。
  • apply plugin: 'net.ltgt.errorprone' を追加します。
  • configurations に errorprone { resolutionStrategy.force 'com.google.errorprone:error_prone_core:2.0.15' } を追加します。

■その2

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

    // dependency-management-plugin によりバージョン番号が自動で設定されないもの、あるいは最新バージョンを指定したいもの
    ..........
    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")
  • testCompile("com.google.code.findbugs:jsr305:3.0.1") を追加します。

■その3

[compileJava, compileTestGroovy, compileTestJava]*.options*.compilerArgs = ['-Xlint:all,-options,-processing,-path']
  • ,-path を追加します。

DomaConfig.java

@Component
public class DomaConfig implements Config {

    ..........

    @Autowired
    public void setDialect(@Value("${doma.dialect}") String domaDialect)
            throws ClassNotFoundException, IllegalAccessException, InstantiationException
            , NoSuchMethodException, InvocationTargetException {
        this.dialect = (Dialect) Class.forName(domaDialect).getConstructor().newInstance();
    }
  • Class.forName(domaDialect).newInstance();Class.forName(domaDialect).getConstructor().newInstance(); に変更します。
  • setDialect メソッドの throws に , NoSuchMethodException, InvocationTargetException を追加します。

CookieUtils.java

public class CookieUtils {

    public static <T extends CookieGenerator> void addCookie(Class<T> clazz, HttpServletResponse response, String cookieValue) {
        try {
            T cookieGenerator = clazz.getConstructor().newInstance();
            cookieGenerator.addCookie(response, cookieValue);
        } catch (InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) {
            throw new RuntimeException(e);
        }
    }

    public static <T extends CookieGenerator> void removeCookie(Class<T> clazz, HttpServletResponse response) {
        try {
            T cookieGenerator = clazz.getConstructor().newInstance();
            cookieGenerator.removeCookie(response);
        } catch (InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) {
            throw new RuntimeException(e);
        }
    }
  • addCookie, removeCookie メソッド内の以下の点を変更します。
    • clazz.newInstance();clazz.getConstructor().newInstance(); へ変更します。
    • catch に列挙する例外に | NoSuchMethodException | InvocationTargetException を追加します。

LendingAppStatusValues.java

@SuppressWarnings("MissingOverride")
@Getter
@AllArgsConstructor
public enum LendingAppStatusValues implements Values {
  • @SuppressWarnings("MissingOverride") を追加します。

LendingBookApprovalResultValues.java

@SuppressWarnings("MissingOverride")
@Getter
@AllArgsConstructor
public enum LendingBookApprovalResultValues implements Values {
  • @SuppressWarnings("MissingOverride") を追加します。

LendingBookLendingAppFlgValues.java

@SuppressWarnings("MissingOverride")
@Getter
@AllArgsConstructor
public enum LendingBookLendingAppFlgValues implements Values {
  • @SuppressWarnings("MissingOverride") を追加します。

ValuesEnumValidator.java

public class ValuesEnumValidator implements ConstraintValidator<ValuesEnum, String> {

    ..........

    @Override
    public void initialize(ValuesEnum constraintAnnotation) {
        this.enumClass = constraintAnnotation.enumClass();
        this.allowEmpty = constraintAnnotation.allowEmpty();

        // enumClass 属性に Values インターフェースを実装していない列挙型が指定されている場合にはエラーにする
        try {
            if (!Values.class.isAssignableFrom(Class.forName(this.enumClass.getName()))) {
                throw new RuntimeException(
                        MessageFormat.format("enumClass 属性に Values インターフェースを実装した列挙型が指定されていません ( {0} )"
                                , this.enumClass.getName()));
            }
        } catch (ClassNotFoundException e) {
            throw new RuntimeException(e);
        }
    }
  • MessageFormat.format(...)this.enumClass.getClass()this.enumClass.getName() に変更します。

SimpleRequestBuilder.java

public class SimpleRequestBuilder implements RequestBuilder {

    private final MockHttpServletRequest request;

    public SimpleRequestBuilder(MockHttpServletRequest request) {
        this.request = request;
    }

    @Override
    public MockHttpServletRequest buildRequest(ServletContext servletContext) {
        return request;
    }

}
  • buildRequest メソッドに @Override を付加します。

TestDataResource.java

@Component
public class TestDataResource extends TestWatcher {

    ..........

    @SuppressWarnings("Finally")
    @Override
    protected void finished(Description description) {
  • finished メソッドに @SuppressWarnings("Finally") を付加します。

TestSqlExecutor.java

public class TestSqlExecutor<L extends Annotation, I extends Annotation> {

    ..........

    @SuppressWarnings("unchecked")
    private I[] value(L testSqlList) {
        try {
            Method method = testSqlList.annotationType().getMethod("value");
            return (I[]) method.invoke(testSqlList);
        } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
            throw new RuntimeException(e);
        }
    }

    private long order(I testSql) {
        try {
            Method method = testSql.annotationType().getMethod("order");
            return (long) method.invoke(testSql);
        } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
            throw new RuntimeException(e);
        }
    }

    private String sql(I testSql) {
        try {
            Method method = testSql.annotationType().getMethod("sql");
            return (String) method.invoke(testSql);
        } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
            throw new RuntimeException(e);
        }
    }

}
  • value メソッド内の testSqlList.getClass().getMethod("value");testSqlList.annotationType().getMethod("value"); へ変更しました。
  • order メソッド内の testSql.getClass().getMethod("order");testSql.annotationType().getMethod("order"); へ変更しました。
  • sql メソッド内の testSql.getClass().getMethod("sql");testSql.annotationType().getMethod("sql"); へ変更しました。

InquiringStatusOfBookQueueServiceTest.java

public class InquiringStatusOfBookQueueServiceTest {

    ..........

    @Test
    public void testSendMessage() throws Exception {
        RabbitAdmin rabbitAdmin = new RabbitAdmin(connectionFactory);
        rabbitAdmin.deleteQueue(Constant.QUEUE_NAME_INQUIRING_STATUSOFBOOK);
        rabbitAdmin.declareQueue(queue);

        Long lendingAppId = Long.valueOf(1L);
        inquiringStatusOfBookQueueService.sendMessage(lendingAppId);

        InquiringStatusOfBookQueueMessage message
                = (InquiringStatusOfBookQueueMessage) rabbitTemplate.receiveAndConvert(Constant.QUEUE_NAME_INQUIRING_STATUSOFBOOK);
        assertThat(message.getLendingAppId()).isEqualTo(lendingAppId);
    }
}
  • testSendMessage メソッド内の new Long(1);Long.valueOf(1L); へ変更します。

BooklistServiceTest.java

public class BooklistServiceTest {

    ..........

    @Test
    public void testTemporarySaveBookListCsvFile() throws Exception {
        new Expectations(LendingUserDetailsHelper.class) {{
            LendingUserDetailsHelper.getLoginUserId(); result = Long.valueOf(1L);
        }};

        ..........

        Long lendingAppId = booklistService.temporarySaveBookListCsvFile(uploadBooklistForm);
        assertThat(lendingAppId).isNotEqualTo(Long.valueOf(0L));
        IDataSet dataSet = new CsvDataSet(new File("src/test/resources/ksbysample/webapp/lending/web/booklist/assertdata/001"));
  • testTemporarySaveBookListCsvFile メソッド内の以下の点を変更します。
    • new Long(1);Long.valueOf(1L); へ変更します。
    • new Long(0);Long.valueOf(0L); へ変更します。

ValuesEnumValidatorTest.java

public class ValuesEnumValidatorTest {

    // テスト用 Value 列挙型
    @SuppressWarnings("MissingOverride")
    @Getter
    @AllArgsConstructor
    private enum TestValues implements Values {
        FIRST("1", "1番目")
        , SECOND("2", "2番目")
        , THIRD("3", "3番目");

        private final String value;
        private final String text;
    }
  • TestValues 列挙型に @SuppressWarnings("MissingOverride") を付加します。

CommonWebApiResponse.java

package ksbysample.webapp.lending.webapi.common;

import lombok.Data;

@SuppressWarnings("TypeParameterUnusedInFormals")
@Data
public class CommonWebApiResponse<T> {

    private int errcode = 0;

    private String errmsg = "";

    private T content;

}
  • @SuppressWarnings("TypeParameterUnusedInFormals") を追加します。

履歴

2017/02/18
初版発行。