mike-neckのブログ

Java or Groovy or Swift or Golang

Gradleでアノテーションプロセッサーを使ってソースコードを生成するようなプロジェクトを作ってみる #ソースコード自動生成

こんにちわ、みけです。

前回org.glassfish.grizzly.http.server.CLStaticHttpHandler

JAX-RS(Jersey)を組み合わせてアプリケーションを作って、

ルートにデプロイしたところ、

片方のハンドラーしか動作しないという残念な結果になってしまいました。

なんか、エロイ人からも特にアドバイス的な音沙汰もなかったので、

CLStaticHttpHandlerを使うのを諦めて、

全部JAX-RSに寄せようと思います。

問題点

さて、スタティックなコンテンツに対するハンドルをJAX-RS

記述する場合、一つだけ困ったことが有ります。

それはコンテンツが増えるたびに、リソースクラスを作らなければいけないことです。

また、逆にコンテンツがなくなるたびに、リソースクラスを削除しなければいけないことにもなります。

こういった、本質的でないクラスの作成を人様がやるには、

人様の時間は限られすぎています。

というわけで、こういう作業はコンパイラーにさせるべきであるので、

アノテーションプロセッサーにやらせることにしました。


というわけで、本日のお題

Gradleでアノテーションプロセッサーを使ってソースコードを生成するようなプロジェクトを作ってみる

です。

アノテーションプロセッサーに関する基本的でかつ詳細な事柄については、

nikkei IT Proの記事が詳しいです。


自作アノテーションプロセッサーを使うときのポイント

  1. アノテーションプロセッサーとアプリケーションの二つのプロジェクトを用意する
  2. アプリケーションはコンパイル時にはプロセッサーを参照できないようにする
  3. プロセッサーをコンパイルするときだけクラスパスに追加する
  4. アノテーションを付与できるようにするために、アノテーションクラスだけをさらに別のプロジェクトに切り出す
  5. プロセッサーでコードを生成する場合は、生成先のディレクトリーをコンパイル前に作っておく
  6. 生成されたコードがすでにある場合は、一度コードを破棄する
  7. 生成されたコードもコンパイル対象に加える

1.アノテーションプロセッサーとアプリケーションの二つのプロジェクトを用意する

アノテーションプロセッサーを自作する場合、

プロジェクトの構造が若干面倒になります。

というのも、アプリケーションプロジェクトのコンパイルの段階で、

既にアノテーションプロセッサーのプロジェクトはビルドされている

必要があるので、プロジェクト間で依存関係が発生します。

したがって、アプリケーションプロジェクト(application)は、

アノテーションプロセッサーのプロジェクト(processor)に依存するようにします。

これをgradleで記述する場合、次のようになります。

build.gradle

subprojects {
    apply plugin: 'java'
}
project(':application') {
    dependencies {
        compile project(':processor')
    }
}

settings.gradle

include 'processor', 'application'
2.アプリケーションはコンパイル時にはプロセッサーを参照できないようにする

ところで、アノテーションプロセッサーで用いられるクラスは、

アプリケーションを作成するためのクラスには関係のないクラスばかりなので、

コンパイルするときに不要なものばかりです。

依存はするけど、コンパイル時には参照できないようにしておきたいです。

したがって、別のconfigurationを作成して、

アプリケーションプロジェクトはそこからプロセッサープロジェクトに

依存するようにします。

したがって、gradleの記述は次のようになります。

build.gradle

project(':application') {
    configurations {
        annotationProcessor
    }
    dependencies {
        annotationProcessor project(':processor')
    }
}
3.プロセッサーをコンパイルするときだけクラスパスに追加する

ただし、これだと、コンパイルするときにプロセッサーが素通りしてしまうので、

コンパイルクラスパスに含める必要があります。

したがって、gradleの記述は次のようになります。

build.gradle

project(':application') {
    configurations {
        annotationProcessor
    }
    dependencies {
        annotationProcessor project(':processor')
    }
    compileJava {
        classpath += configurations.annotationProcessor
    }
}
4.アノテーションを付与できるようにするために、アノテーションクラスだけをさらに別のプロジェクトに切り出す

プロセッサーのトリガーとなるアノテーションは、

プロセッサーはもちろん、アプリケーションプロジェクトからも参照できないと、

アノテーションを付与することができません。

アノテーションクラスはプロセッサー、アプリケーションプロジェクト双方から参照できるように、

別のサブプロジェクトに切り出しておく必要があります。

したがって、サブプロジェクト(common)を作成して、

それにプロセッサー、アプリケーションの両方のプロジェクトが依存するようにします。

settings.gradle

include 'common', 'processor', 'application'

build.gradle

project(':application') {
    configurations {
        annotationProcessor
    }
    dependencies {
        compile project(':common')
        annotationProcessor project(':processor')
    }
    compileJava {
        classpath += configurations.annotationProcessor
    }
}
project(':processor') {
    dependencies {
        compile project(':common')
    }
}
5.プロセッサーでコードを生成する場合は、生成先のディレクトリーをコンパイル前に作っておく

アノテーションプロセッサーの仕様により、ソースコードを生成する場合、

存在しないディレクトリーにソースコードを作成することができません。

したがって、コンパイル前に生成したソースコードを格納するディレクトリーを

作っておく必要があります。

また、出力先のディレクトリーの情報はコンパイラーに引数として渡しておかない場合、

コンパイラーはクラス出力先にソースコードを出力してしまいます。

その結果、出力したコードをさらに利用する場合などは不便になってしまいます。

そのため、-sオプションによって出力ディレクトリーを指定します。

ここでは生成したソースコードsrc/main/generatedに出力するものとします。

build.gradle

project(':application') {
    ext {
        generated = file("${projectDir}/src/main/generated")
    }
    configurations {
        annotationProcessor
    }
    dependencies {
        compile project(':common')
        annotationProcessor project(':processor')
    }
    compileJava {
        doFirst {
            mkdir generated
        }
        options.compilerArgs += ['-s', generated]
        classpath += configurations.annotationProcessor
    }
}
6.生成されたコードがすでにある場合は、一度コードを破棄する

アノテーションプロセッサーの仕様により、生成しようとするファイルがある場合は、

エラーが発生してしまいます。

そのため、コンパイルするたびに、生成されたコードを破棄する必要があります。

そこで、コンパイルタスクは生成されたコードを破棄するタスクに依存するようにします。

build.gradle

project(':application') {
    ext {
        generated = file("${projectDir}/src/main/generated")
    }
    configurations {
        annotationProcessor
    }
    dependencies {
        compile project(':common')
        annotationProcessor project(':processor')
    }
    task cleanGenerated(type: Delete) {
        delete generated
    }
    compileJava {
        dependsOn cleanGenerated
        doFirst {
            mkdir generated
        }
        options.compilerArgs += ['-s', generated]
        classpath += configurations.annotationProcessor
    }
}
7.生成されたコードもコンパイル対象に加える

生成されたコードもソースコードですから、

これをコンパイル対象に加えます。

具体的にはsourceSetsで指定します。

build.gradle

project(':application') {
    ext {
        generated = file("${projectDir}/src/main/generated")
    }
    configurations {
        annotationProcessor
    }
    sourceSets {
        main {
            java {
                srcDirs generated
            }
        }
    }
    dependencies {
        compile project(':common')
        annotationProcessor project(':processor')
    }
    task cleanGenerated(type: Delete) {
        delete generated
    }
    compileJava {
        dependsOn cleanGenerated
        doFirst {
            mkdir generated
        }
        options.compilerArgs += ['-s', generated]
        classpath += configurations.annotationProcessor
    }
}

以上の要求をまとめたビルドスクリプトは次のようになります。

なお、その他の前提条件は次のとおりです。

settings.gradle

include 'common', 'processor', 'application'

build.gradle

apply plugin: 'idea'
ext {
    jdk = 1.8
    encoding = 'UTF-8'
}
allprojects {
    repositories {
        mavenCentral ()
    }
}
subprojects {
    apply plugin: 'java'
    group = 'sample.processor'
    version = '1.0'
    compileJava {
        sourceCompatibility = rootProject.jdk
        targetCompatibility = rootProject.jdk
        options.encoding = rootProject.encoding
    }
    dependencies {
        testCompile 'junit:junit:4.11'
    }
}
project(':application') {
    ext {
        generated = file("${projectDir}/src/main/generated")
    }
    configurations {
        annotationProcessor
    }
    sourceSets {
        main {
            java {
                srcDirs generated
            }
        }
    }
    dependencies {
        compile project(':common')
        annotationProcessor project(':processor')
    }
    task cleanGenerated(type: Delete) {
        delete generated
    }
    compileJava {
        dependsOn cleanGenerated
        doFirst {
            mkdir generated
        }
        options.compilerArgs += ['-s', generated]
        classpath += configurations.annotationProcessor
    }
    clean.dependsOn cleanGenerated
}
project(':processor') {
    dependencies {
        compile project(':common')
    }
}

その他のアノテーションプロセッサーの諸注意は次回以降…

おわり

アノテーションプロセッサーで自動生成するときにハマったあたりの話 #ソースコード自動生成

こんにちわ、みけです。

昨日に引き続いてアノテーションプロセッサーのあれをあれするあれです。

概要

  1. プロセッサーを指定する
  2. アノテーションプロセッサーにパラメーターを渡す
  3. プロセス対象のプロジェクトのリソースは取得できない
1. プロセッサーを指定する

プロセッサーを指定する方法は次のとおり二つあります。

  • javac-processorオプションで指定する
  • src/main/resource/META-INF/services/javax.annotation.processing.Processorファイルで指定する

-processorオプションで指定する場合は次のように、

javac -processor jp.hoge.foo.BarProcessor,jp.hoge.foo.BasProcessor -classpath ...

カンマ区切りでプロセッサーのクラス名を指定します。

gradleで記述する場合は次のようになります。

compileJava {
    options.compilerArgs += ['-processor', 'jp.hoge.foo.BarProcessor,jp.hoge.foo.BasProcessor']
}

プロセッサーの数が増えてくると、プロセッサーの実装の分だけ、

指定しないといけなくなるので、若干つらいかもしれません。

そこで、プロセッサー用のプロジェクトで、

サービスプロバイダーコンフィギュレーションファイル

に記述しておいた方がコンパイラーオプションが増えないので便利かもしれません。

サービスプロバイダーコンフィギュレーションファイルは、

META-INF/services/javax.annotation.processing.Processorというファイルで

プロセッサーのパッケージ名を含めた正規名を一行に一つ記入します。

jp.hoge.foo.BarProcessor
jp.hoge.foo.BazProcessor

このファイルがある場合は、引数でプロセッサーを指定しなくても構いません。

2. アノテーションプロセッサーにパラメーターを渡す

プロセッサーの実装クラスには@SupportedOptionsアノテーションによって、

パラメーターを渡すことができます。

import javax.annotation.processing.SupportedOptions;

@SupportedOptions({"foo", "bar"})
public class SampleProcessor extends AbstractProcessor {
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        Map<String, String> options = processingEnv.getOptions();
        String foo = options.getOrDefault("foo", "FOO");
        String bar = options.getOrDefault("bar", "BAR");
    }
}

実際に渡されたパラメーターは、AbstractProcessorで宣言してある、

ProcessingEnvironment型のprocessingEnv変数から

#getOptions()メソッドで取得することができます。

パラメーターが指定されていない場合には、#getOptions()メソッド

返されるMap<String, String>のキーに対して値は設定されていません。

例えば、上記の例でオプションfooに値が設定されていない場合は、

変数fooには"FOO"が設定されます。

パラメーターの設定方法は、javacコマンドの引数に-Aargument=valueの形で指定します。

例えば、foobarに値を指定する場合は、次のようになります。

javac -Afoo=foofoo -Abar=barara ...

これをgradleで記述する場合は次のようになります。

compileJava {
    options.compilerArgs += ['Afoo=foofoo', 'Abar=barara']
}
3. プロセス対象のプロジェクトのリソースは取得できない

これは単なる僕のポカミスで、アノテーションプロセッサーから、

コンパイル中のプロジェクトのリソースにアクセスできると

何故か思い込んでいて、

ずっと、「できない、できない…」と思ってたというだけの話です。

ファイルオブジェクトへのアクセスを可能にする

javax.annotation.processing.Filerという

インターフェースがあります。

そのインターフェースの下記の4つのメソッドによって、

ファイルへアクセスすることができます。

  • #createSourceFile(String, Element...) - ソースファイルの作成
  • #createClassFile(String, Element...) - クラスファイルの作成
  • #createResource(JavaFileManager.Location, String, String, Element...) - リソースファイルの作成
  • #getResource(JavaFileManager.Location, String, String) - リソースファイルの取得

このときに、リソースファイルの取得メソッドがあるので、

勝つると思い込んでいたわけですが、

これはソースかクラスの出力先のディレクトリーにあるファイルに

アクセスできるだけで、

maven形式のディレクトリー構成のリソースファイル(src/main/resources)には

アクセスできません。

javadocちゃんと嫁という話ですね…(´・ω・`)

おわり