Spring Boot 1.3.x の Web アプリを 1.4.x へバージョンアップする ( その7 )( Google の Java コンパイル時バグチェックツール? Error Prone を試してみる )
概要
記事一覧はこちらです。
- 今回の手順で確認できるのは以下の内容です。
参照したサイト・書籍
google/error-prone
https://github.com/google/error-pronetbroyer/gradle-errorprone-plugin
https://github.com/tbroyer/gradle-errorprone-pluginerror-prone/examples/gradle/build.gradle
https://github.com/google/error-prone/blob/master/examples/gradle/build.gradle- build.gradle に Error Prone を導入する時のサンプルです。
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/guava - warning: Cannot find annotation method - Warnings as errors causes builds to fail
https://github.com/robolectric/robolectric/issues/2446Adding 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
目次
- Error Prone を導入してみる
- Error Prone とは?
- build.gradle を変更する
- build タスクを実行する
- ClassNewInstance の警告を修正する
- MissingOverride の警告を修正する
- GetClassOnClass のエラーを修正する
- 再び build タスクを実行するも、なぜかまだエラーが。。。
- MissingOverride の警告を修正する
警告: タイプ'GuardedBy'内に注釈メソッド'value()'が見つかりません: javax.annotation.concurrent.GuardedByのクラス・ファイルが見つかりません
の警告を修正する- Finally の警告を修正する
- GetClassOnAnnotation のエラーを修正する
- 再び build タスクを実行するもまだエラーが出ます
警告: [path] 不正なパス要素"C:\project-springboot\ksbysample-webapp-lending\build\resources\main": そのファイルまたはディレクトリはありません
の警告を修正する- BoxedPrimitiveConstructor の警告を修正する
- MissingOverride の警告を修正する
- 再び build タスクを実行し、やっと警告もエラーも出なくなりました
- Error-prone Compiler Integration Plugin を導入してみる
- 次回は。。。
手順
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 を変更する
error-prone/examples/gradle/build.gradle を参考にして build.gradle を リンク先のその1の内容 に変更します。
変更後、Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。
build タスクを実行する
clean タスク → Rebuild Project → build タスク の順に実行します。
build タスクが実行されるといろいろダウンロードされます。
その後コンパイルエラーが出力されて、最後に “BUILD FAILED” が出力されました。結構ありますね。。。
ソースファイルと行数、エラーか警告か、エラーの種類 ( [ClassNewInstance] 等 )、エラーと判定されたソースの位置が出力されます。
またエラーの詳細は各エラー毎に出力されている
(see http://errorprone.info/bugpattern/...)
のリンクをクリックすると Web ページが表示されて確認できます。かなり分かりやすいです。出力されたエラーはエラー1個、警告9個で、種類は以下の3種類でした。
ClassNewInstance の警告を修正する
Class#newInstance
は問題があって JDK 9 から非推奨になるらしいので修正します。
src/main/java/ksbysample/webapp/lending/config の下の DomaConfig.java を リンク先の内容 に変更します。
src/main/java/ksbysample/webapp/lending/util/cookie の下の CookieUtils.java を リンク先の内容 に変更します。
MissingOverride の警告を修正する
Values オブジェクトはかなりトリッキーなことをしているので @SuppressWarnings("MissingOverride")
を付けて警告を回避します。
src/main/java/ksbysample/webapp/lending/values/lendingapp の下の LendingAppStatusValues.java を リンク先の内容 に変更します。
src/main/java/ksbysample/webapp/lending/values/lendingbook の下の LendingBookApprovalResultValues.java を リンク先の内容 に変更します。
src/main/java/ksbysample/webapp/lending/values/lendingbook の下の LendingBookLendingAppFlgValues.java を リンク先の内容 に変更します。
GetClassOnClass のエラーを修正する
こちらは単純なミスでした。
- src/main/java/ksbysample/webapp/lending/values/validation の下の ValuesEnumValidator.java を リンク先の内容 に変更します。
再び build タスクを実行するも、なぜかまだエラーが。。。
これでエラーが全て解消されたはずなので、再び clean タスク → Rebuild Project → build タスク の順に実行します。
が、なぜかまた大量にエラーが出ました。エラーの数が一定数を超えるとそれ以上のエラーは出なくなるのでしょうか?
出力されたエラーはエラー3個、警告12個で、種類は以下の4種類でした。
- http://errorprone.info/bugpattern/MissingOverride
- 警告: タイプ'GuardedBy'内に注釈メソッド'value()‘が見つかりません: javax.annotation.concurrent.GuardedByのクラス・ファイルが見つかりません
- http://errorprone.info/bugpattern/Finally
- http://errorprone.info/bugpattern/GetClassOnAnnotation
MissingOverride の警告を修正する
単純な @Override つけ忘れでした。
警告: タイプ'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'
を付ければ警告が消えると書いてありますので、同じように対応します。。
build.gradle を リンク先のその2の内容 に変更します。
変更後、Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。
Finally の警告を修正する
finally 内で throw していることが原因による警告です。テスト用のクラスでエラーを検出する方を優先したいので @SuppressWarnings("Finally")
を付けて警告が出ないようにします。
GetClassOnAnnotation のエラーを修正する
getClass()
ではなく annotationType()
を使え、というエラーでした。annotationType()
は知りませんでしたね。
再び build タスクを実行するもまだエラーが出ます
出ていたエラーを解消したので、再び clean タスク → Rebuild Project → build タスク の順に実行します。
が、まだエラーが出ますね。見た感じ、軽微そうな警告のみになってきました。
出力されたエラーは警告6個で、種類は以下の3種類でした。最初の “[path] 不正なパス要素…” は最初から出ていたのですが、"(see http://errorprone.info/bugpattern/…)“ が出力されていなかったのでスルーしていました。今回はこちらも修正します。
- 警告: [path] 不正なパス要素"C:\project-springboot\ksbysample-webapp-lending\build\resources\main": そのファイルまたはディレクトリはありません
- http://errorprone.info/bugpattern/BoxedPrimitiveConstructor
- http://errorprone.info/bugpattern/MissingOverride
警告: [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 を付ければよいようです。
- build.gradle を リンク先のその3の内容 に変更します。
BoxedPrimitiveConstructor の警告を修正する
new Long(...)
は JDK 9 から非推奨になるので Long.valueOf(...)
を使った方がよいという警告です。
src/main/java/ksbysample/webapp/lending/service/queue の下の InquiringStatusOfBookQueueServiceTest.java を リンク先の内容 に変更します。
src/main/java/ksbysample/webapp/lending/web/booklist の下の BooklistServiceTest.java を リンク先の内容 に変更します。
MissingOverride の警告を修正する
こちらは前と同じでした。@SuppressWarnings("MissingOverride")
を付けて警告を回避します。
- src/test/java/ksbysample/webapp/lending/values/validation の下の ValuesEnumValidatorTest.java を リンク先の内容 に変更します。
再び build タスクを実行し、やっと警告もエラーも出なくなりました
出ていたエラーを解消したので、再び clean タスク → Rebuild Project → build タスク の順に実行します。
今度は1つも警告、エラーが出ずに “BUILD SUCCESSFUL” の文字が出力されました。FindBugs より検出できるバグは少ないと聞いていましたが、結構検出されましたね。。。 また JDK 9 から非推奨になる部分に警告が出て、修正内容も Bug patterns のページで分かるのはかなり良さそうな感触でした。
Error Prone は個人的にはかなり気に入りましたので、今後は積極的に入れていきたいと思います。
Error-prone Compiler Integration Plugin を導入してみる
Error-prone Compiler Integration Plugin をインストールする
IntelliJ IDEA のメインメニューから「File」-「Settings…」を選択します。
「Settings」ダイアログが表示されます。画面左側のリストから「Plugins」を選択した後、画面中央下の「Browse repositories…」ボタンをクリックします。
「Browse Repositories」ダイアログが表示されます。画面左上の検索フィールドに “Error-prone” と入力すると「Error-prone Compiler Integration」が表示されますので、選択して「Install」ボタンをクリックします。
プラグインがダウンロードされて「Install」ボタンが「Restart IntelliJ IDEA」ボタンに切り替わりますのでクリックします。
「Settings」ダイアログに戻りますので「OK」ボタンをクリックします。
「Platform and Plugin Updates」ダイアログが表示されますので「Restart」ボタンをクリックします。
IntelliJ IDEA が再起動します。再起動しただけではまだ有効になっていません。設定を変更します。
再度 IntelliJ IDEA のメインメニューから「File」-「Settings…」を選択します。
「Settings」ダイアログが表示されます。画面左上の検索フィールドに “java compiler” と入力した後、画面左側のリストから「Java Compiler」を選択して、画面中央上の「Use compiler」で “Javac” → “Javac with error-prone” へ変更します。変更後「OK」ボタンをクリックしてダイアログを閉じます。
以上でインストール、設定は完了です。
Rebuild Project を実行する
IntelliJ IDEA のメインメニューから「Build」-「Rebuild Project」を選択します。
gradle からのコンパイル時エラーは全て解消したので大丈夫だろうと思っていたら、1件だけ警告が出ました。
出た警告は以下のものでした。
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); } }
src/main/java/ksbysample/webapp/lending/webapi/common の下の CommonWebApiResponse.java を リンク先の内容 に変更します。
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")
を追加します。
- repositories に
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
初版発行。