倭マン's BLOG

主に Java, Groovy, Griffon 関連の基礎的な記事を書いてます。

どんとこいタイプ・アノテーション! Checker Framework 導入あの手この手 ~without IDE 編~

前回の記事『ラムダ式やストリーム API や新しい日時 API だけじゃない! Java8 のタイプ・アノテーションあの手この手』で、Java8 で導入されたタイプ・アノテーションがどういったものか紹介しました。 その記事では同一要素に重複して同じアノテーションを付けられる @Repeatable なども紹介しましたが、タイプ・アノテーションは主に「タイプ(型)が使われているところはどこにでもアノテーションが付けられる」という機能をさしているのだと思います。

で、その機能を紹介したはいいんですが、実際の使い方には触れていませんでした。 これでは片手落ち感が否めないので、タイプ・アノテーションを使用できるライブラリである Checker Framework というのを見ていきたいと思います(が、次に述べる理由により、ライブラリ自体の使い方はこの記事で扱ってません)。 このライブラリは @NonNull のようなアノテーションを型に付加して null 値代入をコンパイル時にエラーを出すといったように、コンパイル時のエラーチェックを行うものです(@Override アノテーションみたいな感じの使い方)。 Java の公式のチュートリアルでもこのライブラリについて言及されていて、これから標準的に使われるようになるかもしれませんが、1つの難点は Java7 時代の JavaFX のような設定の面倒さがあるところ*1。 あまり正確なところは分からないのですが、Java7 でも使えるようになってるせいか、Java8 で使う場合にもコンパイラを実行するときに設定がいるのが原因じゃないかと。 まぁ、逆に言えば Java7 でも使えるようなので、ご興味のある方は試してみて下さい。 Java6 以前は非サポートなようです*2

ってことで、この記事では Checker Framework の機能ではなく、インストール方法というかコンパイラにチェック機能を追加する設定方法を見ていきます。 Checker Framework の機能自体もそのうち見ていきたいとは思ってるんですが、いつになることやら。 また、これらの設定は通常 IDE でもやらないといけないものですが、ちょっと手が回らないので今回はビルドツールをいくつか扱うだけにします。

この記事で使用する Checker Framework のバージョンは 1.8.1 とします。 Java は 8 です。 試してませんが Java7 でも同じような設定(ただし jdk8.jar の代わりにjdk7.jar が必要)でできるんじゃないかと思います。

参考

コマンドラインから使用する

まずは、あまり使わないかと思いますが、コマンドラインから Checker Framework を使う方法を見ていきます(Windows)。 別に設定が難しいわけではないのですが、使用するコンパイラを変えるのが如何に面倒かを味わってもらおうかと(笑) それは冗談ですが、最近注目を浴びているプロジェクト管理ツールである Gradle にプラグインがないので、誰かこれを参考にして作ってくれないかなぁという期待を込めて。 あっ、一応、あとで Gradle 上で Checker Framework を(手書きで)使う方法は書きますが。

コマンドラインから Checker Framework を使う方法は、公式のマニュアルに3つ、別途に通常の javac コマンドにオプションを付けて使用する方法を1つ紹介します。 どれか1つでOK。 最後の方法は公式のドキュメントには載ってないので使用は自己責任で。

最初3つの方法の、Unix 系(というか bash)での設定方法は The Checker Framework Manual 「1.3 Installation」に載ってます。 Windows に関しては The Checker Framework Manual 「24.1 Javac Compiler」にいくつかの方法が載ってます。

コマンドラインから使用する場合は、当然のことながらライブラリが自動ダウンロードされないので、まずは以下の設定をしておいて下さい:

  1. http://types.cs.washington.edu/checker-framework/current/checker-framework.zip から Zip ファイルをダウンロードして、適当なディレクトリに展開する(例えば "C:\java\")
  2. 環境変数*3CHECKERFRAMEWORK」に上記の Zip ファイルを展開したルート・ディレクトリを設定する(上記の例では "C:\java\checker-framework-1.8.1")

また、実際にコンパイル時にアノテーションによるチェックが行われているかを試すための Java ソースコードとして、以下の「GetStarted.java」ファイルを使います( The Checker Framework Manual 「1.3 Installation」から拝借):

// GetStarted.java
import org.checkerframework.checker.nullness.qual.*;

public class GetStarted{
    void sample(){
        @NonNull Object ref = new Object();
        //@NonNull Object ref = null;
    }
}

コメントアウトしている部分を外すと、コンパイル時にエラーが出ます。 では設定方法を見ていきましょう。

Checker Framework の javac コマンドを javac コマンドとして使う
まずは Checker Framework を展開したときに bin ディレクトリに含まれている javac コマンドを使ってコンパイルする方法(Windows)。

set PATH=%CHECKERFRAMEWORK%\checker\bin;%PATH%
javac -processor org.checkerframework.checker.nullness.NullnessChecker GetStarted.java
  • 1行目は1度だけでOK。 setBASH の export と同じで環境変数を設定するコマンドですね。 PATH の値をセットする際に、%CHECKERFRAMEWORK%\checker\bin を最初に書いてあるところに注意。 これは Checker Framework の javac コマンドを優先して使うようにするために必要です。
  • javac コマンド実行時に -processor オプションによってアノテーション・プロセッサを設定しています。 このアノテーション・プロセッサの設定は他の方法でもどこかで指定する必要があります。 面倒ですが諦めて下さい。 複数のプロセッサを指定する場合はコンマ (,) で区切ります。

この方法では PATH が汚染されるのと、もとの javac コマンドが使えないのが難点。

Checker Framework の javac コマンドを javacheck コマンドとして使う
次の方法は Checker Framework の javac コマンドを javacheck コマンド(別に他の名前でもいいですが)として使う方法:

doskey javacheck=%CHECKERFRAMEWORK%\checker\bin\javac $*
javacheck -processor org.checkerframework.checker.nullness.NullnessChecker GetStarted.java
  • doskeyBASH の alias ですね。 初めて使ったw doskey では引数をパイプ(?)するため、最後に「$*」を付けておかないといけないようです。 alias はいらないらしいそうで。 この doskey コマンドの実行も1度で OK。
  • 2行目は1つ目の方法で javac の代わりに javacheck を使ってるだけです。

この方法だと、もともとの javac コマンドはそのまま使うことができます。 まぁ、普通にコマンドラインから使うにはこれで充分です。 次はコマンドラインから使う方法ですが、Java コード中から使うのに応用できる方法。

Checker Framework の Jar ファイルを使ってコンパイルする
この方法は、Checker Framework のアーカイブに含まれている checker.jar を実行可能 Jar ファイルとして実行する方法です:

java -jar %CHECKERFRAMEWORK%\checker\dist\checker.jar -processor ^
org.checkerframework.checker.nullness.NullnessChecker GetStarted.java

もしくは

doskey javacheck=java -jar %CHECKERFRAMEWORK%\checker\dist\checker.jar $*
javacheck -processor org.checkerframework.checker.nullness.NullnessChecker GetStarted.java

Jar ファイルの実行なので javac コマンドではなく java コマンドを使っていることに注意。 checker.jar に含まれているメインクラスは

です(リンクはソースコード)。 ソースコードは framework サブプロジェクトにあります。 まぁ、ソースコード読もうという人はあんまりいないかもしれませんが、メモのために書いておくと、このクラスは引数をオプションとして解析したり Jar ファイルへのクラスパスを設定したりする処理を行い、実際のコンパイルは com.sun.tools.javac.Main に投げてます。

通常の javac コマンドでコンパイルする
最後は公式のドキュメントには載ってませんが、javac コマンドに(ちょっと長めの)オプションをあれこれ指定して、通常の javac コマンドでコンパイルする方法。

javac -cp .;checker.jar;javac.jar ^
-Xbootclasspath/p:jdk8.jar ^
-processor org.checkerframework.checker.nullness.NullNessChecker GetStarted.java
  • クラスパスに checker.jar, javac.jar を含めます。 上記のコマンドではカレント・ディレクトリにこれらの Jar ファイルがあるとしてますが、そうでない場合はそれらへの(相対 or 絶対)パスをきちんと書く必要があります。
  • 非標準のオプション -Xbootclasspath/p: で jdk8.jar ファイルを指定します。 これもカレント・ディレクトリにない場合はきちんとパスを書く必要があります。 また、Java7 で使いたい場合は jdk7.jar にします(たぶん。 試してないけど)。

まぁ、長々とオプション書いて何が楽しいねんと言われそうですが、ちょっと Gradle で手書きするのに必要なので載せました。

Maven2/3 の設定

次は Maven2/3 で Checker Framework を使う方法。 Maven2/3 に対しては Checker Framework 側でプラグインを作ってくれているので、pom.xmlXML 地獄以外は特に問題なく使えます。 ドキュメントは The Checker Framework Manual 「24.3 Maven plugin」にあります:

<?xml version="1.0"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
         
  <modelVersion>4.0.0</modelVersion>
  <groupId>my.test</groupId>
  <artifactId>annotation-test</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>jar</packaging>
  <name>Type annotation Test</name>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>

  <dependencies>
    <dependency>
      <groupId>org.checkerframework</groupId>
      <artifactId>checker-qual</artifactId>
      <version>1.8.1</version>
    </dependency>    
  </dependencies>
  
  <build>
    <plugins>
      <plugin>
        <groupId>org.checkerframework</groupId>
        <artifactId>checkerframework-maven-plugin</artifactId>
        <version>1.8.1</version>
        <executions>
          <execution>
            <phase>process-classes</phase>
            <goals>
              <goal>check</goal>
            </goals>
          </execution>
        </executions>
        <configuration>
          <processors>
            <processor>org.checkerframework.checker.nullness.NullnessChecker</processor>
          </processors>
        </configuration>
      </plugin>
    </plugins>
  </build>
</project>
  • マニュアルにあるリポジトリの設定は必要ありません(バージョン 1.8.0 以降)。
  • 複数のアノテーション・プロセッサが必要な場合、<checkerframework-maven-plugin> 要素下にある <processors> 要素に <processor> 要素を付け加えていけばいいんでしょう。
  • 結構あれこれとプラグインに設定ができるそうです。 詳しくはマニュアル参照。

ちょっと疑問なのが、process-classes フェーズに check ゴールを付加してるところ。 コンパイル2回してたりしない? まさかね。 もともとのコンパイルはスキップしたりしてるのかな?

Gradle の設定

最後は Gradle。 Gradle に関してもマニュアルに設定方法が書いてるのですが、その方法だと、「コマンドラインから使用する」の箇所で書いたようなアーカイブのダウンロード & 展開と環境変数の設定が必要になります。 うーむ、Maven2/3 では pom.xml を書く以外は全自動だったのに、Gradle では手動のインストールが必要みたいに扱われてるのは Gradle にとって不当な扱いだ! ってことで、それらのインストールがいらない build.gradle を書いてみました。 手書きなのでちょっと汚いです。 誰か Gradle プラグイン作って。

apply plugin : 'java'

group = "my.test"
version = "1.0-SNAPSHOT"
sourceCompatibility = 1.8
targetCompatibility = 1.8

project.ext{
    checkerVersion = '1.8.1'
}

tasks.withType(Compile){ options.encoding = "UTF-8" }

repositories.mavenCentral()

dependencies{
    ['checker', 'checker-qual', 'compiler', 'jdk8'].each{
        compile "org.checkerframework:$it:$checkerVersion"
    }
}

tasks.withType(JavaCompile){
    options.with{
        fork = true
        compilerArgs = [
            '-Xbootclasspath/p:'+getJdk8JarPath(),
            '-processor', getAnnotationProcessors().join(',')
        ]
    }
}

task wrapper(type: Wrapper) {
    gradleVersion = "1.12"
}

def getJdk8JarPath(){
    return configurations.compile.files.find{ it.name == "jdk8-${checkerVersion}.jar" }.absolutePath
}

def getAnnotationProcessors(){
    return [
            'nullness.NullnessChecker',
            'interning.InterningChecker'
        ].collect{ 'org.checkerframework.checker.'+it }
    
}
  • 基本的には、「コマンドラインから使用する」の箇所に書いた4つ目の方法を Groovy & Gradle 風に書き直しただけです。 JavaCompiler タスクに最低限のオプションだけを設定しています。
  • このままだとテストコードについてもチェックが行われるので(別にいいんですけど)、テストコードのコンパイルではチェックを行わないような設定もしたいところ。
  • アノテーション・プロセッサを追加したい場合は getAnnotationProcessors() メソッドの適当な箇所に追加して下さい。
  • checker-qual への依存関係はなくても Gredle でビルドする分にはいりませんが、IDE とか使う場合はいるんじゃないかと。
  • マルチ・プロジェクト(サブプロジェクトがあるプロジェクト)では、マニュアルにあるように allprojects{ ... } の ... の部分にコンパイラ等のスクリプトを書けば OK。

以上、コマンドラインからの実行、Maven, Gradle の設定方法を見てきました。 マニュアルには Apache Ant についての設定も書いてあります。 必要な方はそちらをどうぞ。

Checker Framework を実際の開発に使うには、さらに IDE の方で

を行う必要があるので、今回の記事の設定だけでは Checker Framework を導入するには不十分ですが、この記事はとりあえずこのへんで。 @Override アノテーションも今や使っている人の方が多いんじゃないかと思うので、Checker Framework も使われ出したら広まるの速いんじゃないかなぁ。 Lombok のような別ライブラリもあるけど。 なんにしろまず使ってみないことには話が始まらないね。

JUnit実践入門 ~体系的に学ぶユニットテストの技法 (WEB+DB PRESS plus)

JUnit実践入門 ~体系的に学ぶユニットテストの技法 (WEB+DB PRESS plus)

Jenkins実践入門 ?ビルド・テスト・デプロイを自動化する技術 (WEB+DB PRESS plus)

Jenkins実践入門 ?ビルド・テスト・デプロイを自動化する技術 (WEB+DB PRESS plus)

*1:ライセンスがどうこうという話ではありませんが。

*2:対応してた痕跡はコードに残ってますが。

*3:Windows のバージョンによるかと思いますが、「コントロールパネル ▶ システムとセキュリティ ▶ システム ▶ システムの詳細設定 ▶ 環境変数」あたりで設定できます。