如果在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 の外に宣言する
コンパニオンオブジェクトに宣言する

广告
将在 10 秒后关闭
bannerAds