Edited at

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 の外に宣言する

    • コンパニオンオブジェクトに宣言する