20190522のKotlinに関する記事は2件です。

Kotlin + Spring でインジェクトされるオブジェクトがプロキシ(CGLib)を伴っていた場合、メソッドがprivate変数をデフォルト非null引数に使っていると死ぬ

タイトルどおり。

条件が揃うと、一見正しそうなKotlin + Springのコードが死にます。

再現コード

再現コードは下記にあります。

https://github.com/knjname/2019-05-22_springkotlinpitfall

$ git clone https://github.com/knjname/2019-05-22_springkotlinpitfall 2019-05-22_springkotlinpitfall
$ cd !$
$ ./gradlew bootRun

...
java.lang.IllegalStateException: Failed to execute CommandLineRunner
        at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:816) ~[spring-boot-2.1.5.RELEASE.jar:2.1.5.RELEASE]
        at org.springframework.boot.SpringApplication.callRunners(SpringApplication.java:797) ~[spring-boot-2.1.5.RELEASE.jar:2.1.5.RELEASE]
        at org.springframework.boot.SpringApplication.run(SpringApplication.java:324) ~[spring-boot-2.1.5.RELEASE.jar:2.1.5.RELEASE]
        at org.springframework.boot.SpringApplication.run(SpringApplication.java:1260) ~[spring-boot-2.1.5.RELEASE.jar:2.1.5.RELEASE]
        at org.springframework.boot.SpringApplication.run(SpringApplication.java:1248) ~[spring-boot-2.1.5.RELEASE.jar:2.1.5.RELEASE]
        at knjname.springkotlinpitfall.SpringkotlinpitfallApplicationKt.main(SpringkotlinpitfallApplication.kt:41) ~[main/:na]
Caused by: java.lang.IllegalArgumentException: Parameter specified as non-null is null: method knjname.springkotlinpitfall.Nuke.withOptional, parameter lst
        at knjname.springkotlinpitfall.Nuke.withOptional(SpringkotlinpitfallApplication.kt) ~[main/:na]
        at knjname.springkotlinpitfall.Nuke$$FastClassBySpringCGLIB$$13faa123.invoke(<generated>) ~[main/:na]
...

解説

下記コードが問題を再現させるコードです。

package knjname.springkotlinpitfall

import org.springframework.boot.CommandLineRunner
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.stereotype.Component
import org.springframework.transaction.annotation.Transactional

@Component
@SpringBootApplication
class SpringkotlinpitfallApplication(
        private val nuke: Nuke
) : CommandLineRunner {

    override fun run(vararg args: String?) {
        // ここから起動します
        nuke.withOptional()
    }
}

@Component
class Nuke {

    private val a = listOf("a")

    // ここで死ぬ!!
    // Caused by: java.lang.IllegalArgumentException: Parameter specified as non-null is null:
    // method knjname.springkotlinpitfall.Nuke.withOptional, parameter lst
    fun withOptional(
            lst: List<Any> = a
    ) = Unit

    @Transactional
    fun xxx() = Unit

}


fun main(args: Array<String>) {
    runApplication<SpringkotlinpitfallApplication>(*args)
}

上記を実行すると、 withOptional メソッドの呼び出しにて、Kotlinのnullチェックに引っかかって例外で死にます。

下記条件が揃っているためです。

  • @Transactional を持つことにより、 Nuke クラスはCGLibでラップされることとなる。
    • Springで注入された Nuke インスタンスは全てCGLibのプロキシクラスになる。
    • CGLib でラップされたクラスのフィールド変数は null となる。
  • Kotlinがデフォルト引数つきのメソッドを呼ぶ際は Nuke クラスにヘルパ用の static メソッド (withOptional$default) を生成し、そのメソッドに Nukeのインスタンス(今回はCGLibにラッパされたインスタンス) を引数として渡して呼び出す。
  • 該当のstaticメソッドは Nuke.a をデフォルト引数の値とするため参照するが、CGLibにラップされたクラスのフィールドであるため、上述の通り、null 値が入る。
    • つまり、 listOf("a") は使われない。
  • null 値がKotlinのランタイム時の withOptional メソッド内のnullチェックによってチェックされ、例外が発生する。

上記のように分かりづらい経緯を経て死にます。デフォルト引数怖いっ!

ワークアラウンド

上記条件にひっかからなければ回避可能なので、下記のような回避を取れるでしょう。

  • デフォルト引数に指定する定数を
    • private val ではなく val を用いる
    • クラス内に宣言しない
    • class の外に宣言する
    • コンパニオンオブジェクトに宣言する
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

そろそろ Kotlin Coroutine をちゃんと使ってみよう

TL;DR

launch {
    val result = opTakesLongEnoughToHaveMyNeckLong()
    showOffResult(result)
}

そろそろ、 Kotlin でも非同期処理はこんな風に書きたい時代


ならば、実際にそう書いてみよう!


そもそもコルーチンとは

みたいな話をし始めると日が暮れてしまったり居眠りしてしまったり首が伸びてしまったりするので、ばっさり割愛。

(JVM レベルにどう落とし込まれているかなど、興味深い話題もありますが)


導入

今回は、実践的なものを作ってみるという趣旨から、 Android アプリをターゲットとしてみます。

ということで、まずは app/build.gradle でコルーチン関連の基礎的なライブラリへの依存関係を追加します。

dependencies {
    // ...
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.2.1'
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.2.1'
    // ...
}

※バージョンは本稿執筆時点(2019/05/22)の最新


なぜライブラリ?

なぜでしょうね?

言語としての Kotlin には非同期処理を表現するに足る最低限のキーワードのみ持たせて、実装はプラットフォームによって自由にすげ替えることのできるような構造を目指した、といったところでしょうか。


実践

非同期といえば?

通信!

ですよね。
ということで、 Java 系界隈で REST API を抽象化するライブラリとしては定番中の定番、 Retrofitを組み合わせてみます。


app/build.gradle に追加するのはこんな感じ。

dependencies {
    implementation 'com.squareup.retrofit2:retrofit:2.5.0'
    implementation 'com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:0.9.2'
    implementation 'com.squareup.retrofit2:converter-gson:2.5.0'
}

ちなみに、 converter-gson は、これも定番の JSON パーサー gson を Retrofit から透過的に使えるようにするためのもの。コルーチンを使う使わないには関係しないので、別のパーサーの方が好きならばそちらを使えばいいです。


REST API を Kotlin の interface

Retrofit は、アノテーションをベースに REST API を記述するタイプのライブラリです。
例えば、某サービスのログイン API はこのような記述になります。

import com.google.gson.annotations.SerializedName
import kotlinx.coroutines.Deferred
import retrofit2.http.POST
import retrofit2.http.Query

interface Login {
    @POST("/api/member/logins")
    fun login(
        @Query("mail") mail: String
        , @Query("password") password: String
        , @Query("keep_me") keepMe: Boolean
    ): Deferred<LoginResult>
}

data class LoginResult(
    @SerializedName("r") val resultCode: String
    //, @SerializedName("d") val resultData: LoginData?
)

本当は login(...) そのものを suspend fun にしてしまいたいところですが、 Retrofit は直接対応していないので、 CoroutineCallAdapterFactory の助けを借りつつ、一旦 kotlinx.coroutines.Deferred<T> を返すものとして宣言してしまいます。


Deferred<T> を返す Retrofit のメソッドを suspend fun に仕立て上げる

object ApiService {
    private val okHttpClient = OkHttpClient.Builder()
        // ...
        .build()

    @PublishedApi internal val retrofit = Retrofit.Builder()
        .baseUrl("https://api.example.com")
        .addConverterFactory(GsonConverterFactory.create())
        .addCallAdapterFactory(CoroutineCallAdapterFactory.invoke())
        .client(okHttpClient)
        .build()

    suspend inline fun <reified T, reified U> request(block: T.() -> Deferred<U>): U =
        block(retrofit.create(T::class.java)).await()
}

これで、 val res = ApiService.request<Login, LoginResult> { login("aaa", "pass", keepMe = true) } のような記法が可能になります。


suspend fun とそうじゃないコードとのつなぎ目

CoroutineScopeCoroutineContext と...

:confused:

通常の fun から suspend fun を直接呼ぶことはできないので、一番最初に非同期処理を始めるところはどう書けばいいんだ? となってしまいます。

CoroutineScope インタフェースを実装したオブジェクトなら launch などの suspend なブロックを取れるメソッドが使えるようになるので、なんとかして手頃なオブジェクトに CoroutineScope になってもらう作戦になります。


単純な例: ActivityCoroutineScope になれればいいじゃない

class MainActivity: Activity(), CoroutineScope {
    // onDestroy() で 未完了のコルーチン呼び出しをまとめて cancel しまくるための CoroutineContext
    private val supervisor = SupervisorJob()

    // CoroutineScope になるためにはこの抽象プロパティーを実装しないといけない
    override val coroutineContext: CoroutineContext
        // メインスレッドを表すグローバルな CoroutineContext と、 supervisor スコープを合成した CoroutineContext が、
        // CoroutineScope としてのこの Activity の CoroutineContext となるようにする。
        get() = Dispatchers.Main + supervisor

    override fun onDestroy() {
        super.onDestroy()
        // 未完了のコルーチン呼び出しをまとめて始末
        supervisor.cancelChildren()
    }

    // ...
}

これで、 Activity の中なら launch その他が使えるようになりました。


いよいよ、 onClickListener から接続!

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        loginButton.setOnClickListener {
            performLogin()
        }
    }

    private fun performLogin() {
        val mail = mailForm.text.toString()
        val password = passwordForm.text.toString()

        launch {
            // ここからコルーチンの世界!
            val loginResult = ApiService.request<Login, LoginResult> {
                login(mail, password, keepMe = false)
            }
            if (loginResult.resultCode != "00101") {
                // 失敗
                return@launch
            }
            // ...
        }
    }

その他

例外? 例外!

コルーチンの中で例外が起きたらどうする? 問題。

端的には、 suspend fun 本体から別の suspend fun を呼んでいて、その中で例外が起きうる時にその例外を捕まえたければ、通常の同期的な書き方と同じ要領で普通に try {} catch {} でくるめば意図通りの意味になります。

    val loginResult = try {
        ApiService.request<Login, LoginResult> {
            login(mail, password, keepMe = false)
        }
    } catch (e: IOException) {
        Log.d(TAG, "login failed", e)
        return@launch
    }
    // ...

いくつかのリクエストを並行させたい

CoroutineScope#awaitAll() など。キャンセル可能にする範囲を細かく管理したい場合はまた違った戦略が必要ですが、単純なケースならこれで十分でしょう。

    awaitAll(
        async { foo() }
        , async { bar() }
        , async { baz() }
    )

Rx とどちらがいいか?

そもそも基本思想が違うので、適材適所といったところ。

  • 複雑なタイミング制御などが必要なら、データフローを宣言的に記述できる Rx が有利。
  • シンプルな一連の非同期処理だけなら、逐次処理に近い記法で記述できるコルーチンが有利。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む