タイトルどおり。
条件が揃うと、一見正しそうなKotlin + Springのコードが死にます。
再現コード
再現コードは下記にあります。
$ 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
となる。
- Springで注入された
- Kotlinがデフォルト引数つきのメソッドを呼ぶ際は
Nuke
クラスにヘルパ用の static メソッド (withOptional$default
) を生成し、そのメソッドにNuke
のインスタンス(今回はCGLibにラッパされたインスタンス) を引数として渡して呼び出す。 - 該当のstaticメソッドは
Nuke.a
をデフォルト引数の値とするため参照するが、CGLibにラップされたクラスのフィールドであるため、上述の通り、null
値が入る。- つまり、
listOf("a")
は使われない。
- つまり、
-
null
値がKotlinのランタイム時のwithOptional
メソッド内のnullチェックによってチェックされ、例外が発生する。
上記のように分かりづらい経緯を経て死にます。デフォルト引数怖いっ!
ワークアラウンド
上記条件にひっかからなければ回避可能なので、下記のような回避を取れるでしょう。
- デフォルト引数に指定する定数を
-
private val
ではなくval
を用いる - クラス内に宣言しない
- class の外に宣言する
- コンパニオンオブジェクトに宣言する
-