GraphQL サーバー on Kotlin ことはじめ (DroidKaigi 2019 に参加して GraphQL について学んだ)
DroidKaigi 2019 にスポンサー枠で参加しました。 弊社では Android アプリエンジニアをはじめとして各分野のソフトウェアエンジニアを募集しております。 人生を豊かにするプロダクトの開発や、大きな企業でのソフトウェア開発をいかに改善していくかといったところに興味がある方はぜひお声がけください! いっしょにやっていきましょう!
それはともかく DroidKaigi、様々なセッションがあって素晴らしかったですね。 運営、スピーカー、スポンサー、そして参加者の皆様、ありがとうございました。
私個人としては、GraphQL についての知見を得られたのが大きな収穫でした。
本記事について
本記事は、GraphQL サーバーを Kotlin で立てるにあたって、仕組みを学んだ軌跡を残すものです。 GraphQL 自体の初心者が、Kotlin で GraphQL サーバーを実装する際には参考になると思います。 本番環境で使える技術とかを紹介しているわけではないのでご注意ください。
DroidKaigi 2019 と GraphQL と私
GraphQL についてはこれまでも雰囲気では知っていたのですが、はてなの id:takuji31 さんと id:funnelbit さんの発表で改めて GraphQL の良さを感じました。
ところで、上記の発表では主にクライアントサイド視点での GraphQL の話が主で、サーバーサイドの実装がどうなるのかが非常に気になるところです。 クエリのパースから実際のデータの取得処理まで、素朴に実装しようとすると難しそうに感じます。 懇親会のときに id:takuji31 さんと id:funnelbit さんに聞いてみたところ、「サーバーサイドの実装者ではないのでそこまで詳しくはないが、言語ごとにいい感じのライブラリがあるから、割と何とかなるっぽい」 とのことでした。
ということで GraphQL の Kotlin サーバーに詳しい人に教えを請いたい〜、と思っていたら shiraji さんが導いてくれて gfx さんや taka さんたちから知見を吸わせてもらえました。 神か! (ありがとうございました。)
知見
- GraphQL をするならまずは gfx さんのこの記事を見ろ! : 「GraphQL」徹底入門 ─ RESTとの比較、API・フロント双方の実装から学ぶ - エンジニアHub|若手Webエンジニアのキャリアを考える!
- GraphiQL というブラウザ上で動く GraphQL IDE が存在する : graphiql - npm
- JVM 系言語における GraphQL の実装は graphql-java が一番良さそう。
- Spring Boot で graphql-java を使った実装のサンプルは Ubie 社 (shiraji さん) が公開しているぞ! : GitHub - ubie-inc/kotlin-graphql-sample: Sample implementation of Kotlin+Spring+GraphQL
- graphql-java を Spring Boot で使うときの便利ライブラリの依存関係は taka さんがまとめてくれている : コードで見る graphql-java 関連ライブラリの関係性 - Qiita
自分で動かして試してみる
上記のようなページを見ることでなんとなく Kotlin での GraphQL サーバーサイド実装をどうすれば良いのかが見えてきましたが、Spring Boot を使った上記の例では便利ライブラリが詳細を隠ぺいしてくれているので細かな理解ができませんでした。 というわけで自分でも手を動かして試してみます。
graphql-java を試す
graphql-java は GraphQL のスキーマやクエリのパース、バリデーション、実行といった機能を提供するものです。 HTTP のインターフェイスは提供しません。 Kotlin で GraphQL の挙動を試したい場合は、まずは graphql-java を生で触るのが良いでしょう。 下記のドキュメントを見ながら進めると良いと思います。
外部 IO 無しで簡単に試せるので、最初のとっかかりとしては非常にやりやすいと思います。 私は下記のようなコードを書いて試していました。
- Hello world 的なやつ : https://github.com/nobuoka/kotlin-graphql-playground/blob/10d3f14c13b9d0e6d95fbbe82d1df85314227ba6/playground/src/main/kotlin/1-HelloWorld.kt
- 条件を指定してデータをフェッチするやつ : kotlin-graphql-playground/2-Schema.kt at 10d3f14c13b9d0e6d95fbbe82d1df85314227ba6 · nobuoka/kotlin-graphql-playground · GitHub
まだリソース間に関連がある場合などは試せていません。
HTTP インターフェイスとつなぎこむ
GraphQL のサーバー・クライアント間の通信プロトコルについては、(おそらく) 仕様上は何も定められていなさそうです。 公式サイトに、HTTP での通信についてベストプラクティスとして書かれています。
これに従ってサーバーサイドの HTTP のエンドポイントを実装し、graphql-java とつなぎこむ、という感じにすれば良さそうです。 もともとの GraphQL のクライアント・サーバー間のやり取りがシンプルなので、HTTP のエンドポイントとしての実装もシンプルになります。
細かなエラーハンドリングなどはできていませんが、Ktor で実装すると以下のような感じになりました。
GraphiQL を使ってみる
ここまでくると自分のサーバーで GraphiQL も提供したいですよね?
GraphiQL の JS ライブラリは、npm パッケージとして配信されています。
自分で HTML ファイルや CSS ファイル、JS ファイルをビルドすることもできますが、既にビルドされている JS ファイルを使って簡単に GraphiQL のエンドポイントを生成することもできます。 使い方の例は GraphiQL のリポジトリに含まれています。
上記 HTML ファイルを自分のサーバーから配信すれば、GraphiQL が提供されます。 ただし、依存するリソースである graphiql.css と graphiql.js をどうにかする必要があります。 (上記の HTML そのままの場合は、これらのファイルも同じサーバーから配信する必要があります。)
今回は手軽に試したかったので CDN で配信されているものを使うようにしました。 下記 CDN で graphiql.css と graphiql.js が配信されています。
これを使うように書き換えた HTML をサーバーから配信すると、GraphiQL を使えるようになります。
Ktor で GraphiQL を表示させてみるところまでできた。 GraphiQL すごい♡ pic.twitter.com/jjV3Hb8ZYk
— Nobuoka Yu (@nobuoka) 2019年2月10日
おわり
というわけで初心者が GraphQL を学んでいる軌跡でした。 ここに書かれていない知見等ありましたら是非教えてくださいませ〜〜。
CloudFront のプライベートコンテンツ配信を試した (Kotlin で signed cookie を生成する)
- AWS の CDN である CloudFront でプライベートコンテンツを配信する方法として、signed URL を用いる方法と、signed cookie を用いる方法がある。
- 本記事では、signed cookie の生成処理を Kotlin (Java) で行う方法を説明する。
事前準備 (CloudFront 側の準備)
- アクセスする側の挙動を確認するために、試験的に CloudFront 側も準備したのでその手順を書いておく。 (AWS コンソール上で操作を行った。)
- 内容は次の 2 つ。
- CloudFront のディストリビューションを用意。
- CloudFront でコンテンツへのアクセスを制限する。
CloudFront のディストリビューションを用意
- 参考 : ディストリビューションを作成するためのステップ (概要) - Amazon CloudFront
- Delivery メソッドとして Web を選択。 (RTMP というのもあって、これはリアルタイムのストリーミングとかに使われるっぽい。 HTTP(S) での CDN として使うなら Web。)
- Origin としては適当なドメインの HTTP サーバーを指定した。
- → この段階では、新たに作成されたディストリビューションに割り当てられているドメインにアクセスすると普通にオリジンの内容が配信されることが確認できる。
コンテンツへのアクセスを制限する
キーペアの作成と CloudFront への登録
CloudFront にキーペアを登録する方法としては、ローカルマシン上で作成して公開鍵をアップロードするか、AWS コンソール上で作成して秘密鍵をダウンロードするかのどちらかとのこと。 今回はローカルマシン上で作成した。 コマンドは上記参考ページに載っている。
# 秘密鍵の作成 openssl genrsa -out private_key.pem 4096 # 公開鍵の作成 openssl rsa -pubout -in private_key.pem -out public_key.pem # Java で使う用に秘密鍵を DER 形式に変換 openssl pkcs8 -topk8 -nocrypt -in private_key.pem -inform PEM -out cloud_front_key.der -outform DER
アカウントメニューから 「セキュリティ認証情報」 画面に行くと 「CloudFront のキーペア」 という項目があるので、ここで公開鍵をアップロードする。 登録された鍵のアクセスキー ID と、上のコマンドで作成された DER 形式の鍵が後で必要になる。
信頼された署名者をディストリビューションに追加する
上で登録した鍵を使って signed cookie を生成してプライベートコンテンツにアクセスできるように、AWS アカウントを信頼された署名者として CloudFront のディストリビューションに登録する。
(ちなみにディストリビューションを作成した AWS アカウント以外のアカウントも登録できるらしい。 今回は自分自身を登録する。)
この段階で、signed cookie や signed url を使わないアクセスはエラーになるようになる。
Signed cookie を生成してプライベートコンテンツにアクセスする (Kotlin)
上で生成された DER 形式の秘密鍵とアクセスキーを使うことで、下記のようなコードでプライベートコンテンツにアクセスできる。 (下記では Java SE 11 から新たに追加された API (java.net.http
モジュール) を使用しているので、Java SE 11 でなければ動かない。)
import java.net.URI import java.net.http.HttpClient import java.net.http.HttpRequest import java.net.http.HttpResponse import java.nio.file.Files import java.nio.file.Paths import java.security.KeyFactory import java.security.PrivateKey import java.security.Signature import java.security.spec.PKCS8EncodedKeySpec import java.time.Instant import java.time.Period import java.util.* // CloudFront のドメイン名 private val cloudFrontDomainName = "xxxxx.cloudfront.net" // AWS コンソール上に表示されている CloudFront のキーペアのアクセス ID private const val cloudFrontKeyPairId = "xxxx" // 自分で生成した DER 形式の秘密鍵 private const val cloudFrontKeyPairFile = "cloud_front_key.der" fun main() { // ポリシーステートメントを用意。 val endingDate = Instant.now().plus(Period.ofDays(1)) val cloudFrontPolicyStatement = """ { "Statement": [ { "Resource": "http://$cloudFrontDomainName/", "Condition": { "DateLessThan": { "AWS:EpochTime": ${endingDate.epochSecond} } } } ] } """.trimIndent() // CloudFront に登録された鍵の情報を用意。 val keyFactory = KeyFactory.getInstance("RSA") // すべての Java 実装で RSA がサポートされる。 Java SE 8 のドキュメントで確認。 val cloudFrontKeySpec = PKCS8EncodedKeySpec(Files.readAllBytes(Paths.get(cloudFrontKeyPairFile))) val cloudFrontPrivateKey = keyFactory.generatePrivate(cloudFrontKeySpec) val cloudFrontKey = CloudFrontKey(cloudFrontKeyPairId, cloudFrontPrivateKey) // Signed cookie の値を生成。 val cloudFrontCookieValue = CloudFrontSignedCookieValue.create(cloudFrontPolicyStatement, cloudFrontKey) // Signed cookie を使用してプライベートコンテンツにアクセス。 val targetUri = URI.create("http://$cloudFrontDomainName/") val response = request(targetUri, HttpResponse.BodyHandlers.ofString(), cloudFrontCookieValue) println(response.body()) } fun <T> request( uri: URI, responseBodyHandler: HttpResponse.BodyHandler<T>, cloudFrontCookieValue: CloudFrontSignedCookieValue ): HttpResponse<T> { val client = HttpClient.newBuilder() .build() val request = HttpRequest.newBuilder() .uri(uri) .headers( "Cookie", "CloudFront-Policy=${cloudFrontCookieValue.policy}", "Cookie", "CloudFront-Signature=${cloudFrontCookieValue.signature}", "Cookie", "CloudFront-Key-Pair-Id=${cloudFrontCookieValue.keyPairId}" ) .build() return client.send(request, responseBodyHandler) } data class CloudFrontSignedCookieValue( val policy: String, val signature: String, val keyPairId: String ) { companion object { fun create(policyStatement: String, key: CloudFrontKey): CloudFrontSignedCookieValue { val canonicalPolicyStatement = Regex("\\s").replace(policyStatement, "") val encodedPolicyStatement = encodeForCookieValue(canonicalPolicyStatement.toByteArray()) val encodedSignature = sign(canonicalPolicyStatement.toByteArray(), key.privateKey) return CloudFrontSignedCookieValue(encodedPolicyStatement, encodedSignature, key.keyPairId) } private fun sign(base: ByteArray, privateKey: PrivateKey): String { val signatureInstance = Signature.getInstance("SHA1withRSA") // すべての Java 実装で SHA1withRSA がサポートされる。 Java SE 8 のドキュメントで確認。 signatureInstance.initSign(privateKey) signatureInstance.update(base) val signature = signatureInstance.sign() return encodeForCookieValue(signature) } private fun encodeForCookieValue(value: ByteArray) = Base64.getEncoder().encodeToString(value) .replace('+', '-') .replace('=', '_') .replace('/', '~') } } data class CloudFrontKey( val keyPairId: String, val privateKey: PrivateKey )
AWS のドキュメントには 『エンコーダが正常に機能するように、Bouncy Castle の Java 用暗号 API の jar をプロジェクトに追加してから Bouncy Castle プロバイダを追加します。』 と書かれているが、Java SE に含まれるエンコーダだけで問題なく署名できるはず。
AWS SDK for Java
AWS SDK for Java に CloudFrontCookieSigner
というクラスがあり、これを使うことで signed cookie を生成することも可能。 通常はこれを使うのが良さそう。
kotlinx.coroutines 0.26.0 でコルーチンの構造化 (structured concurrency) がやりやすくなった
この記事は Recruit Engineers Advent Calendar 2018 の 10 日目の記事です。 今日は Kotlin におけるコルーチンの構造化された並行性 (structured concurrency) やコルーチンスコープ (coroutine scope) についてです。
昨日は古川陽介さんの 『R-ISUCON Winter 2018 解説記事 | リクルートテクノロジーズ メンバーズブログ』 でした。 明日は masahiro331 さんの予定です。
前書き : コルーチンについて
Kotlin のコルーチンについては下記記事に書きましたので、参照ください。 (Kotlin 1.1 のころの情報なので少し古くなっています。)
雑に言うと 「コルーチンとは軽量のスレッド (のようなもの) である」 という理解で良いと思います。
kotlinx.coroutines 0.26.0 での並行性モデルの変更
少し前の話ですが、Kotlin 1.3 がリリースされ、コルーチンが fully stable になりました *1。 さらにその少し前、kotlinx.coroutines 0.26.0 にて並行性モデルが大きく変更されました。
私は Kotlin 1.2 のころからコルーチンを使っていたのですが、Kotlin 1.3 にマイグレーションするにあたってこの並行性モデルの変更に少しはまってしまったので、調べたことを書き残しておきます。
kotlinx.coroutines 0.26.0 よりも前
kotlinx.coroutines 0.26.0 より前は、下記のようなコードを実行すると、標準出力には 「Bar」 しか表示されず 「Foo」 は表示されませんでした。
import kotlinx.coroutines.experimental.delay import kotlinx.coroutines.experimental.launch import kotlinx.coroutines.experimental.runBlocking fun main(args: Array<String>) = runBlocking { launch { delay(1000) println("Foo") } println("Bar") }
なぜなら、launch
で起動されたコルーチンはグローバルスコープのコルーチンであり、その終了を待たずに処理が runBlocking
を抜けてしまい、(アクティブなコルーチンが存在しても) プロセスが殺されてしまうからです。 ガイドでは、「グローバルスコープのコルーチンはデーモンスレッドのようなもの」 と表現されています。
Active coroutines that were launched in GlobalScope do not keep the process alive. They are like daemon threads.
kotlinx.coroutines/coroutines-guide.md at 0.26.0 · Kotlin/kotlinx.coroutines · GitHub
これを避けて、launch
の処理を最後まで終わらせるためには、val job = launch { ... }; job.join()
という感じで明示的にコルーチンの終了を待ったり、下記のように明示的にコルーチンコンテキストを指定して、launch
で起動されるコルーチンが runBlocking
のコルーチンの子になるようにするなどの必要がありました。
fun main(args: Array<String>) = runBlocking { launch(kotlin.coroutines.experimental.coroutineContext) { delay(1000) println("Foo") } println("Bar") }
すなわち、明示的に記述しなければコルーチンは構造化されませんでした。 この挙動での問題として、issue では下記の例が書かれています。
suspend fun loadAndCombineImage(name1: String, name2: String): Image { val image1 = async { loadImage(name1) } val image2 = async { loadImage(name2) } return combineImages(image1.await(), image2.await()) }
上記のコードでは、image1.await()
が例外を送出した場合に、image2
のコルーチンがキャンセルされずに放置されてしまいます。 仮に async(coroutineContext)
を使ったとしても、親コルーチンが広すぎて適切ではありません。 本来は、loadAndCombineImage
で一つのスコープを形成し、画像読み込み処理をそのスコープの子にする、という風にコルーチンを構造化する必要があります。
kotlinx.coroutines 0.26.0 でコルーチンを構造化しやすくなった
コルーチンの構造化をやりやすくするために、launch
や async
といったコルーチンビルダーが CoroutineScope
の拡張関数に変更されたり、coroutineScope
という関数が導入されたりしました。
例えば、下記のように書くと、launch
で起動されるコルーチンは自動的に runBlocking
のコルーチンの子になるので、処理は launch
のコルーチンの終了を待ってから runBlocking
を抜けるようになります。
fun main(args: Array<String>) = runBlocking { launch { delay(1000) println("Foo") } println("Bar") }
loadAndCombineImage
の例については、下記のように書くことで loadAndCombineImage
の処理に対応するコルーチンスコープが形成され、例外送出時には自動的に子の画像読み込み処理がキャンセルされるようになります。
suspend fun loadAndCombineImage(name1: String, name2: String): Image = coroutineScope { val image1 = async { loadImage(name1) } val image2 = async { loadImage(name2) } combineImages(image1.await(), image2.await()) }
便利ですね。
グローバルスコープのコルーチンを起動したい場合には GlobalScope
オブジェクトを使用します。
Kotlin 1.3 へのマイグレーション時にはまったところ
私が Kotlin 1.3 にマイグレーションするときにはまったのは、もともとグローバルスコープのコルーチンを起動していた箇所が、意図せずに別のコルーチンの子になるように変更されてしまった箇所でした。 雑な例ですが、もともと kotlinx.coroutines 0.25.0 で下記のようなコードを書いていたとします。
launch { val v = async { ... } }
kotlinx.coroutines 0.26.0 以降にマイグレーションする際には、
GlobalScope.launch { val v = GlobalScope.async { ... } }
という風に書き換えないといけません。 上記の例の場合、launch
メソッドの方は他のコルーチンスコープに属していないためマイグレーション時にコンパイルエラーが発生するので GlobalScope
のつけ忘れをすることはないのですが、上記の例の async
メソッドのようにもともと他のコルーチンスコープの中で使われているコルーチンビルダーについては GlobalScope
を付けなくてもコンパイルエラーにならないため、GlobalScope
のつけ忘れをしてしまって意図せぬ挙動になる可能性があります。 私はこれにはまりました。
皆様もお気を付けください。
ガイド
本記事では kotlinx.coroutines 0.26.0 での変更点を主に説明しましたが、新たにコルーチンを学ぶ場合は下記のようなガイドを読むと良いと思います。
また、CoroutineScope
のドキュメントも参考になります。 Android の Activity のようにライフサイクルが明確に定まっているものに CoroutineScope
を紐づけることで、そのライフサイクルの中で管理したいコルーチンを扱いやすくなります。
コルーチン、いい感じに使っていきましょう。
Gradle 4.9 の新しいタスク定義 API と Gradle Kotlin DSL 1.0 RC での対応
Gradle 4.9 の新しいタスク定義 API
Gradle 4.9 で、新しいタスク定義 API が導入されました。 まだ incubating です。
パフォーマンス向上のため、タスクの設定を遅延実行する、というのがこの新しい API の導入の目的のようです。 古い API では Task
インスタンスをすぐに生成していたのを、新 API ではまず Provider<Task>
を生成して、必要になった段階で Task
インスタンスを生成する、という形になります。
Gradle Kotlin DSL 1.0 RC での対応
ちょうど昨日 Gradle 4.10 がリリースされましたね! めでたい!
Gradle 4.10 is out! https://t.co/wmo5e8eb8y
— Gradle (@gradle) 2018年8月27日
⚡️ Incremental @java compilation by default
📤 Periodic Gradle cache cleanup
🚀 @Kotlin DSL 1.0 RC
📦 Plugins DSL supports SNAPSHOT versions pic.twitter.com/5oNXRYJgra
Gradle 4.10 には Gradle Kotlin DSL 1.0-RC3 が載っており、Kotlin DSL の方でも新しいタスク定義 API がサポートされています。
Release 1.0-RC3 · gradle/kotlin-dsl · GitHub
- introducing the new existing and registering delegated properties designed specifically with configuration avoidance in mind which expose
NamedDomainObjectProvider<T>
instead of the container element type.
下記のように使用できます。
// 既存タスクの設定。 val test by tasks.existing(Test::class) { ... } // 新規タスクの宣言と設定。 val jacocoMerge by tasks.registering(JacocoMerge::class) { ... }
使ってみた
早速使ってみました。
Update Gradle (version 4.10) by nobuoka · Pull Request #13 · nobuoka/wd-image-processor · GitHub
小さいプロジェクトなので特にパフォーマンスの向上などは感じられませんが、移行自体は苦労なくできます。
Gradle のマルチモジュールプロジェクトで JaCoCo の結果を集計する
Java / Kotlin のコードカバレッジツールとして JaCoCo を使いたい。 Gradle のマルチモジュールプロジェクトでの JaCoCo の導入について記す。
(この図は JaCoCo によるコードカバレッジの集計結果の履歴を Codecov で表示した例。)
JaCoCo について
JaCoCo は、JVM 言語のコードカバレッジツールである。 Java Agent によるオンザフライ方式のバイトコード instrumentation によるカバレッジ計測が可能である。
以前、Kotlin のコードカバレッジツールについて書いたので、こちらも参考にどうぞ。
Gradle プロジェクトで JaCoCo を使ったカバレッジ計測を行う
Gradle の JaCoCo プラグインを使うことで、Gradle のプロジェクトで JaCoCo を使ったコードカバレッジ計測を手軽に行うことができる。 ちなみに Gradle 4.9 の段階ではこのプラグインは incubating である。
Junit 5 のテスト実行時にカバレッジを計測する
各モジュールのテスト実行時にコードカバレッジを計測するだけなら非常に簡単で、単に JaCoCo プラグインを適用するだけで良い。
While all tasks of type
The JaCoCo Plugin - Gradle User ManualTest
are automatically enhanced to provide coverage information when thejava
plugin has been applied, any task that implementsJavaForkOptions
can be enhanced by the JaCoCo plugin. That is, any task that forks Java processes can be used to generate coverage information.
バージョンを表す変数の定義や repositories
の定義などは省略しているが、おおよそ以下のようなビルドスクリプトで JUnit 5 でのテスト実行時に JaCoCo によるコードカバレッジの計測がなされる。
apply plugin: 'kotlin' apply plugin: 'jacoco' test { useJUnitPlatform() } dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" testCompileOnly("org.junit.jupiter:junit-jupiter-api:$junit_version") testRuntime("org.junit.jupiter:junit-jupiter-engine:$junit_version") }
デフォルトでは、カバレッジの計測結果は build/jacoco/test.exec というファイルに出力されるようである。
複数モジュールのコードカバレッジの集計
難しいのはここからで、複数モジュールでのコードカバレッジをどう集計するかで私は結構悩んだ。 JaCoCo の結果集計のためのタスクとしては JacocoMerge
というのがあるのだが、これをどう定義するのかが難しい。 (どのサブモジュールのカバレッジを集計するのかを明示的に指定するのであればそれほど大変ではないが、自動で全サブモジュールのカバレッジを集計するためのタスク定義をするのが難しい。)
結論としては、ルートプロジェクトに以下のようなタスク定義を行うことで対応した。 (ルートプロジェクトにも jacoco
プラグインを適用している。)
task jacocoMerge(type: JacocoMerge) { // ルートプロジェクトの評価時にはまだサブプロジェクトは存在しないので、 // 各プロジェクトの評価完了時に中の処理が走るように。 gradle.afterProject { p, state -> // ルートプロジェクトと `jacoco` プラグインが適用されていないプロジェクトは除く。 if (p.rootProject != p && p.plugins.hasPlugin('jacoco')) { executionData p.tasks.test.jacoco.destinationFile dependsOn(p.tasks.test) } } }
大雑把に説明すると、jacocoMerge
タスクの executionData
に各サブモジュールのテスト実行時のカバレッジ計測結果を渡していき、かつ、jacocoMerge
タスクの依存に各サブモジュールの test
タスクを追加する、ということをしている。
gradle.afterProject
を使っている理由や if
文を使っている理由はコメントに書いてあるとおりである。
Gradle のプロジェクトの評価順については下記ページを参照されたい。
集計結果のコードカバレッジのレポーティング
さらにレポーティングのタスクは、ルートプロジェクトに下記のように定義することで行える。 java
プラグインが適用されている全サブモジュールのソースファイルをコードカバレッジのレポート対象に含める場合の例である。
task jacocoMergedReport(type: JacocoReport, dependsOn: [tasks.jacocoMerge]) { executionData jacocoMerge.destinationFile sourceDirectories = files() classDirectories = files() gradle.afterProject { p, state -> if (p.rootProject != p && p.plugins.hasPlugin('java')) { sourceDirectories = sourceDirectories + files([p.sourceSets.main.allJava.srcDirs]) classDirectories = classDirectories + p.sourceSets.main.output } } reports { xml.enabled = true xml.destination file("${buildDir}/reports/jacoco/report.xml") html.destination file("${buildDir}/reports/jacoco/html") } }
本当は JacocoReport#sourceSets
を使いたかったが、内部で gradle.afterProject
を使われていて期待する挙動にならなかったので諦めた。
ちなみに上の xml.destination
の値は Codecov が期待するレポートファイルの位置である。
参考
- Gradleのマルチプロジェクト構成でJUnitやJacocoのレポートを集計する : プロジェクトの評価順を気にしなくてよければ
gradle.afterProject
を使わずにこういう感じで書ける。 - Aggregated Jacoco reports in a multi-project Gradle build · GitHub : こちらもプロジェクトの評価順を気にしなくてよいように書いた場合の例。