如果在Kotlin + Spring中注入的对象带有代理(使用CGLib),并且方法使用私有变量作为默认非空参数,那么它会导致死机
按照标题所说。
当符合条件时,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的空值检查并导致异常,程序因此崩溃。
由于下列条件满足。
@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 の外に宣言する
コンパニオンオブジェクトに宣言する