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')
    }
}

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

おわり