Kotlin のコルーチン (coroutines) について学ぶ
KotlinConf 2017 の情報を追ったり、Ktor を見たりしているとコルーチンがよく出てくる。 コルーチンについては概要は知っているが詳細を追いかけていなかったので、コルーチンについて学んでメモ程度に記録しておく。
Kotlin 1.1 においてコルーチンは実験段階で、将来にはこのページの情報は古くなっているかもしれないので注意されたし。
本記事に書かれている内容
- コルーチンとは何か
- コルーチンの実装がどこにあるのか (言語サポートと標準ライブラリと外部ライブラリ)
- コルーチンの基本的な使い方
- コルーチンのキャンセル処理について
- コルーチンコンテキストについて
- チャンネル
- 並行性の問題
- Select 文
Coroutine (コルーチン) って何?
まずは公式リファレンスの情報を追う。
- 公式リファレンス : Coroutines - Kotlin Programming Language
コルーチンは軽量なスレッドのようなもの。 スレッドの場合は、非同期処理を行う際に呼び出し側はスレッドをブロック (blocking) して待機するが、コルーチンの場合は非同期処理の呼び出しでコルーチンを中断 (suspension) することができる。 コルーチンの中断は、スレッドのブロックと比べて安く、より制御しやすい。
Kotlin 言語やライブラリとコルーチンの関係
言語機能 : suspending 関数 (suspending functions)
コルーチンに関する言語機能として、suspending 関数がある。 suspend
修飾子が付けられた関数である。
suspend fun foo(): Bar { ... }
このような関数の呼び出し時に、コルーチンの中断が発生する可能性がある。 (中断されない可能性もある。) Suspending 関数を呼ぶことができるのは、コルーチンの中や他の suspending 関数の中からだけである。
無名ラムダも suspending 関数になりえる。 kotlinx.coroutines
に含まれる async
関数の宣言は以下のようになっており、引数のラムダは suspending 関数である。
fun <T> async(block: suspend () -> T)
上記関数に渡したラムダは suspending ラムダとなる。
コルーチンの低レベルなコア API について
コルーチンの低レベルなコア API は、主にコルーチンを扱うライブラリのためのもので、基本的にはアプリケーションコードでは使わない。 (buildSequence
と buildIterator
だけはアプリケーションコードからの使用が想定されているらしい。)
低レベルなコア API についての詳細は以下にある。
コルーチンの使い方 (高レベル API)
kotlinx.coroutines
のリポジトリの中に詳しいドキュメントがあるので、それを読んでいく。 個人的にはコルーチンについて学ぶのに最初に読むドキュメントとしてはこれが一番わかりやすいと思う。
最初の例
最初の例として、launch
関数が使われた例が書かれている。
fun main(args: Array<String>) { launch { // 新しいコルーチンの起動。 // スレッドを使う場合のスレッドの作成・開始に相当する。 delay(1000L) // コルーチンの中断 (1 秒間)。 // スレッドを使う場合の `Thread.sleep(1000L) 相当だが、 // スレッドを使う場合と違って非ブロッキング。 println("World!") // ここに処理が来るのは 1 秒間の中断の後。 } println("Hello,") // 上記コルーチンが中断していてもここに処理は来る。 Thread.sleep(2000L) // アプリケーション全体が終了してしまうのを防ぐ。 }
スレッドとの対比を考えるとわかりやすいだろう。
最後に Thread.sleep
しているのは、コメントにあるようにアプリケーション全体が終了してしまうことを防ぐためである。 コルーチンはデーモンスレッドのような感じあり、アクティブなコルーチンが存在していてもそれによってプロセスが生き続けるわけではない。
上記の例では main
関数全体はコルーチン上で動かされていない (ので Thread.sleep
が使われている) が、全体をコルーチンで動かすために runBlocking
関数が紹介されている。 新しいコルーチンを起動し、コルーチンの処理が完了するまで現在のスレッドをブロックする、というもの。 コルーチンを使った処理とそうでない処理の橋渡しのために設計された関数である。
fun main(args: Array<String>) = runBlocking<Unit> { launch { /* ... この中は上の例と同じ ... */ } println("Hello,") // 上記コルーチンが中断していてもここに処理は来る。 delay(2000L) // アプリケーション全体が終了してしまうのを防ぐ。 }
上記の例では launch
で起動したコルーチンを待つために delay(200)
しているが、本来はコルーチン上の処理完了を明示的に待ちたい。 スレッドで Thread#join
するのと同じように、launch
の返り値である Job
の join
メソッドを呼ぶことで、コルーチンの処理が完了するのを (非ブロッキングに) 待つことができる。
fun main(args: Array<String>) = runBlocking<Unit> { val job = launch { /* ... この中は上の例と同じ ... */ } println("Hello,") // 上記コルーチンが中断していてもここに処理は来る。 job.join() // 上記コルーチンの処理完了を待つ。 }
Suspending 関数の導入
ここまでの例では全て suspending ラムダを使ってきたが、実際のコードでは関数やメソッドとして処理を記述したいことが多い。 この記事の最初の方で紹介した suspending 関数として記述できる。
fun main(args: Array<String>) = runBlocking { val job = launch { sayWorld() } println("Hello,") // 上記コルーチンが中断していてもここに処理は来る。 job.join() } suspend fun sayWorld() { delay(1000L) // コルーチンの中断 (1 秒間)。 println("World!") // ここに処理が来るのは 1 秒間の中断の後。 }
キャンセルとタイムアウト
Job#cancel
メソッドを使ってコルーチンのキャンセル処理が可能。
キャンセル処理は協調的な処理である。 すなわち、コルーチンの処理がキャンセル処理に対応していなければならない。
具体的に言うと、下記のようなコードを書くとコルーチンの処理の中でキャンセルされるタイミングがないために、キャンセルがリクエストされてもコルーチンの処理が最後まで続いてしまう。
fun main(args: Array<String>) = runBlocking { val job = launch { sayWorld() println("Complete!") // `sayWorld` 処理中にキャンセルリクエストされても、 // `sayWorld` がキャンセルに対応してないのでここも処理される。 } println("Hello,") job.cancelAndJoin() } suspend fun sayWorld() { val startTimestamp = System.currentTimeMillis() while (System.currentTimeMillis() < startTimestamp + 5000L) { // Computing... // キャンセル処理に対応していないので、 // キャンセルがリクエストされても 5 秒間動き続ける } println("World!") // キャンセルされるタイミングがないので、 // キャンセルがリクエストされてもここも処理される。 }
キャンセルに対応する一つの方法としては、定期的にキャンセルに対応している suspending 関数を呼ぶことである。 例としては yield
関数が挙げられている。 他の方法として、自分でキャンセルされているかどうかを明示的に確認する、というものもある。
ちなみにキャンセルに対応した suspending 関数は、キャンセルされた際に CancellationException
例外を送出する。 そのため、try-finally
による終了処理を書いておけば、キャンセル時にも終了処理が行われる。
ちなみにキャンセルされたコルーチンから suspending 関数を呼ぶと、(既にキャンセルされているので) CancellationException
例外が送出されてしまう。 なので、めったにないことではあるが、終了処理で suspending 関数を呼ぶことは (普通にやろうとしても) 不可能である。 この問題に対応するには、run
関数 に NonCancellable
コンテキストを渡し、処理を実行してやる必要がある。
コルーチンをキャンセルしたい理由としてタイムアウト処理があるので、withTimeout
関数というものも用意されている。
async/await
TypeScript や ECMAScript を使っている人には async
/await
キーワードはなじみ深いものだと思う。 Kotlin でもライブラリで async
関数や await
メソッドといったものが提供されているが、コルーチンとの関係がいまいちよくわかっていなくて自分にとっては混乱のもとだった。
async
関数は、launch
関数と同じで新しいコルーチンを起動するものである。 launch
関数とは違って、返り値として Deferred
が返される。 JS 界隈の人にとっては Deferred
や Promise
というと馴染み深いであろう。 コルーチンから値を受け取ることができるのである。
そして、Deferred
からの値の取り出しに使われるのが Deferred#await
メソッドである。 この値の取り出しの待ち合わせも非ブロッキングである。
// 下記のようにコルーチン上で新しい非同期処理を開始して待合せたり val deferredValue1 = async { /* 何らかの非同期処理 */ } val deferredValue2 = async { /* 何らかの非同期処理 */ } println("${deferredValue1.await()}, ${deferredValue2.await()}") // 下記のように関数定義を行って、 fun asyncFoo() = async { /* 何らかの非同期処理 */ } // コルーチンから使ったり、 val fooValue = asyncFoo().await() // コルーチンの外で使ったりできる val deferredFooValue = asyncFoo() // ただし await メソッドは suspending 関数なので // コルーチンの外では使えない
上のような使い方を見ると JS の async
/await
をより柔軟に使えるようにしたもの、というような印象を受けるが、実際はもう少しややこしい気がしている。 JS ではメインのイベントループが 1 つ回っているだけなので、async 関数の本体の処理と呼び出し側の処理は同じスレッド的なものの上で動く。 一方で、Kotlin の場合に JS のイベントループに相当するものがコルーチンだと考える *1 と、単純にコルーチン内で suspending 関数を呼び出すのが JS の async
/await
との対比になるような気がする。
つまり、TypeScript で以下のように書くのが、
// 下記のような関数を async function asyncFoo(): string { /* ... */ } // 下記のように使用する let foo = await asyncFoo();
Kotlin における下記のコードに相当する、という考え方もできる。
// 下記のような suspending 関数を suspend fun foo(): String = /* ... */ // コルーチン内で下記のように使用する val foo = foo()
ともかく、JS 界隈の async
/await
との対比で理解しようとするよりは、コルーチンの仕組みをおさえた方が理解しやすいと感じた。
コルーチンコンテキストとディスパッチャ
- コルーチンコンテキストとディスパッチャの詳細 : kotlinx.coroutines/coroutines-guide.md at master · Kotlin/kotlinx.coroutines · GitHub
- コルーチンは、
CoroutineContext
で表現されるコンテキストで実行される。- コルーチンコンテキストは、
マップと集合によって値を持つコンテキスト要素 (CoroutineContext.Element
) の (インデックス付きの) 集合である。- インデックス付きというのは、各要素を
Key
インスタンスによって参照可能であるということ。 例えばコルーチンコンテキストに含まれるジョブ要素を取得するにはcoroutineContext[Job]
とすればよい。 (マップと言ってしまって良いと思われる。)
- インデックス付きというのは、各要素を
- (
CoroutineContext.Element
がCoroutineContext
を継承してるのは設計がイケてないという気がする……。)
- コルーチンコンテキストは、
- コルーチンコンテキストはジョブの情報を持っているし、コルーチンディスパッチャ (
CoroutineDispatcher
) の情報も持っている。coroutinContext[Job]
って感じでコンテキストからジョブ情報を取れる。- コルーチンディスパッチャは、どのスレッド (またはスレッド群) でコルーチンが実行されるかを決めるもの。
launch
やasync
のようなコルーチンビルダは、オプションでコルーチンコンテキストを受け取る。- デフォルトで使用されるディスパッチャは
DefaultDispatcher
で、現在の実装ではCommonPool
ディスパッチャと同じ。 - 親のコルーチンと同じコンテキストを使いたい場合は
coroutineContext
で参照すればよい。 Unconfined
ディスパッチャというものもあり、これはコルーチンを開始したり再開したりしたディスパッチャ上で動くもの。 つまり、コルーチンが中断して再開した場合、中断前と再開後で別のディスパッチャによって動かされる可能性がある。
- デフォルトで使用されるディスパッチャは
- コルーチン内でコンテキストを変化させることもできる :
run
関数 - コルーチンに親子関係を持たせたい場合は、親コルーチンのコンテキストを渡せばよい。 (コンテキストとしてジョブを指定してやればそれだけで良さそう。 詳細は下記)
- コルーチンに親子関係があるとき、親コルーチンの終了は子のコルーチンの終了を待つし、親コルーチンがキャンセルされたときは子のコルーチンもキャンセルされる。
- コンテキストは
+
演算子で合成できる。 右側にあるコンテキストが左側のコンテキストの関係する箇所を置き換える。 (置き換えられなかったものは引き継がれる。) - デバッグの話
- JVM オプションに
-Dkotlinx.coroutines.debug
を追加すると、スレッド名にコルーチン名が追加される。 CoroutineName
コンテキスト要素を使うことで、コルーチンの名前を指定できる。
- JVM オプションに
コルーチンの親子関係
上で説明したように、launch
関数などに指定するコンテキストにジョブが含まれていると、そのジョブが親コルーチンとなる。 そして、親コルーチンの終了は子のコルーチンが終了するのを待つ。
val job = launch { println("Parent coroutine") launch(coroutineContext) { // coroutineContext に親となるコルーチンのジョブが含まれている delay(1000L) println("Child coroutine") } } job.join() // ここに処理が来る前に 「Child coroutine」 は出力される。
また、親のコルーチンがキャンセルされると子のコルーチンもキャンセルされることを応用して、Android の Activity などのライフサイクルに紐づけてコルーチンをキャンセルさせたい場合などに、ライフサイクルに紐づくジョブを作っておいて、それを親にするという手法を取ることができる。
// Activity のライフサイクルに紐づくジョブ val activityRelatedJob = Job() val networkJob = launch(activityRelatedJob) { // 非同期通信など } // Activity 終了時に親ジョブをキャンセルすることで子も全てキャンセルできる。 activityRelatedJob.cancel()
チャンネル (Channels)
- チャンネルの詳細 : kotlinx.coroutines/coroutines-guide.md at master · Kotlin/kotlinx.coroutines · GitHub
- Deferred は単一の値をコルーチン間でやりとりする便利な方法。 値のストリームをコルーチン間でやり取りするのに使えるのがチャンネル。
Channel
はBlockingQueue
のようなもの。 主要な違いは値の追加と取り出しが suspending 関数になっていること。- それ以上値がないことを示すためにチャンネルを閉じることができる。 受け取り側は
for
ループで受け取ることができる。 - Producer-consumer パターンとして一般的なパターン。
- チャンネルを生成する便利関数として
produce
関数がある。 - 受け取り側の便利拡張関数としては
consumeEach
メソッド がある。
- チャンネルを生成する便利関数として
- あるコルーチンが (おそらく無限に) 値のストリームを流し続けて、他のコルーチンが値を消費したり値に変換をかけたりするパイプラインというパターンもよく使われる。
- バッファのないチャンネルの場合、送信が先に呼ばれると受信が呼ばれるまで送信処理が中断されるし、受信が先に呼ばれると送信が呼ばれるまで受信が中断される。
- チャンネル作成時にバッファの大きさを指定できる。
- 複数のコルーチンが受信や送信を呼んで中断している場合、先に呼んだものから順に値を受け取ったり送信したりできる。 (Channels are fair)
変更可能な状態の共有と並行性
- 詳細 : kotlinx.coroutines/coroutines-guide.md at master · Kotlin/kotlinx.coroutines · GitHub
- コルーチンも複数スレッド上で動きうるので、並行性の問題がある。
- Java でのマルチスレッド用の対策も 1 つの方法。
- 特定の値を参照するのを特定のスレッドからだけにするのも 1 つの方法。 (Thread confinement)
- 例えば UI に関するオブジェクトは UI スレッドからしか参照しないようにする、とか。
Mutex
による排他制御という方法もある。- これはスレッドの世界における
synchronized
やReentrantLock
相当のもの。 Mutex
は非ブロッキング。
- これはスレッドの世界における
- コルーチンと状態、そして他のコルーチンとやり取りするためのチャンネルをまとめたアクター (actor) という概念を用いても良い。
- アクターを生成するための
actor
コルーチンビルダが用意されている。 - 状態を触るのをアクターに限定することで並行性の問題を解消する。
- アクターを生成するための
おすすめ書籍
JVM 上での並行性・マルチスレッド対応については、下記の書籍がおすすめである。
Java並行処理プログラミング ―その「基盤」と「最新API」を究める―
- 作者: Brian Goetz,Joshua Bloch,Doug Lea
- 出版社/メーカー: ソフトバンククリエイティブ
- 発売日: 2006/11/22
- メディア: 単行本
- 購入: 30人 クリック: 442回
- この商品を含むブログ (174件) を見る
書評も書いたので参考にどうぞ。
Select expression
- 詳細 : kotlinx.coroutines/coroutines-guide.md at master · Kotlin/kotlinx.coroutines · GitHub
- 呼び出すと中断する複数の suspending 関数を同時に待ち、利用可能になった最初の一つを選択する、ということもできる。
select
関数を利用する。Job#join
に対応する select 文を表すJob#onJoin
プロパティや、ReceiveChannel#receive
に対応する select 文を表すReceiveChannel#onReceive
プロパティがある。select
に渡すラムダの中でそれらの select 文を使って利用可能になったときに実行される処理を定義していく。
例
ドキュメントからの引用。
suspend fun selectFizzBuzz(fizz: ReceiveChannel<String>, buzz: ReceiveChannel<String>) { select<Unit> { // <Unit> means that this select expression does not produce any result fizz.onReceive { value -> // this is the first select clause println("fizz -> '$value'") } buzz.onReceive { value -> // this is the second select clause println("buzz -> '$value'") } } }
どういう仕組みなのか初見ではわからなかったので簡単に解説しておく。
fizz.onReceive
は、上で説明したように ReceiveChannel#receive
に対応する select 文 (SelectClause1
オブジェクト) を返す。 そして、fizz.onReceive
と後続のブロック部分は、省略せずに書くと fizz.onReceive.invoke({ ... })
という形式になっている。
SelectBuilder
に SelectClause1#invoke
拡張関数が定義されている。 select
関数が受け取る引数の定義が SelectBuilder.() -> Unit
なので、fizz.onReceive.invoke({ ... })
というコードを記述できるのである。
詳細は別記事に書いた。
終わり
というわけで、Kotlin のコルーチンについて、下記のドキュメントを見ながら学んだことをまとめてみた。
- 公式リファレンス : Coroutines - Kotlin Programming Language
- ライブラリのガイド : kotlinx.coroutines/coroutines-guide.md at master · Kotlin/kotlinx.coroutines · GitHub
この 2 つのドキュメントを読むことで、コルーチンについて基本的な部分はおおよそ理解できるだろう。 本記事が、皆さんがコルーチンを理解するための一助となれば。
その他参考になるページ
- 実例によるkotlinx.coroutinesの手引き(日本語訳) - Qiita : 本ページでも参考にしたガイドの日本語訳。
- Androidの非同期処理をKotlinコルーチンで行う // Speaker Deck : Android にコルーチンを導入するのに参考になる。
*1:それはそれで正しくなくて、より正確にはイベントループはコルーチンコンテキストの一種だと考えるのが対比としては一番良い気はする
Kotlin で拡張関数をオーバーライドして実装を切り替えられるぞ!
背景 : コルーチンの Select 式の実装を理解するのが難しかった
コルーチンのドキュメントを読んでいて select
関数というのが出てきたのだけど、これの実装がどうなっているのかすぐにはわからなかった。
suspend fun selectFizzBuzz(fizz: ReceiveChannel<String>, buzz: ReceiveChannel<String>) { select<Unit> { // <Unit> means that this select expression does not produce any result fizz.onReceive { value -> // this is the first select clause println("fizz -> '$value'") } buzz.onReceive { value -> // this is the second select clause println("buzz -> '$value'") } } }kotlinx.coroutines/coroutines-guide.md at master · Kotlin/kotlinx.coroutines · GitHub
fizz.onReceive
は SelectClause1<E>
型のプロパティなのだけど、その後ろのラムダが何なのかぱっと見はわからなかったのである。 (SelectClause1<E>
型には invoke
は定義されていない。)
select
関数のシグネチャは以下。 引数の関数型はレシーバ付きで、レシーバの型は SelectBuilder
である。
public inline suspend fun <R> select(crossinline builder: SelectBuilder<R>.() -> Unit): R
この SelectBuilder
を見ると、下記のような拡張関数が定義されていた。 これらが fizz.onReceive { /* ... */ }
の実体なのであった。
public operator fun <Q> SelectClause1<Q>.invoke(block: suspend (Q) -> R)
しかも SelectBuilder
はインターフェイスで、拡張関数は別のクラスで実装されていた。
学び
拡張関数をオーバーライドできる
Extensions declared as members can be declared as
Extensions - Kotlin Programming Languageopen
and overridden in subclasses.
拡張関数をメンバーとして定義できるのは知ってたのだけど、オーバーライドできるとは知らなかった!
interface StringExtensionScope { fun String.bar(): String } class StringExtensionScopeImpl : StringExtensionScope { override fun String.bar() = this + " bar" }
そして実行時に実装を変更できる
拡張関数を定義した型をレシーバとするレシーバ付きの関数型を使うと、ラムダ内をスコープにして拡張関数を有効にできる! 実行時に実装を変更することも可能!
val printFooBar: StringExtensionScope.() -> Unit = { // StringExtensionScope で定義されている拡張関数を利用できる。 println("foo".bar()) } // そして実行時に拡張関数の実装を変更できる。 printFooBar(StringExtensionScopeImpl())
拡張関数といえば 「静的に解決されるものである」 という印象だったのでオーバーライドして実行時に実装を切り替えられるとは思っていなかったけど実際は切り替えることができる。 ちなみに拡張レシーバ (extension receiver) の型はやはり静的に見られるので、拡張レシーバの型に応じて動的に実装を切り替えるということはできない。
This means that the dispatch of such functions is virtual with regard to the dispatch receiver type, but static with regard to the extension receiver type.
Extensions - Kotlin Programming Language
終わり
小ネタだけど、拡張関数は常に静的に解決されるものだと思ってたのでちょっとびっくりした。
Kotlin も拡張関数と operator とレシーバ付き関数型が組み合わさってくると結構コード追いづらくなるなーと感じる。 (まあ Scala とかと比べるとまだまだ追いやすい方だとは思うけど。)
Java SE 9 の javac で過去バージョンをターゲットにするときには --release オプションを使用すると良い
JDK の javac
コマンドを使って過去バージョンの Java 言語で書かれたソースコードをコンパイルする際には、-source
オプションや -target
オプションを使用してきました。 (JDK 8 までの話。) このとき、適切なブートストラップクラスパスを設定しなければ、対象バージョンに存在しない Java API を使用していてもビルドが通ってしまう、という問題がありました。
JDK 9 の javac
コマンドには --release
オプションが追加されました。 今後は (基本的には) このオプションを使用するようにすると良さそうです。
- ドキュメント : javac
Java SE 9 (Oracle JDK 9) のマイグレーションガイドより
マイグレーションガイドには以下のように書かれています。
If you use the
-source
and-target
options withjavac
, then check the values that you use. In JDK 9,javac
uses a "one plus three back" policy of supporting-source
and-target
options.The supported
-source
/-target
values are 9 (the default), 8, 7, and 6 (6 is deprecated, and a warning is displayed when this value is used).In JDK 8,
-source
and-target
values of 1.5/5 and earlier were deprecated and caused a warning to be generated. In JDK 9, those values cause an error.>javac -source 5 -target 5 Sample.java warning: [options] bootstrap class path not set in conjunction with -source 1.5 error: Source option 1.5 is no longer supported. Use 1.6 or later. error: Target option 1.5 is no longer supported. Use 1.6 or later.If possible, use the new
--release
flag instead of the-source
and-target
options. The--release N
flag is conceptually a macro for:-source N -target N -bootclasspath $PATH_TO_rt.jar_FOR_RELEASE_NThe valid arguments for the
--release
flag follow the same policy as for-source
and-target
, one plus three back.javac can recognize and process class files of all previous JDKs, going all the way back to JDK 1.0.2 class files.
See JEP 182: Policy for Retiring
Java Platform, Standard Edition Oracle JDK 9 Migration Guide, Release 9javac
-source
and-target
Options.
つまり、--release
フラグを指定すると、-source
オプションと -target
オプションを指定し、さらに適切なブートクラスパスの指定も行った状態になります。 これまで、過去バージョンのブートクラスパスを設定するには、過去バージョンの Java 実行環境を取得してその中の rt.jar を指定してやる必要があったので、格段に便利になりました。
試してみた
実際に、以下のようなコードを含む Java のソースファイルをコンパイルしてみました。
public class Main { public static void main(String[] args) { String test = String.join("", ""); // Java SE 8 で Java API に導入されたメソッド。 } }
--release 8
を指定した場合は、問題なくコンパイルされます。
~\java-project> javac --release 8 .\Main.java
--release 7
を指定した場合は、下記のようにエラーになります。
~\java-project> javac --release 7 .\Main.java .\Main.java:3: エラー: シンボルを見つけられません String test = String.join("", ""); ^ シンボル: メソッド join(String,String) 場所: クラス String エラー1個
Java 7 向けにビルドしたけど Java 7 の実行環境で動かすと (Java 7 にない API を使っていて) 例外が発生する、というようなミスが減りそうで最高ですね!
Gradle での使い方
Gradle 4.2.1 現在、JavaCompile
タスクで javac
の --release
オプションを指定する方法として特別なメソッドは提供されていません。 通常のコンパイラオプションを指定する方法は使用できるので、通常のコンパイラオプションの指定と同じ方法で --release
オプションを指定します。
具体的には CompileOptions#compilerArgs
プロパティを使用します。 このドキュメントを読むと、『For example, it is possible to pass the --release
option of JDK 9』 とか 『Note that if --release
is added then -target
and -source
are ignored.』 とか書かれています。 --release
オプションにもばっちり対応されていますね。
超単純な build.gradle の例を書いておきます。
apply plugin: 'java' tasks.withType(JavaCompile) { options.compilerArgs.addAll(['--release', '8']) }
Java 9 のモジュールシステム (JPMS; JSR 376) の概要
Java SE 9 がリリースされましたね! めでたい!
さてさて、Java SE 9 の目玉といえばやはり The Java Platform Module System (JPMS; JSR 376) ですよね! Project Jigsaw の心臓部です。
この記事では JSR 376 をさらっと読んで、JPMS の全体像をまとめておきます。 (実際の使い方などはこの記事の範囲外です。) モジュールシステムの理解への取っ掛かりとして皆さんの一助となれば幸いです。
仕様 (JSR 376) 概要
The Java Platform Module System (JPMS) の目的とその手段
JSR 376 に書かれているとおりの内容を日本語にしてます。
目標
JPMS の目標は、親しみやすく、それでいてスケーラブルなモジュールシステムを定義すること。
JPMS の全体像
JPMS の詳細は Java 言語仕様や JVM 仕様などの中に含まれています。 JSR 376 は、それらの仕様のどこに JPMS が影響しているのかを分かりやすく表現しています。
- Java 言語仕様と JVM 仕様
- まとめ : JPMS: Modules in the Java Language and JVM
- Java 言語仕様 (JPMS による差分が強調されている) : The Java Language Specification Java SE 9 Edition (JPMS に関する注記版)
- JVM 仕様 (JPMS による差分が強調されている) : The Java Virtual Machine Specification Java SE 9 Edition (JPMS に関する注記版)
- Java API
- JAR ファイル仕様への変更 : JPMS: Changes to the JAR File Specification
- JNI ファイル仕様への変更 : JPMS: Changes to the Java Native Interface
- JVM TI 仕様への変更 : JPMS: Changes to the JVM Tool Interface
- JDWT 仕様への変更 : JPMS: Changes to the Java Debug Wire Protocol
多くのアプリケーションエンジニアが気にするのは、特に Java 言語仕様や Java API 仕様、JAR ファイル仕様といったところだと思います。 ここら辺の内容は後で少し触れます。
JSR 376 の一部ではありませんが、次のような文書も紹介されています。
また、今回のリリースに含めなかった機能等についての説明や、変更履歴についても JSR 376 には書かれています。
Java 言語仕様における JPMS
Java 言語仕様の中の JPMS に関わる部分をさらっと読んだのでメモ程度にまとめておきます。
- 7.7 節 Module Declarations より
- モジュール宣言は、新しい名前付きモジュール (named module) を記述する。
- 名前付きモジュールは、他のモジュールへの依存や、他のモジュールに公開するものを記述する。
- モジュール宣言により、モジュール名が導入される。 モジュール名は、他のモジュールとの関係を記述するのに使用される。
- モジュール名は、1 個以上の Java 識別子をドットで連結したもの。
- モジュールにはノーマルモジュール (normal module) とオープンモジュール (open module) の 2 種類がある。
- モジュール宣言では、
java.util.ServiceLoader
によるサービスの提供あるいは利用の宣言も可能。 - 名前付きモジュールに関連付けられていないクラスなどは、無名モジュール (unnamed module) に関連付けられる。
- 6.1 節 より
- モジュール名は、モジュールが export する本質的なパッケージ名に合わせるのが良い。 それが難しい場合は、著者が持つドメイン名を逆さに並べたもので始めると良い。
JAR ファイル仕様における JPMS
こちらもメモ程度に。
- クラスパス上ではなくモジュールパス上に配置された JAR ファイルはモジュールである。
- トップレベルに module-info.class ファイルを持つ JAR ファイルは、モジュール式 JAR ファイルである。
- そうでない JAR ファイルは、自然発生的なモジュール (automatic module) とみなされる。
- その場合のモジュール名は JAR ファイル名から決められ、export されるパッケージは .class ファイルから決められる。
さあ、始めましょう
- IntelliJ IDEA : Java 9 and IntelliJ IDEA | IntelliJ IDEA Blog
- Gradle : Building Java 9 Modules
- Gradle で Java 9 のモジュールを利用するガイド。 私は試してません。
- ヌーラボのアカウント基盤を Java 9 にマイグレーションして起きた問題と解決法 | ヌーラボ
- モジュール化以外も含め、Java 9 対応についてわかりやすくまとまっています。
- モジュールシステムのスタートガイド : Project Jigsaw: Quick Start Guide
WebDriver によるスクリプト実行の現状 (geckodriver と ChromeDriver)
WebDriver とは、Web ブラウザを外部から操作するための標準化された API です。 詳細は先日書きましたのでご参照ください!
今回は、WebDriver のコマンドで JS スクリプトを実行させる方法の説明です。 W3C WebDriver API を見ながらどういう API になっているのか説明します。 また、geckodriver および ChromeDriver での現在の実装状況についても書いています。
これらは 2017 年 5 月 10 日現在の情報ですので、最新の情報は最新の W3C 勧告や Driver 実装を見てください。
スクリプトを実行させる API
W3C WebDriver での仕様
W3C WebDriver 勧告では、スクリプトを実行するための下記の 2 つのコマンドが定義されています。
- Execute Script コマンド (
POST /session/{session_id}/execute/sync
) - Execute Async Script コマンド (
POST /session/{session_id}/execute/async
)
前者は同期的なスクリプト実行をサポートし、後者は非同期的なスクリプト実行をサポートする、という風に思ってしまうところですが、なんとどちらも非同期のスクリプト実行をサポートしています! (な、なんだってー
どちらも渡されたスクリプト (リクエストボディに含まれる script
プロパティの値) を関数本体 (FunctionBody) として扱って promise-calling の形で実行するのですが、前者の方は関数の返り値をレスポンスに使うのに対して、後者の方は Promise
の resolve
を引数リストに追加したうえでスクリプトを呼び出し、関数の返り値を無視する (つまり、引数リストに追加された resolve
を関数内で呼ぶことで結果を渡す) という違いがあります。 *1
前者の方は以下のようなスクリプトを受け付けるわけですね。
var waitTime = arguments[0] || 2000; var p = new Promise(function (resolve, reject) { setTimeout(function () { resolve() }, waitTime); }).then(function () { return "Hello!"; }); return p;
後者で同様の処理を実行させるには、以下のようにする必要があります。
// 引数リストの最後に追加された resolve 関数を受け取る。 var callback = arguments[arguments.length - 1]; var waitTime = arguments[0] || 2000; var p = new Promise(function (resolve, reject) { setTimeout(function () { resolve() }, waitTime); }).then(function () { return "Hello!"; }); // 引数リストの最後に追加された resolve 関数を呼ぶことで結果を返す。 callback(p);
ちなみに関数に渡される引数は、 リクエストボディの args
プロパティで指定できます。 つまり、リクエストボディは以下の形式です。
{ "script": "return 100 + arguments[0];", "args": [200] }
JSON wire protocol での仕様
さて、どちらも非同期なスクリプト実行を扱えるのであれば、なぜ W3C WebDriver には 2 つのスクリプト実行コマンドが用意されているのでしょうか? おそらく、W3C 勧告よりも古い仕様である Selenium の JSON wire protocol から引き継いだものだと思われます。 JSON wire protocol でも 2 つのスクリプト実行コマンドが用意されていました。
これら 2 つは、(W3C 勧告の 2 つとは違って) 前者が同期的なスクリプト実行用、後者が非同期的なスクリプト実行用と、明確に役割が分かれています。 リクエストボディの型や、非同期実行での結果の返し方 (引数リストの最後に追加されたコールバック関数に値を渡す) などは W3C 勧告のコマンドと同じです。 ただし、これらは Promise
を扱えません。
JSON wire protocol でスクリプトをそのまま W3C 勧告の仕様にあった Driver 実装でも使えるように、W3C 勧告の方でも 2 種類のコマンドが定義されたのだろうと思われます。
ChromeDriver で非同期スクリプトを実行すると
普通に ChromeDriver で非同期スクリプトを実行させようとすると、以下のメッセージが返ってくることがあります。
"value": { "message": "asynchronous script timeout: result was not received in 0 seconds..." }
これは、スクリプトのタイムアウトが 0 s に設定されているためです。 タイムアウト時間を設定することができるので、先にタイムアウト時間を設定する必要があります。 POST /session/{session_id}/timeouts
というエンドポイントに、以下のようなリクエストボディで HTTP リクエストを発行しましょう。
{ "type": "script", "ms": 2000 }
geckodriver と ChromeDriver の現状
- geckodriver 0.16.1
- ChromeDriver 2.29.461571
上記 Driver のそれぞれの実装状況を見てみました。
geckodriver の実装状況
ChromeDriver の実装状況
どちらでも動かすために
WebDriverIO の実装を見ると、古い仕様のエンドポイントでコマンドを発行してみて、エラーになったら新しい仕様でコマンドを発行しなおす、ということをやっていました。
厳しい世界ですね……。