JAX-RS のリソースから送出された例外の扱い
JAX-RS のリソースのメソッドから例外が送出された場合の挙動についてちゃんと把握していなかったので調べた。
前提知識 : JAX-RS について
JAX-RS は、RESTful Web API を提供するための Java の API。 もともとは Java EE の一部。 現在は Eclipse Foundation に移管されて EE4J プロジェクトの一部 (Jakarta EE の一部) となっている。
JAX-RS のリソースから送出された例外の扱いについて
今回は JAX-RS 2.1 の仕様を参照する。
3.3.4 節に書かれている。
3.3.4 Exceptions
A resource method, sub-resource method or sub-resource locator may throw any checked or unchecked exception. An implementation MUST catch all exceptions and process them in the following order:
- 1. Instances of
WebApplicationException
and its subclasses MUST be mapped to a response as follows. If theresponse
property of the exception does not contain an entity and an exception mapping provider (see Section 4.4) is available forWebApplicationException
or the corresponding subclass, an implementation MUST use the provider to create a newResponse
instance, otherwise theresponse
property is used directly. The resultingResponse
instance is then processed according to Section 3.3.3.- 2. If an exception mapping provider (see Section 4.4) is available for the exception or one of its superclasses, an implementation MUST use the provider whose generic type is the nearest superclass of the exception to create a
Response
instance that is then processed according to Section 3.3.3. If the exception mapping provider throws an exception while creating aResponse
then return a server error (status code 500) response to the client.- 3. Unchecked exceptions and errors that have not been mapped MUST be re-thrown and allowed to propagate to the underlying container.
- 4. Checked exceptions and throwables that have not been mapped and cannot be thrown directly MUST be wrapped in a container-specific exception that is then thrown and allowed to propagate to the underlying container. Servlet-based implementations MUST use
ServletException
as the wrapper. JAX-WSProvider
-based implementations MUST useWebServiceException
as the wrapper.Note: Items 3 and 4 allow existing container facilities (e.g. a Servlet filter or error pages) to be used to handle the error if desired.
Exception mapping provider による変換
上記の説明の中に、「exception mapping provider」 というものが出てくる。 詳細は 4.4 節に書かれている。
4.4 Exception Mapping Providers
Exception mapping providers map a checked or runtime exception to an instance of
Response
. An exception mapping provider implements theExceptionMapper<T>
interface and may be annotated with@Provider
for automatic discovery.When a resource class or provider method throws an exception for which there is an exception mapping provider, the matching provider is used to obtain a
Response
instance. The resultingResponse
is processed as if a web resource method had returned theResponse
, see Section 3.3.3. In particular, a mappedResponse
MUST be processed using the ContainerResponse filter chain defined in Chapter 6.When choosing an exception mapping provider to map an exception, an implementation MUST use the provider whose generic type is the nearest superclass of the exception. If two or more exception providers are applicable, the one with the highest priority MUST be chosen as described in Section 4.1.3.
To avoid a potentially infinite loop, a single exception mapper must be used during the processing of a request and its corresponding response. JAX-RS implementations MUST NOT attempt to map exceptions thrown while processing a response previously mapped from an exception. Instead, this exception MUST be processed as described in steps 3 and 4 in Section 3.3.4.
すなわち、リソースメソッドなどから送出された例外を変換して Response
オブジェクトを生成する機能を提供するものである。
Exception mapping provider は複数登録することができる。 例外が送出されると、対応する exception mapping provider が選択され、Response
が生成されて、それがレスポンスとして使用される。 対応する exception mapping provider がない場合、非検査例外だとそのまま再送出されるし、検査例外だとコンテナに応じた非検査例外にラップされて送出されるとのこと。
WebApplicationException
について
基本的には上に書いた通りの例外ハンドリングがなされるが、リソースメソッドなどから送出された例外が WebApplicationException
の場合は、exception mapping provider を通らないことがある。 3.3.4 節の箇条書きの 1 つめがそれである。
WebApplicationException
の response
プロパティがエンティティを持っていない場合で、対応する exception mapping provider が存在する場合には exception mapping provider による Response
生成がなされる。 それ以外の場合は WebApplicationException
の response
プロパティの値がそのままレスポンスとして使われる。
サンプルコード
JAX-RS の参照実装である Jersey を使った Kotlin のサンプルコード。
import org.glassfish.jersey.jdkhttp.JdkHttpServerFactory import org.glassfish.jersey.server.ResourceConfig import javax.ws.rs.GET import javax.ws.rs.Path import javax.ws.rs.WebApplicationException import javax.ws.rs.core.Response import javax.ws.rs.core.UriBuilder import javax.ws.rs.ext.ExceptionMapper @Path("exceptions") class MyResource { @GET @Path("re") fun re() { // RuntimeException なので対応する MyExceptionMapper で変換される。 throw RuntimeException("RuntimeException") } @GET @Path("wae-without-entity") fun waeWithoutEntity() { // エンティティのない WebApplicationException なので MyExceptionMapper で変換される。 throw WebApplicationException("WebApplicationException with response not containing entity", Response.status(Response.Status.BAD_REQUEST).build()) } @GET @Path("wae-with-entity") fun waeWithEntity() { // エンティティを持つ WebApplicationException なので MyExceptionMapper で変換されない。 throw WebApplicationException("WebApplicationException with response containing entity", Response.status(Response.Status.BAD_REQUEST).entity("Entity").build()) } } class MyExceptionMapper : ExceptionMapper<Throwable> { override fun toResponse(exception: Throwable): Response { println(exception) return Response.status(Response.Status.BAD_REQUEST).entity("$exception").build() } } fun main() { val baseUri = UriBuilder.fromUri("http://localhost/").port(9998).build() val config = ResourceConfig(MyResource::class.java, MyExceptionMapper::class.java) JdkHttpServerFactory.createHttpServer(baseUri, config) }
ビルドコードは以下のような感じで動くはず。 (Gradle。)
plugins { id 'org.jetbrains.kotlin.jvm' version '1.3.11' } repositories { mavenCentral() } dependencies { compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8" compileOnly "javax.ws.rs:javax.ws.rs-api:2.1" compile "org.glassfish.jersey.containers:jersey-container-jdk-http:2.27" compile "org.glassfish.jersey.inject:jersey-hk2:2.27" // JDK 9 以降は下記が必要。 // See : https://stackoverflow.com/questions/43574426/how-to-resolve-java-lang-noclassdeffounderror-javax-xml-bind-jaxbexception-in-j compile "javax.xml.bind:jaxb-api:2.2.11" compile "javax.activation:activation:1.1.1" } compileKotlin { kotlinOptions.jvmTarget = "1.8" } compileTestKotlin { kotlinOptions.jvmTarget = "1.8" }
読んだ : Clean Architecture 達人に学ぶソフトウェアの構造と設計 (Robert C. Martin 著)
Robert Cecil Martin 氏、いわゆるアンクル・ボブ (ボブおじさん; Uncle Bob) による Clean Architecture についての書籍。
Clean Architecture 達人に学ぶソフトウェアの構造と設計
- 作者: Robert C.Martin,角征典,高木正弘
- 出版社/メーカー: KADOKAWA
- 発売日: 2018/07/27
- メディア: 単行本
- この商品を含むブログを見る
アンクル・ボブの Clean Architecture といえば、下記記事を読んだことがある人も多いだろう。
ここ数年の Android アプリ開発界隈でも Clean Architecture はよく話題に上がっているように思う。 私自身も web 上で Clean Architecture について調べて学んだりしてきた。
しかし、web 上の情報を追いかけるだけだと、レイヤ分けや依存関係のルールはわかっても、より本質的な思想の部分まではなかなか理解できなかった。 例えば、「クリーンアーキテクチャにおいてサーバーとクライアントの構成にする場合にどういう扱いにするのが妥当なのか?」 というのは長年考えてきてなかなか自分の中で納得した答えは出せなかったもののひとつである。
本書を読むことで、Clean Architecture の本質的な思想についても深く理解できた。 また、そもそもの 「アーキテクチャとは何か?」 といった部分まで考えることができた。 ソフトウェアアーキテクチャについてより深い洞察を得たい人におすすめの一冊である。
本書の流れ
本書は、「設計とアーキテクチャは地続きのものであり、本質的な違いはない」 というところから始まる。 そして、構造化プログラミングや関数型プログラミングといったコーディングのパラダイムが説明される。 その後に設計の原則 (SOLID 原則)、コンポーネントの原則 (REP、CCP、CRP) についての説明がある。 ここまでが本書の前半である。
本書の後半では、前半の話を踏まえて、アーキテクチャの説明に入っていく。 アーキテクチャの話だけ読みたい人もいるだろうが、個人的には SOLID 原則や REP、CCP、CRP といった部分についても改めて学ぶことができて非常に良かった。
学びや感想
設計とアーキテクチャ、その目的について
1 章で、「設計とアーキテクチャについて、本質的には違いはない」 ということが言われる。 通常、アーキテクチャは下位レベルの詳細とは切り離された文脈で使用され、設計は下位レベルの構造や意思決定を意味しているが、実際のところ下位レベルの詳細と上位レベルの構造が全体の設計の一部になる。 そうした連続した構造がシステムの形状を定義するので、設計とアーキテクチャを明確に区別することはできない。 上位レベルから下位レベルまで決定の連続であり、その目的は下記の通りである。
ソフトウェアアーキテクチャの目的は、求められるシステムを構築・保守するために必要な人材を最小限に抑えることである。
ソフトウェアの振る舞いが正しいものであることを重視し、アーキテクチャを軽視すると、開発を続けていくうちに生産性がどんどん低くなっていく。 「振る舞いが正しいこと」 と 「変更しやすいこと (良いアーキテクチャ・設計であること)」 のどちらが重要であるかという質問に対して、ビジネスマネージャは、「振る舞いが正しいこと」 であると答えることが多く、開発者もそれに賛同することが多い。 しかし、下記のように考えると、より重要なものは 「アーキテクチャ」 である。
- 振る舞いが正しく、変更しづらい (アーキテクチャ・設計が良くない) プログラムは、今は正しくとも将来的な要件の変更に追従できなくなり、やがて役に立たなくなる。
- 振る舞いが誤っており、変更しやすい (アーキテクチャ・設計が良い) プログラムは、今の正しくない振る舞いを正しく修正することも容易であり、将来的な要件の変更にも追従しやすく、長く価値を保つ。
開発者以外に対してアーキテクチャ・設計の重要性を説明することは非常に難しいと思っていたが、上記の観点で説明すると結構説明しやすい気がした。
優れたソフトウェア開発チームは、真正面から闘争に立ち向かう。 ステークホルダーたちと対等に、ひるむことなく口論する。 ソフトウェア開発者もステークホルダーであることは忘れてはいけない。 保護すべきソフトウェアに対する責任がある。 それが、あなたの役割であり、義務である。 それが、あなたが雇われている大きな理由だ。
『開発者はスクラッチからシステム全体を再設計することが答えだと考えているかもしれないが、それもまたウサギのやり方だ。 崩壊をもたらした自信過剰が、今度は競走をやり直せばもっとうまく構築できるという話に変わっている。 現実はそれほどうまくはいかない。』 せやぞ#書籍 #CleanArchitecture
— Nobuoka Yu (@nobuoka) 2018年10月15日
ソフトウェアの 1 つ目の価値は 『ふるまい』 で、2 つ目の価値は 『アーキテクチャ』。 ふるまいは緊急ではあるが重要ではない。 アーキテクチャは緊急ではないが重要。 後者の方が優先度は高いが、前者の方が優先度が高いと勘違いしてしまうことが多い。 なるほど。#書籍 #CleanArchitecture
— Nobuoka Yu (@nobuoka) 2018年10月15日
テストと反証可能性について
Dijkstra は 「テストはバグが存在しないことではなく、バグが存在することを示すものである」 と述べた。 つまり、テストによってプログラムが正しくないことは証明できるが、プログラムが正しいことは証明できないのである。 テストに十分な労力をかけていれば、そのプログラムは目的のために十分に真であるとみなせる。
この事実は、驚くべきことを示している。ソフトウェア開発は、数学的な構成要素を操作しているかのように思えるかもしれないが、実際には数学的な試みではない。 むしろソフトウェア開発は科学のようなものである。 どれだけ最善を尽くしても正しくないことを証明できないことによって、その正しさを明らかにしているのである。
数学的なアプローチか科学的なアプローチか、という観点は持っていなかったので、「なるほどー」 と思った。
最小の機能から最大のコンポーネントまで、あらゆるレベルにおいて、ソフトウェアは科学のように、反証可能性によって動かされている。ソフトウェアアーキテクトは、簡単に反証できる(テスト可能な)モジュール、コンポーネント、サービスを定義しようとする。そのために、さらに上位のレベルにおいて、構造化プログラミングのような制限を課している。
だからこそ、小さな粒度から大きな粒度まで、あらゆるレベルで反証可能な形で部品化することが重要。 TDD の利点って、この 「反証可能な形で部品化する」 ということをサポートしてくれるからなんだろうな。 『TDD で綺麗な設計に近づける』 と漠然と思っていたけど、すごく納得した
— Nobuoka Yu (@nobuoka) 2018年10月15日
単一責任の原則
SOLID 原則の一つである単一責任の原則 (SRP) について、「たった一つのことだけ行うべき」 だと思っていたけど、正しくは 「モジュールが責務を負うべきアクター (ソフトウェアの変更を望む人たちのグループ) がひとつだけになるようにすべき」 ということだった。 アクターをどう定義するかが難しい気もするけど、アクターの定義をどうするかを含めてしっかり考えていく必要があるんだろうなぁと思う。
複数のアクターが使用するコードが存在すると、あるアクターのための変更が他のアクターに想定外の影響を及ぼしてしまう、ということ。 SRP を完全に理解した。
— Nobuoka Yu (@nobuoka) 2018年10月15日
コンポーネントの原則
再利用・リリース等価の原則 (REP)、閉鎖性共通の原則 (CCP)、全再利用の原則 (CRP)、初めて聞いたな。 コンポーネントの凝集性に関する原則。 再利用性、保守性、それからコンポーネントの変更による他コンポーネントへの影響を小さく保つことの 3 つのバランスをどうとるか#書籍 #CleanArchitecture
— Nobuoka Yu (@nobuoka) 2018年10月16日
『安定度・抽象度等価の原則 (SAP) : コンポーネントの抽象度は、その安定度と同程度でなければいけない。』
— Nobuoka Yu (@nobuoka) 2018年10月16日
抽象的で安定しているか、具体的で不安定かのどちらかがよい。 コンポーネント版の DIP のためにこれが必要。#書籍 #CleanArchitecture
アーキテクチャについて
アーキテクチャについては、下記の内容が非常に刺さったり共感するものだった。
- アーキテクチャはユースケースを叫ぶものでなければならない。 どういう技術を採用するかということに意識がいきがちだけど、重要なのはユースケースであって、どういう技術を採用するかではない。
- アーキテクトとしては詳細の決定はできるだけ遅らせるようにすること。 DB に何を採用するのかとか、通信プロトコルに何を採用するのかなど、そういったものは技術的な実装詳細であり、優れたアーキテクチャはそれらの決定を遅らせられるものである。
- 入出力はテストしづらい部分なので、依存関係の一番外側に持ってくること。
- フレームワークへの依存は避けるべき。 フレームワークは実装詳細であり、アーキテクチャとして重要なものではない。
つまり、冒頭で述べた私のこれまでの疑問の話で言うと、(クライアントとサーバーの両方で一つのソフトウェアなのであれば) クライアント・サーバー間の通信なども詳細であり、アーキテクチャとしてはクライアントもサーバーも含めてユースケースを考える、というのが優れたアーキテクトとしての姿勢なのだと感じた。
ツイートメモ
『アーキテクチャの主な目的は (中略) システムのライフタイムコストを最小限に抑え、プログラマの生産性を最大にすることである。』
— Nobuoka Yu (@nobuoka) 2018年10月16日
アーキテクチャと技術的負債は対応関係にありそう。 『エンジニアリング組織論への招待』 でもそう書かれてた気がする#書籍 #CleanArchitecture
『レイヤーやユースケースを切り離す方法はいくつもある。 ソースコード (ソース) レベル、バイナリコード (デプロイ) レベル、実行単位 (サービス) レベルで切り離すことができる。』 『システムの切り離し方式は時間とともに変化する可能性』 選択肢を残すことが大事#書籍 #CleanArchitecture
— Nobuoka Yu (@nobuoka) 2018年10月16日
ここで言われる保守性を高めることこそがアーキテクチャでありアーキテクトの仕事であろうなー。 その仕事には基本設計も含まれるんだけど、詳細設計やコーディングをできない人がアーキテクチャを考えることは難しい。
— Nobuoka Yu (@nobuoka) 2018年10月18日
https://t.co/of3FgtGEV8
もっと戦略的に事業の展開を予測しながらアーキテクチャを考えていくということをしていく必要性を感じているけどその水準に全然達していない
— Nobuoka Yu (@nobuoka) 2018年10月18日
『優れたソフトウェアアーキテクチャがあれば、フレームワーク、データベース、ウェブサーバー、その他の環境の問題やツールの意思決定を延期・留保できる。 フレームワークの選択肢は残されたままだ。』
— Nobuoka Yu (@nobuoka) 2018年10月22日
いいこと言ってる。 アーキテクチャはユースケースをサポートする。 #書籍 #CleanArchitecture
生まれてから一度もフレームワークに依存したいと思ったことはないんだけど、わりとフレームワークに依存してアプリケーションを書きたがる人が多いように思う。
— Nobuoka Yu (@nobuoka) 2018年10月22日
『システムアーキテクチャがユースケースをサポートするものであり、フレームワークから少し距離を置いたものになっていれば (中略) テストを実行するためにウェブサーバーを起動する必要はない。 テストを実行するためにデータベースに接続する必要はない』
— Nobuoka Yu (@nobuoka) 2018年10月22日
早くここにたどり着きたい
Humble Object パターンって初めて聞いた。 (みんなやってることではあるけれど。)
— Nobuoka Yu (@nobuoka) 2018年10月23日
描画処理などのテストしづらい処理をテストしやすい処理を担うクラスとテストしづらい処理を担うクラスに分けて、後者を控えめ (humble) にしておく、というもの。 Presenter と View とか #書籍 #CleanArchitecture
『ORM システムはどこに属するのだろうか? もちろんデータベースのレイヤーだ。実際、ORM はゲートウェイインターフェイスとデータベースの間に Humble Object の境界を作るものである』
— Nobuoka Yu (@nobuoka) 2018年10月23日
JPA とかはこの思想にそぐわなくてあんま好きになれないんだよなー。 #書籍 #CleanArchitecture
『作者にとって、フレームワークとの結合には何のリスクもない。』 『さらに作者は、あなたに対してもフレームワークとの結合を望んでいる。 (中略) 作者にとって、自分の作った基底クラスを大勢のユーザーが継承することほど、自尊心が満たされることはない』
— Nobuoka Yu (@nobuoka) 2018年10月26日
激しい#書籍 #CleanArchitecture
フレームワークなんかと結婚するな!
— Nobuoka Yu (@nobuoka) 2018年10月26日
#書籍 #CleanArchitecture
クラス・インターフェイスごとには同じような依存関係を持たせるにしてもどこでコンポーネントを切るかでいろんなパターンになるという話。 わかりやすい図だ。 #書籍 #CleanArchitecture pic.twitter.com/M9veKX5eZM
— Nobuoka Yu (@nobuoka) 2018年11月3日
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
を紐づけることで、そのライフサイクルの中で管理したいコルーチンを扱いやすくなります。
コルーチン、いい感じに使っていきましょう。
Maven の Surefire プラグインと JMockit と JaCoCo プラグイン
ビルドツールとして Maven を使っている Java プロジェクトで JMockit と JaCoCo を使いたいときの話。 ユニットテストの実行には Maven Surefire プラグインを使用しているものとする。 また、JaCoCo の適用には JaCoCo Maven プラグインを使用するものとする。
JMockit と JaCoCo の -javaagent
指定を共存させる
JMockit も JaCoCo も、Java プログラミング言語エージェントの仕組みを利用している *1。 なので、どちらも基本的には -javaagent
の指定が必要である。
JMockit の -javaagent
指定
JMockit の方は、Surefire プラグインの argLine
パラメータで -javaagent
を指定する。
<plugins> <plugin> <artifactId>maven-surefire-plugin</artifactId> <version>2.21.0</version> <!-- or some other version --> <configuration> <argLine> -javaagent:${settings.localRepository}/org/jmockit/jmockit/${jmockit.version}/jmockit-${jmockit.version}.jar </argLine> <!-- ... --> </configuration> <plugin> <!-- ... --> <plugins>JMockit - Tutorial - Introduction
JaCoCo の -javaagent
指定
JaCoCo については jacoco:prepare-agent
ゴールにて argLine
プロパティ *2 に -javaagent
が追加される仕組みになっている。
Surefire プラグインに argLine
パラメータが指定されていないときには argLine
プロパティの値が使われるので、Surefire プラグインに argLine
パラメータが指定されていない場合にはそれ以上の設定は必要ない。
一方で、Surefire プラグインに argLine
パラメータの指定がされている場合は (そのままでは) JaCoCo プラグインが設定した argLine
プロパティの値が使われないため、別途設定が必要となる。 具体的には、Surefire プラグインの late property evaluation (late replacement) を使用する。 これは、プロパティ参照の ${...}
の代わりに @{...}
を使うというもの。 ${...}
を使った場合はプラグインの実行前に評価されるため JaCoCo プラグインが argLine
プロパティを変更してもそれが反映されないが、@{...}
を使うとプラグインの実行後の値が反映される。
One of the ways to do this in case of maven-surefire-plugin - is to use syntax for late property evaluation:
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <configuration> <argLine>@{argLine} -your -extra -arguments</argLine> </configuration> </plugin>
IntelliJ IDEA での問題
さて、Maven でビルドを実行する際に JMockit と JaCoCo を共存させるだけならここまでで解決するが、さらにややこしいのが IntelliJ IDEA の存在である。
IntelliJ IDEA で Maven プロジェクトを扱う場合、デフォルトでは JUnit 実行時に Surefire プラグインの argLine
パラメータを読み取るのだが、IntelliJ IDEA は late replacement (@{...}
) に対応していない。 そのため、下記のようなエラーが発生してしまう。
エラー: メイン・クラス@{argLine}が見つからなかったかロードできませんでした
- IntelliJ IDEA が
@{...}
を解釈しない件 : https://youtrack.jetbrains.com/issue/IDEA-143187
回避策の一つは、IntelliJ IDEA でのユニットテスト実行時に Surefire プラグインの argLine
パラメータを参照する設定を無効にすること。 設定の中の 「Maven」 の 「Running Tests」 で Surefire プラグインの argLine
を参照するかどうか指定できる。
私が採った回避策は、argLine
パラメータには空文字列として宣言したプロパティを含めておき、JaCoCo を有効にしたいときだけコマンドライン引数で @{argLine}
を埋め込むというもの。
<properties> <!-- Surefire プラグインの `argLine` に含める文字列。 JaCoCo の Java Agent を有効にするためには `@{argLine}` を指定すること。 IntelliJ IDEA が `@{...}` による late replacement に対応しておらず、直接 `@{argLine}` を指定すると IntelliJ IDEA でのテスト実行時にエラーが発生してしまうので、`@{argLine}` をコマンドラインオプションで指定できるように このようなプロパティを用意した。 See : http://maven.apache.org/surefire/maven-surefire-plugin/test-mojo.html (Surefire Plugin の `argLine` の説明) See : https://www.eclemma.org/jacoco/trunk/doc/prepare-agent-mojo.html (JaCoCo の Java Agent の指定について) See : https://youtrack.jetbrains.com/issue/IDEA-143187 (IntelliJ IDEA が argLine の `@{...}` を解釈しない件) --> <surefireArgLine/> </properties> <!-- (中略) --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <version>2.19.1</version> <configuration> <argLine> ${surefireArgLine} <!-- See : http://jmockit.github.io/tutorial/Introduction.html#runningTests --> -javaagent:${settings.localRepository}/org/jmockit/jmockit/${jmockit.version}/jmockit-${jmockit.version}.jar </argLine>
上のようにしておけば IntelliJ IDEA でのテスト実行にエラーが出ることはない。 そして Maven をコマンドラインから動かすときには、下記のように指定すれば JaCoCo が有効になる。
mvn package -D surefireArgLine="@{argLine}"
もっといい方法があれば知りたい……。
参考
- Java プログラミング言語エージェントについて : java.lang.instrument (Java SE 10 & JDK 10 )
Android アプリバンドル (Android App Bundle) について学んだ
2018 年の Google I/O でも発表があった *1 Android アプリバンドル (Android App Bundle)。 Android Studio 3.2 を使っているとアプリのビルドで App Bundle を選べるようになっていたりするし何となく存在は知っていたけどちゃんと調べてはいなかった。
Instant アプリを試しに作ってみようとしたら Android アプリバンドルの知識が必要になったので、この機会にちゃんと調べてみることにした。 とりあえずコードラボの内容をさーっと見たのでまとめておく。
- コードラボ : Your First Android App Bundle
Android アプリバンドルについて
- Android アプリバンドル (Android App Bundle) : Google Play へアップロードするための新しい形式。 アプリのコンパイルされたコードとリソースをすべて含むが、APK 生成や署名は Google Play に任される。
- 動的配信 (Dynamic Delivery) : Google Play における新しいアプリ提供のモデル。 各ユーザー端末に最適化された APK を生成して提供するためにアプリバンドルが使用される *2。
- 動的機能モジュール (dynamic feature modules) : この種のモジュールをアプリのプロジェクトに追加し、アプリバンドルに含めることで、必要になってから動的配信を通じてダウンロードされるアプリの機能を作ることができる。 2018 年 11 月 11 日時点ではまだベータ版。
動的配信 (Dynamic Delivery) は、分割 APK 機構 (split APK mechanism) *3 を基礎としている。 分割 APK により、Google Play は大きなアプリを小さなパッケージに分解できる。 分割 APK の種類は 3 種類。
- Base APK : 他の分割 APK から使用されるコードやリソースを含み、アプリの基本機能を提供する。 ユーザーがアプリをダウンロードする際には常にこの APK が含まれる。
- Configuration APKs : 指定された端末構成のためのネイティブライブラリとリソースのみを含む。 ロケールや画面密度、CPU アーキテクチャといったものに依存する APK コンテンツを最適化するため。
- Dynamic feature APKs : アプリが最初にインストールされたときには必要でないが、あとからダウンロードされて使われるかもしれない機能を含む。
これらの APK は Google Play がビルドして提供してくれる。 Android 4.4 (API level 20) 以下の端末向けには、Google Play が自動で端末構成に最適化された単一 APK を提供してくれる。
Android Studio 3.2 でアプリバンドルを生成する
プロジェクト自体は普通に作成すればそれで良い。 (Dynamic feature module を作らないなら、プロジェクト構造は通常の app ディレクトリ下にアプリ全体のコードやリソースを含める形式で良い。)
Android Studio で、「Build」 メニューから 「Build Bundle(s)」 を選んだり、「Generate Signed Bundle or APK」 の 「Android App Bundle」 を選んだりすることでアプリバンドルを生成できる。
コードラボには、app/build.gradle の android { }
ブロックに下記の内容を追加する必要があるようなことが書かれているが、「Add support for Dynamic Delivery | Android Developers」 を見る限りは Android Studio 3.2 の正式版の段階ではデフォルトで有効になっているようで、下記記述はしなくても良さそう。
bundle { language { enableSplit = true } density { enableSplit = true } abi { enableSplit = true } }
アプリバンドルの確認
アプリバンドルを確認する方法は 2 つ。
- ローカルで bundletool コマンドラインツールを使用する。
- Play Console にバンドルをアップロードし、内部テストトラック (internal test track) を使って Google Play を通して確認。
Bundletool というのは、Gradle や Android Studio、Google Play が Android アプリバンドルをビルドしたり、アプリバンドルから種々の APK に変換したりするのに使用しているツール。 コマンドラインツールとしても使用できる。 「Releases · google/bundletool · GitHub」 で JAR 形式で配布されている。 使用方法はコードラボにも書かれているし、「bundletool | Android Developers」 などにも書かれている。
アプリバンドルを Google Play にアップロードする
Google Play へのアップロードで気を付ける必要があるのは 「Google Play アプリ署名」 を使用する必要があるということぐらいで、それ以外には特に難しいところはなかった。
App Bundle を使用するアプリには Google Play アプリ署名が必要です。アプリ署名を有効にした後の流れは次のとおりです。
アプリ署名鍵を管理する - Play Console ヘルプ
おまけ : アプリバンドルの instant app サポート
2017 年から instant apps というものは存在していた *4 が、アプリバンドルによってより簡単に実現できるようになった。
- Create your first instant app | Android Developers
- Add an instant experience to your existing Android App Bundle | Android Developers : アプリバンドルに instant アプリの機能を追加する方法
- Android Studio Release Updates: Android Studio 3.3 Canary 11 available : Android Studio 3.3 Canary 11 では Android Studio でのサポートも追加された模様。
ドキュメントを見る限りは簡単なのだけど、自分の場合は動作確認にかなりてこずってしまった。 また今度まとめる。
関連ページ
*1:Google Developers Japan: Google I/O 2018:Android の新機能 など参照。
*2:それぞれの端末に必要なコードやリソースだけがダウンロードされる。
*3:Android 5.0 (API level 21) 以降で使用可能
*4:【インストールせずにすぐ実行できる!】Android Instant Apps を使ってみた – PSYENCE:MEDIA など。