- 投稿日:2019-05-22T20:28:47+09:00
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 の外に宣言する
- コンパニオンオブジェクトに宣言する
- 投稿日:2019-05-22T18:18:55+09:00
そろそろ 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とそうじゃないコードとのつなぎ目
CoroutineScopeとCoroutineContextと...
![]()
通常の
funからsuspend funを直接呼ぶことはできないので、一番最初に非同期処理を始めるところはどう書けばいいんだ? となってしまいます。
CoroutineScopeインタフェースを実装したオブジェクトならlaunchなどのsuspendなブロックを取れるメソッドが使えるようになるので、なんとかして手頃なオブジェクトにCoroutineScopeになってもらう作戦になります。
単純な例:
ActivityがCoroutineScopeになれればいいじゃない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 が有利。
- シンプルな一連の非同期処理だけなら、逐次処理に近い記法で記述できるコルーチンが有利。