LoginSignup
4
0

More than 3 years have passed since last update.

Kotlin + Spring でインジェクトされるオブジェクトがプロキシ(CGLib)を伴っていた場合、メソッドがprivate変数をデフォルト非null引数に使っていると死ぬ

Last updated at Posted at 2019-05-22

タイトルどおり。

条件が揃うと、一見正しそうな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 となる。
  • Kotlinがデフォルト引数つきのメソッドを呼ぶ際は Nuke クラスにヘルパ用の static メソッド (withOptional$default) を生成し、そのメソッドに Nukeのインスタンス(今回はCGLibにラッパされたインスタンス) を引数として渡して呼び出す。
  • 該当のstaticメソッドは Nuke.a をデフォルト引数の値とするため参照するが、CGLibにラップされたクラスのフィールドであるため、上述の通り、null 値が入る。
    • つまり、 listOf("a") は使われない。
  • null 値がKotlinのランタイム時の withOptional メソッド内のnullチェックによってチェックされ、例外が発生する。

上記のように分かりづらい経緯を経て死にます。デフォルト引数怖いっ!

ワークアラウンド

上記条件にひっかからなければ回避可能なので、下記のような回避を取れるでしょう。

  • デフォルト引数に指定する定数を
    • private val ではなく val を用いる
    • クラス内に宣言しない
    • class の外に宣言する
    • コンパニオンオブジェクトに宣言する
4
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
0