ひだまりソケットは壊れない

ソフトウェア開発に関する話を書きます。 最近は主に Android アプリ、Windows アプリ (UWP アプリ)、Java 関係です。

まじめなことを書くつもりでやっています。 適当なことは 「一角獣は夜に啼く」 に書いています。

CloudFront のプライベートコンテンツ配信を試した (Kotlin で signed cookie を生成する)

  • AWSCDN である CloudFront でプライベートコンテンツを配信する方法として、signed URL を用いる方法と、signed cookie を用いる方法がある。
  • 本記事では、signed cookie の生成処理を Kotlin (Java) で行う方法を説明する。

事前準備 (CloudFront 側の準備)

  • アクセスする側の挙動を確認するために、試験的に CloudFront 側も準備したのでその手順を書いておく。 (AWS コンソール上で操作を行った。)
  • 内容は次の 2 つ。

CloudFront のディストリビューションを用意

コンテンツへのアクセスを制限する

キーペアの作成と 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 JavaCloudFrontCookieSigner というクラスがあり、これを使うことで signed cookie を生成することも可能。 通常はこれを使うのが良さそう。

JAX-RS のリソースから送出された例外の扱い

JAX-RS のリソースのメソッドから例外が送出された場合の挙動についてちゃんと把握していなかったので調べた。

前提知識 : JAX-RS について

JAX-RS は、RESTful Web API を提供するための JavaAPI。 もともとは 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 the response property of the exception does not contain an entity and an exception mapping provider (see Section 4.4) is available for WebApplicationException or the corresponding subclass, an implementation MUST use the provider to create a new Response instance, otherwise the response property is used directly. The resulting Response 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 a Response 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-WS Provider-based implementations MUST use WebServiceException 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 the ExceptionMapper<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 resulting Response is processed as if a web resource method had returned the Response, see Section 3.3.3. In particular, a mapped Response 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 つめがそれである。

WebApplicationExceptionresponse プロパティがエンティティを持っていない場合で、対応する exception mapping provider が存在する場合には exception mapping provider による Response 生成がなされる。 それ以外の場合は WebApplicationExceptionresponse プロパティの値がそのままレスポンスとして使われる。

サンプルコード

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 達人に学ぶソフトウェアの構造と設計

Clean Architecture 達人に学ぶソフトウェアの構造と設計

アンクル・ボブの Clean Architecture といえば、下記記事を読んだことがある人も多いだろう。

ここ数年の Android アプリ開発界隈でも Clean Architecture はよく話題に上がっているように思う。 私自身も web 上で Clean Architecture について調べて学んだりしてきた。

しかし、web 上の情報を追いかけるだけだと、レイヤ分けや依存関係のルールはわかっても、より本質的な思想の部分まではなかなか理解できなかった。 例えば、「クリーンアーキテクチャにおいてサーバーとクライアントの構成にする場合にどういう扱いにするのが妥当なのか?」 というのは長年考えてきてなかなか自分の中で納得した答えは出せなかったもののひとつである。

本書を読むことで、Clean Architecture の本質的な思想についても深く理解できた。 また、そもそもの 「アーキテクチャとは何か?」 といった部分まで考えることができた。 ソフトウェアアーキテクチャについてより深い洞察を得たい人におすすめの一冊である。

本書の流れ

本書は、「設計とアーキテクチャは地続きのものであり、本質的な違いはない」 というところから始まる。 そして、構造化プログラミングや関数型プログラミングといったコーディングのパラダイムが説明される。 その後に設計の原則 (SOLID 原則)、コンポーネントの原則 (REP、CCP、CRP) についての説明がある。 ここまでが本書の前半である。

本書の後半では、前半の話を踏まえて、アーキテクチャの説明に入っていく。 アーキテクチャの話だけ読みたい人もいるだろうが、個人的には SOLID 原則や REP、CCP、CRP といった部分についても改めて学ぶことができて非常に良かった。

学びや感想

設計とアーキテクチャ、その目的について

1 章で、「設計とアーキテクチャについて、本質的には違いはない」 ということが言われる。 通常、アーキテクチャは下位レベルの詳細とは切り離された文脈で使用され、設計は下位レベルの構造や意思決定を意味しているが、実際のところ下位レベルの詳細と上位レベルの構造が全体の設計の一部になる。 そうした連続した構造がシステムの形状を定義するので、設計とアーキテクチャを明確に区別することはできない。 上位レベルから下位レベルまで決定の連続であり、その目的は下記の通りである。

ソフトウェアアーキテクチャの目的は、求められるシステムを構築・保守するために必要な人材を最小限に抑えることである。

ソフトウェアの振る舞いが正しいものであることを重視し、アーキテクチャを軽視すると、開発を続けていくうちに生産性がどんどん低くなっていく。 「振る舞いが正しいこと」 と 「変更しやすいこと (良いアーキテクチャ・設計であること)」 のどちらが重要であるかという質問に対して、ビジネスマネージャは、「振る舞いが正しいこと」 であると答えることが多く、開発者もそれに賛同することが多い。 しかし、下記のように考えると、より重要なものは 「アーキテクチャ」 である。

  • 振る舞いが正しく、変更しづらい (アーキテクチャ・設計が良くない) プログラムは、今は正しくとも将来的な要件の変更に追従できなくなり、やがて役に立たなくなる。
  • 振る舞いが誤っており、変更しやすい (アーキテクチャ・設計が良い) プログラムは、今の正しくない振る舞いを正しく修正することも容易であり、将来的な要件の変更にも追従しやすく、長く価値を保つ。

開発者以外に対してアーキテクチャ・設計の重要性を説明することは非常に難しいと思っていたが、上記の観点で説明すると結構説明しやすい気がした。

優れたソフトウェア開発チームは、真正面から闘争に立ち向かう。 ステークホルダーたちと対等に、ひるむことなく口論する。 ソフトウェア開発者もステークホルダーであることは忘れてはいけない。 保護すべきソフトウェアに対する責任がある。 それが、あなたの役割であり、義務である。 それが、あなたが雇われている大きな理由だ。


テストと反証可能性について

Dijkstra は 「テストはバグが存在しないことではなく、バグが存在することを示すものである」 と述べた。 つまり、テストによってプログラムが正しくないことは証明できるが、プログラムが正しいことは証明できないのである。 テストに十分な労力をかけていれば、そのプログラムは目的のために十分に真であるとみなせる。

この事実は、驚くべきことを示している。ソフトウェア開発は、数学的な構成要素を操作しているかのように思えるかもしれないが、実際には数学的な試みではない。 むしろソフトウェア開発は科学のようなものである。 どれだけ最善を尽くしても正しくないことを証明できないことによって、その正しさを明らかにしているのである。

数学的なアプローチか科学的なアプローチか、という観点は持っていなかったので、「なるほどー」 と思った。

最小の機能から最大のコンポーネントまで、あらゆるレベルにおいて、ソフトウェアは科学のように、反証可能性によって動かされている。ソフトウェアアーキテクトは、簡単に反証できる(テスト可能な)モジュール、コンポーネント、サービスを定義しようとする。そのために、さらに上位のレベルにおいて、構造化プログラミングのような制限を課している。


単一責任の原則

SOLID 原則の一つである単一責任の原則 (SRP) について、「たった一つのことだけ行うべき」 だと思っていたけど、正しくは 「モジュールが責務を負うべきアクター (ソフトウェアの変更を望む人たちのグループ) がひとつだけになるようにすべき」 ということだった。 アクターをどう定義するかが難しい気もするけど、アクターの定義をどうするかを含めてしっかり考えていく必要があるんだろうなぁと思う。


コンポーネントの原則


アーキテクチャについて

アーキテクチャについては、下記の内容が非常に刺さったり共感するものだった。

  • アーキテクチャユースケースを叫ぶものでなければならない。 どういう技術を採用するかということに意識がいきがちだけど、重要なのはユースケースであって、どういう技術を採用するかではない。
  • アーキテクトとしては詳細の決定はできるだけ遅らせるようにすること。 DB に何を採用するのかとか、通信プロトコルに何を採用するのかなど、そういったものは技術的な実装詳細であり、優れたアーキテクチャはそれらの決定を遅らせられるものである。
  • 入出力はテストしづらい部分なので、依存関係の一番外側に持ってくること。
  • フレームワークへの依存は避けるべきフレームワークは実装詳細であり、アーキテクチャとして重要なものではない。

つまり、冒頭で述べた私のこれまでの疑問の話で言うと、(クライアントとサーバーの両方で一つのソフトウェアなのであれば) クライアント・サーバー間の通信なども詳細であり、アーキテクチャとしてはクライアントもサーバーも含めてユースケースを考える、というのが優れたアーキテクトとしての姿勢なのだと感じた。

ツイートメモ

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 でコルーチンを構造化しやすくなった

コルーチンの構造化をやりやすくするために、launchasync といったコルーチンビルダーが 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 でのユニットテスト実行時に 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}"

もっといい方法があれば知りたい……。

*1:JaCoCo についてはオフラインインストゥルメントの仕組みもあるが。

*2:デフォルトの場合。 変更可。