Kengo's blog

Technical articles about original projects, JVM, Static Analysis and TypeScript.

SpotBugsプラグイン実装方法2017

過去にFindBugsプラグインの実装方法について記事にしたとおり、FindBugsプラグインの実装には複雑なハックが必要でした。特にfindbugs.xmlやmessages.xmlなどのメタデータ管理が煩雑でした。

これがSpotBugs 3.1.0-RC3ではある程度楽になっているので、シンプルになった方法をここにまとめておきます。

プロジェクトの雛形を作る

archetypeがコミュニティから提供されるようになりました。これを使えばプラグイン開発の知識がなくてもすぐにプロジェクトを作成できます。

github.com

使い方は他のmaven archetypeと同じで、archetype:generate実行時にarchetypeのgroupId, artifactIdそしてversionを指定するだけです。現状バージョン0.1.0が最新なので、以下のコマンドを実行してください:

$ mvn archetype:generate \
    -DarchetypeArtifactId=spotbugs-archetype \
    -DarchetypeGroupId=com.github.spotbugs \
    -DarchetypeVersion=0.1.0

バグとして検出すべきケースを実装し、単体テストを回す

プロジェクト生成に成功すると、src/test/javaに2つのクラスが見つかるはずです。

これらを実際に解析して結果を確認しているテストケースもあります。

このテストケースは、SpotBugsが出しているtest-harnessモジュールを使っています*1SpotBugsRuleというクラスがキモで、これを使って実際の.classファイルを解析し結果を確認できます。assertion用のMatcherクラスも提供されているので、hamcrestを使ったテストも書きやすいです。

     @Rule
     public SpotBugsRule spotbugs = new SpotBugsRule();
     @Test
     public void testIssuesAreFound() {
         Path path = Paths.get("target/classes/my/company/AnalyzedClass.class")
         BugCollection bugCollection = spotbugs.performAnalysis(path);

         // There should only be exactly 1 issue of this type
         final BugInstanceMatcher bugTypeMatcher = new BugInstanceMatcherBuilder()
                 .bugType("NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE").build();
         assertThat(bugCollection, containsExactly(bugTypeMatcher, 1));

         final BugInstanceMatcher bugInstanceMatcher = new BugInstanceMatcherBuilder()
                 .bugType("NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE")
                 .inClass("AnalyzedClass")
                 .atLine(25)
                 .build();
         assertThat(bugCollection, hasItem(bugInstanceMatcher));
     }

以前はプラグインを実際に使ったテストをするためにはfindbugs-maven-pluginを使ってインテグレーションテストを回すしか方法がありませんでしたが、この手法ならより高速にテストを回せます。また test-driven-detectors4findbugsと違って公式なので、今後のSpotBugsの更新にも追随できるはずです*2

メタデータを作成する

まずbugrank.txtは現状必要ないようです。無くてもプラグインはビルドできます。そもそもBugRankという概念はこの論文でやこのページ紹介されている通り、SonarQubeのSeverityのようなもので、priority (confidence)とは全く別の概念です。が、果たしてそれがどの程度浸透しているのか……。SpotBugsが使うpriorityやpriority (confidence)のことは忘れてSonarQubeのSeverityだけ使うのが、運用も回しやすいと思います。

ので、全体的な情報を提供するfindbugs.xmlと、ユーザ向けメッセージを管理するmessages.xmlだけ作成すれば十分です。プロジェクトの雛形にはこれらが既に作成されているので、それを参考にコピペで要素を増やしてください。

番外編:SonarQubeプラグインを作成する

SonarQubeプラグインsonar-packaging-maven-plugin を使ってパッケージできます。sonar-findbugsと拙作findbugs-slf4jが参考になります。

キモはfindbugs-pluginバージョン3.5への依存を宣言することです。このバージョンはSpotBugs 3.1.0-RC2を使っているため、SpotBugsに依存したプラグインを書くには必須です。

なおsonar-findbugsバージョン3.5はpre-releaseバージョンで、まだSonarQubeのUpdate Centerでは配布されていません。使用する際はGitHubのリリースページからjarを落としてきて手動インストールする必要があります。

またSonarQube用のrule.xmlを生成する必要があるのですが、これは雑ですがMavenプラグインを作ったので使ってもらえればと思います。Groovy好きな方はsonar-findbugsのコードを参照しても良いでしょう。

まとめ

以上でSpotBugsプラグインを実装してSonarQubeも含めた運用を回すことができるはずです。 まだ各種親Detectorを学習する手間は必要ですが、5年前に比べてだいぶマシにはなりました。

追記:公式かつ英語のドキュメントもできました。

*1:find-sec-bugsからコードを持ってきて作られたもの

*2:test-driven-detectors4findbugsはFindBugs 1.3.9〜2.0.2にしか対応していなかった

FindBugsのバグパターンをMavenでビルド&テストする

以前の記事FindBugsバグパターンの実装方法を試行錯誤している最中に作成したものでしたが、今回はその結果どのような開発手法に行き着いたかをまとめます。Mavenベースです。

前提

  • findbugs.xmlsrc/main/resourcesに置いてしまうと単体テスト実行時にエラーが発生してしまう(前回記事参照)
  • テストが面倒
  • Mavenプラグイン、Antタスク、SonarQubeなど様々な利用方法があり得る

拡張すべきクラスについては前回記事を参照してください。

ビルドのTips

メタファイルをどうpackageするか

findbugs.xmlsrc/main/resourcesに置いてしまうことによるエラーを回避するために、メタファイルをsrc/main/metaファイルに保存してprepare-packageフェーズにリソースに追加するという手法を取ります。プロファイルを利用することもなくシンプルな解決になります。

Mavenを実行するたびにcleanを実行しなければならないという制約は付きますが、Detector用プロジェクトは基本的に小さいものになるはずですので問題にはならないでしょう。プラグイン設定は以下のようになります。

      <plugin>
        <!--
          Copy meta files to default outputDirectory at "prepare-package" phase.

          Because of findbugs specification, an edu.umd.cs.findbugs.PluginDoesntContainMetadataException
          instance would be thrown at "test" phase if we put these meta files on '/src/main/resources'.
          We have to copy these meta files after testing.

          See "How to build FindBugs plugin with Maven" thread in FindBugs mailing list
          (findbugs-discuss@cs.umd.edu) to get detail and other solution.

          Note: You should execute "clean" phase before you execute "test" phase.
        -->
        <artifactId>maven-resources-plugin</artifactId>
        <executions>
          <execution>
            <phase>prepare-package</phase>
            <goals>
              <goal>copy-resources</goal>
            </goals>
            <configuration>
              <outputDirectory>${project.build.outputDirectory}</outputDirectory>
              <resources>
                <resource>
                  <directory>src/main/meta</directory>
                </resource>
              </resources>
            </configuration>
          </execution>
        </executions>
      </plugin>

依存ライブラリをパッケージする

findbugs-maven-pluginだけでなくSonarQubeなどでの利用も検討している場合、依存ライブラリも同梱してしまうと便利です。私はmaven-shade-pluginでこれを実現しています。

      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-shade-plugin</artifactId>
        <version>2.0</version>
        <executions>
          <execution>
            <phase>package</phase>
            <goals>
              <goal>shade</goal>
            </goals>
            <configuration>
              <createDependencyReducedPom>false</createDependencyReducedPom>
            </configuration>
          </execution>
        </executions>
      </plugin>

テストのTips

テストには大きく分けて2通りの方法を採用しています。FindBugs本体が提供する機構はドキュメントが無いため現時点では使わず、外部ライブラリに頼った手法のみを利用しています。

test-driven-detectors4findbugs による単体テスト

youdevise/test-driven-detectors4findbugs @ GitHubはDetectorをtest-drivenな開発手法で作成できる優れたライブラリです。FindBugs 2.0.3にはまだ対応できていませんが、多くの場合問題にはならないでしょう。

使い方は非常にシンプルで、テスト用のBugReporterをDetectorに渡してからassertXxxメソッドでバグが発見されるかどうかを調べるだけです。

// quoted from https://github.com/WorksApplications/findbugs-plugin/blob/1c1930cacd0fe2aed9b41633b9b72d6a9b50e07e/src/test/java/jp/co/worksap/oss/findbugs/jsr305/BrokenImmutableClassDetectorTest.java
public class BrokenImmutableClassDetectorTest {

    private BrokenImmutableClassDetector detector;
    private BugReporter bugReporter;

    @Before
    public void setup() {
        bugReporter = bugReporterForTesting();
        detector = new BrokenImmutableClassDetector(bugReporter);
    }

    @Test
    public void testEnumIsImmutable() throws Exception {
        assertNoBugsReported(When.class, detector, bugReporter);
    }

    @Test
    public void testMutableClass() throws Exception {
        assertBugReported(MutableClass.class, detector, bugReporter, ofType("IMMUTABLE_CLASS_SHOULD_BE_FINAL"));
        assertBugReported(MutableClass.class, detector, bugReporter, ofType("BROKEN_IMMUTABILITY"));
    }
}

この方法は簡単かつ充分なテストが行えるため、まず最初に導入したいところです。ただしこのライブラリはfindbugs.xmlのようなメタファイルは参照しないため、メタファイルと実装の不整合を見つけることはできません。実際に実行を試せる環境を別途用意するか、次に述べる統合テストを併用する必要があります。

なお依存関係は以下のようになります。

    <dependency>
      <groupId>com.google.code.findbugs</groupId>
      <artifactId>findbugs</artifactId>
      <version>2.0.1</version>
      <scope>provided</scope>
    </dependency>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.11</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.hamcrest</groupId>
      <artifactId>hamcrest-library</artifactId>
      <version>1.3</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>com.youdevise</groupId>
      <artifactId>test-driven-detectors4findbugs</artifactId>
      <version>0.2.1</version>
      <scope>test</scope>
    </dependency>

実際にFindBugsを実行した結果を解析

eller86/findbugs-slf4jで利用している方法です。Mavenのマルチモジュールを利用したやや複雑な構成です。

まずはDetector実装を含むサブモジュールを作成します。次にDetectorによる解析を行うためのクラスファイルをsrc/main/javaに持つサブモジュールを作成し、pre-integration-testフェーズでfindbugs-maven-pluginを実行することで、その結果をintegration-testフェーズで解析・照合することが可能になります。 解析用サブモジュールをDetectorサブモジュールにtestスコープで依存させることで、mvn clean install実行時にfindbugs-maven-pluginが常に新しい実装を利用することを保証できます。

この手法は実際にfindbugs-maven-pluginを利用するため、FindBugsプラグインが期待通りに動作すること、すなわちメタファイルの整合性も含めた動作保証が可能になります。XMLパーサを用意するなど実施コストは高くなりますが、この動作保証を手動で行う手間を考えるとペイすると言えるでしょう。

プラグイン設定は以下のようになります。src/test/javaにあるテストケースをintegration-testフェーズで実行するためにmaven-surefire-pluginに2つのexecutionを設定しています。

      <plugin>
        <groupId>org.codehaus.mojo</groupId>
        <artifactId>findbugs-maven-plugin</artifactId>
        <version>2.5.2</version>
        <executions>
          <execution>
            <phase>pre-integration-test</phase>
            <goals>
              <goal>findbugs</goal>
            </goals>
          </execution>
        </executions>
        <configuration>
          <threshold>Low</threshold>
          <plugins>
            <plugin>
              <groupId>my.groupId</groupId>
              <artifactId>my.detector</artifactId>
              <version>0.0.1-SNAPSHOT</version>
            </plugin>
          </plugins>
        </configuration>
      </plugin>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-surefire-plugin</artifactId>
        <version>2.12</version>
        <executions>
          <execution>
            <id>default-test</id>
            <phase>test</phase>
            <configuration>
              <skip>true</skip><!-- all test case should run at integration-test phase -->
            </configuration>
          </execution>
          <execution>
            <id>default-integration-test</id>
            <phase>integration-test</phase>
            <goals><goal>test</goal></goals>
          </execution>
        </executions>
      </plugin>

参考資料

My acts on the WWW in 2013

I will summarize my online actions in this year. Old articles are below:

personal products

I have released several personal products, and 4 of them are important to explain my action:

They are not web service. Only document, Maven plugin and Findbugs plugin I have released. I have learned a lot of technology to maintain web service, but I made no chance to use it in production environment.

Static analysis is the highest priority theme in this year, so I costed much time to implement findbugs-slf4j. But now I am facing a huge problem which is based on limitation of FindBugs.

I also costed time to learn how to improve productivity by build tool or other automation tools. Maven is the key tool in this area, but I think we need another tool to automate troublesome routine work. I will make a chance to explain about it next year.

Shanghai -> Singapore

I moved from Shanghai to Singapore at this summer. I think that Singapore isn't good place to work and live, because atmosphere and price aren't good. I hope that my life at here will be improved next year.

My work at these countries brought good opportunity and theme to think about team work, productivity and diversity. At team work, we should know that communication is the most important thing to build great team.

It was difficult to understand when I worked at Japan, because we have no doubt about our expectation: oral communication is "perfect" to tell opinion. At foreign country you cannot expect so, it is one of the goodness of office at foreign country.

Blog

I posted 49 entries and 6 of them got 10 or more bookmarks:

Yes, my blog is not famous for English user yet. Maybe it is better to join some event which is hosted by English users.

Page view in this year is 33,000 and 75.47% of them are new visitor. The frequently used search keywords are:

  • slf4j
  • Guava
  • BufferedOutputStream
  • java ファイル出力 高速化 (=File I/O acceleration)

Visitors searched about library like SLF4J and Guava. Currently there is no user who is looking for information about Maven, RequireJS and other tools. It is little strange... maybe I need to choose "correct" words to help user to reach to my blog.

Contribution

ウェブアプリケーションサーバでよくあるクラスローダのトラブル

これはJ2EE Advent Calendarの25日目の記事です。昨日の記事はnobuokaさんによるJava Persistence API (JPA) 実践入門でした!

本記事の趣旨は、developerWorksクラスローダーとJ2EEパッケージング戦略を理解するに書いてあることをコードで確かめようというものです。昨今はOSGiの登場などによりあまり目立たなくなったのかもしれませんが、未だにクラスローダはJ2EEアプリケーションの実装・運用において重要な役割を担っています。本記事がクラスローダの理解に役立てば幸いです。

はじめに:クラスローダとは?

クラスローダとは、クラス定義をclassファイルから読み込んでくれるものです。 通常のJavaアプリケーションではJVMが用意する複数の基本クラスローダが存在し、このクラスローダがJREのクラスとCLASSPATHにあるクラスを読み込んでくれています。各クラスローダは1つの親クラスローダを持っていて、自分でロードする前に親にロードを委譲したり、自分でクラスを見つけられなかったときに親に問い合せたりします。

エンタープライズウェブアプリケーションの場合は基本クラスローダだけではなく、アプリケーションサーバがEARごと・WARごとにクラスローダを作成しています*1。このため、アプリケーションごとに異なるライブラリを使用することが可能です。またWARのクラスローダはEARのクラスローダを親として持っているため、skinny WARと呼ばれる技法によりEARのファイルサイズを減らすこともできます。

クラスロードポリシー

クラスローダはクラスを読み込んでくれるものだと書きましたが、その探し方には2パターンあります。

1つ目は「親が最初」、つまりまずは親クラスローダに読み込みを依頼するというものです。親クラスローダが対象クラスを見つけた場合はそれを返し、見つけられなかった場合は自分が参照できるJARファイルやclassファイルからの発見を試みます。クラスローダのデフォルトの挙動はこの「親が最初」になっています。

a ClassLoader instance will delegate the search for the class or resource to its parent class loader before attempting to find the class or resource itself. http://docs.oracle.com/javase/7/docs/api/java/lang/ClassLoader.html

2つ目は「親が最後」です。まず自分でクラスを探し、見つからなかったときにだけ親に委譲します。GitHubに「親が最後」クラスローダの簡易実装を置いてあります

クラスのアンロード

クラスローダはロードしたクラスをメモリ上のパーマネント領域に記録します。ロードされたクラスはクラスローダおよびクラスの両方が使われなくなったときにアンロードされ、メモリが解放されます。

例えばアプリケーションサーバを起動したまま(JVMを動作させたまま)ウェブアプリケーションを再起動すると、かつて使っていたクラスローダとクラスはGCとアンロードによってメモリから消去されます。新しくクラスローダが作られ、新規にクラスがロードされていきます。

詳しく学ぶにはNAKAMURA MinoruさんのJava のクラスアンロード (Class Unloading)が参考になります。

事例集

以上を踏まえて、トラブルを見ていきましょう。

warやearにパッケージしたライブラリが使われない問題

クラスローダはデフォルトで「親が最初」ポリシーを利用するため、Java基本クラスローダなどの親クラスローダが同じ名前のクラスを持っていた場合、子クラスローダは自分が参照している場所からクラスを読み込むことがありません。 このため以下のテストケースがpassします。親も子も同じクラスを参照するため、各クラスローダから取ってきたシングルトンインスタンスが等しいことがわかります。

    @Test
    public void parentFirstHasNoMultiSingletonProblem() throws Exception {
        Object singletonInParent = findSingletonInstanceFrom(firstClassLoader);
        Object singletonInChild = findSingletonInstanceFrom(childOfFirstClassLoader);
        assertThat(singletonInChild, is(sameInstance(singletonInParent)));

        Object singletonInAnotherChild = findSingletonInstanceFrom(anotherChildOfFirstClassLoader);
        assertThat(singletonInAnotherChild, is(sameInstance(singletonInParent)));
        assertThat(singletonInAnotherChild, is(sameInstance(singletonInChild)));
    }

このため以下のようなトラブルが発生した場合は、親クラスローダがパッケージされているものと同じライブラリを持っていないか確認する必要があります。

シングルトンがシングルトンじゃない問題

developerWorksにも細かい解説が載っていますが、シングルトンパターンで使用するようなstaticフィールドは「JVM内に1つしかない」のではなく「ロードされたクラスごとに1つずつある」ため、以下のような条件下で期待と異なる動作をします。

  • 「親が最後」ポリシーを使用した場合
  • 兄弟クラスローダがそれぞれクラスをロードした場合

以下のテストケースは上記2パターンでシングルトンインスタンスが複数存在してしまうケースを説明するものです。

    @Test
    public void parentLastMayHaveMultiSingletonProblem() throws Exception {
        Object singletonInParent = findSingletonInstanceFrom(secondClassLoader);
        Object singletonInChild = findSingletonInstanceFrom(childOfSecondClassLoader);
        assertThat(singletonInChild, is(not(sameInstance(singletonInParent))));

        Object singletonInAnotherChild = findSingletonInstanceFrom(anotherChildOfSecondClassLoader);
        assertThat(singletonInAnotherChild, is(not(sameInstance(singletonInParent))));
        assertThat(singletonInAnotherChild, is(not(sameInstance(singletonInChild))));
    }

こうした問題もあるので、今であればJ2EE環境では自前でシングルトンを実装するのではなく、環境が提供する手法(@Singletonアノテーションとか)に頼ったほうが良いでしょう。テスト可能性も向上するはずです。

アプリケーションを再起動したのにメモリが解放されない問題

上述したとおり、クラスがアンロードされるためにはクラスとクラスローダの双方が利用されない必要があります。利用されないというのは、それらのインスタンスやstaticフィールド、Classインスタンスなどへの参照がなくなることです。

例として以下のテストを見てみます。クラスとクラスローダに対する参照は破棄されているもののインスタンスに対する参照(singletonInstance変数)が残っているため、クラスがアンロードされません。

    @Test
    public void classLoaderShouldNotBeFinalizedIfSomeoneRefersIt() throws Exception {
        UnloadEventListener listener = new UnloadEventListener();
        Object singletonInstance = loadSingletonFromNewClassLoader(listener);
        assertThat(listener.unloaded, is(false));

        runFinalization(listener);
        assertThat("class loader should not be finalized, because we still use Singleton instance", listener.unloaded, is(false));
        System.out.printf("Singleton instance is %s%n", singletonInstance.getClass().getClassLoader()); // we should use singletonInstance like this, or optimization removes singletonInstance local variable
    }

本番環境では、Java基本クラスローダやアプリケーションサーバのクラスローダがロードしたクラスからの参照がリークの原因となります。

事例をまとめた資料として、nekopさんのクラスローダーリークパターンがすばらしいです。JCL(Jakarta Commons Logging)あるいはJUL(java.util.logging)を使用されている場合は必見です。

おわりに

以上、駆け足でしたがクラスローダとそれに関連するトラブルを紹介しました。クラスローダはJVMの基本のひとつであり、J2EEではEJBなどでも利用されています。理解を深めて気軽に触れるようになっておきましょう。

サンプルコード

本記事で使用したコードはすべてGitHubから入手いただけます。クラスローダ実装例ないしJUnitの使い方としても使えそうです。

関連記事

クラスローダ自作はバイトコードを生成・改変する際にも行います。このブログでもObjectWeb ASMによるクラスの動的定義で使用しています。

*1:アプリケーションサーバによっては設定で変更することも可能

Useful libraries and Maven plugins to implement executable jar

When you implement executable jar, there is some good libraries and Maven plugins to implement. I will list them and explain how to use.

args4j

args4j is a good library to parse command line argument. It is more modern than commons-cli, and its committer is Mr.KAWAGUCHI the author of Jenkins, so its improving speed is good: your patch will be checked and merged (here is my case).

config loader

For me, xml is not so good format because of its redundancy. Yml is also not so good, JSON is better and easy to understand.

To load JSON configuration file, I know 2 major solutions:

typesafehub/config allows you to parse human-readable JSON (hocon) format, so user can reduce redundant configuration and add comment into JSON format. jackson-mapper can map your configuration to POJO, so your code can be simple than typesafehub/config.

You can write simple code to use good point of both. Here is an example.

Maven multi module project

When you develop command line tool, it is better to divide your product into (at least) 2 parts:

  1. user interface layer (which supports command line interface)
  2. others (logic, persistence, etc.)

Interface specific code should be separated from other code to improve portability. If you have plan to implement other interface (daemon, GUI, web application etc.) which shares logic, this separation will help you to reuse your code.

To separate, Maven multi module project can be solution. Make 2 submodule and name them as 'command-line' and 'core' (or 'logic' etc.). Only command-line module depends on args4j and configuration parser, so core module cannot be compile with class in these libraries.

Maven assembly plugin

Maven provides several way to make executable jar. In my opinion, assembly plugin is simplest and easiest solution. It also package dependencies to jar file so you can simply distribute single jar file. See Stackoverflow to know how to configure.

To change the name of jar file, you need to use finalName parameter.

Note that you must check license of your dependencies to know it is re-distributable or not. If your dependencies is not re-distributable, you should stop redistribute it.

How to build FindBugs 3.0-SNAPSHOT by Maven

Now FindBugs 3.0-SNAPSHOT depends on SNAPSHOTs which is not hosted on Maven repository. To build, we have to execute install:install-file manually.

$ git clone https://code.google.com/p/skypencil-findbugs/
$ cd findbugs/findbugs
$ git checkout repair-maven
$ mvn install:install-file -Dfile=lib/bcel-6.0-SNAPSHOT.jar -DgroupId=com.google.code.findbugs -DartifactId=bcel -Dversion=6.0-SNAPSHOT -Dpackaging=jar
$ mvn install:install-file -Dfile=lib/asm-debug-all-5.0_BETA.jar -DgroupId=org.ow2.asm -DartifactId=asm-debug-all -Dversion=5.0_BETA -Dpackaging=jar
$ mvn clean install -DskipTests

Note that this example is using my clone, because official repository is not ready to use Maven. See this ticket for detail.