
Spring FrameworkとKotlinのハマりどころ
-
2020年11月25日
こんにちは。cocone tech blogの編集長Nです。
以前の記事で紹介しました「#私を布教して」は、サーバーサイドで扱う言語として、Kotlinをメインに選択しています。
しかし、「#私を布教して」チーム(現在は CARROT 株式会社ですが)のサーバー開発者全員が、配属されるまで Kotlin を(ほぼ)使用したことがない状態でした。
今までココネではサーバーサイドで扱うメインの言語は Java でした。
(ご存知かもしれませんが)Kotlin は Java と互換性があり、特に移行時の学習コストの小ささがメリットに挙げられます。
ただし、当然異なる言語ですので、新しい考え方が必要で躓きやすい点もいくつかあります。
今回はそのうちで「#私を布教して」チームのサーバー開発で躓いた1つの問題について紹介いたします。
問題
表題の件です。
結論から述べますと「@Repository アノテーションをつけてクラスからメンバ変数を参照すると null になる」件です。
実際のコードはこちら。
// AbstractMongodbRepository.kt abstract class AbstractMongodbRepository { private lateinit var clazz: Class @PostConstruct private fun determineClass() { val clazz = this.javaClass val type = clazz.genericSuperclass val parameterizedType = type as ParameterizedType val actualTypeArguments = parameterizedType.actualTypeArguments val entityClass = actualTypeArguments[0] as Class<*> this.clazz = entityClass as Class } abstract fun getMongoTemplate(): MongoTemplate fun findOne(query: Query): T? { // これがダメ return this.getMongoTemplate().findOne(query, this.clazz) } }
// UserService.kt @Service class UserService( private val userMongodbRepository: UserMongodbRepository ) { fun info(myCode: String): User? { val query: Query = Query.query(Criteria.where("mycode").`is`(myCode)) return this.userMongodbRepository.findOne(query) } }
// UserMongodbRepository.kt @Repository class UserMongodbRepository( private val mongoTemplate: MongoTemplate ) : AbstractMongodbRepository() { override fun getMongoTemplate(): MongoTemplate { return this.mongoTemplate } }
こちらでUserService.infoを実行するとエラーになります。
結果:
kotlin.UninitializedPropertyAccessException: lateinit property clazz has not been initialized
Kotlinではlateinit 付きの clazz が参照の前に初期化されていないとエラーになります。
lazyしても、Delegates.notNull() しようが初期化されていない(≒ null) ことには変わりがありません。
解決策
AbstractMongodbRepository の各メソッドに open アノテーション をつける。
// AbstractMongodbRepository.kt abstract class AbstractMongodbRepository { private lateinit var clazz: Class @PostConstruct private fun determineClass() { val clazz = this.javaClass val type = clazz.genericSuperclass val parameterizedType = type as ParameterizedType val actualTypeArguments = parameterizedType.actualTypeArguments val entityClass = actualTypeArguments[0] as Class<*> this.clazz = entityClass as Class } abstract fun getMongoTemplate(): MongoTemplate open fun findOne(query: Query): T? { return this.getMongoTemplate().findOne(query, this.clazz) } }
まず、Kotlin のクラスは何も指定しなければ、Java の final になります。
継承
デフォルトでは、Kotlinのすべてのクラスは Effective Java のアイテム17( 継承またはそれの禁止のためのデザインとドキュメント )に合致する final です。
メンバのオーバーライド
私たちはKotlinに明示的にすることにこだわります。そして、Javaとは異なり、Kotlinはメンバをオーバーライドできるメンバ(私たちは open と呼んでいます)とオーバライド自体に明示的アノテーションを必要とします。
引用元:Kotlin Programming Language
次に、Spring Frameworkの @Repository アノテーションの特性として、「そのクラスインスタンスの Proxy が作られ、それが Injection される」というものがあります。
その Proxy として作られるクラスは Spring Framework の Reflection 機能 によって元インスタンスを複製します。その際にはコンストラクタなしでサブクラス化されます。
final なクラスやメソッドは、その際にオーバーライドされないため、元インスタンスのまま実行されます。
実際に @PostConstruct で実行される this と、オーバーライドできなかった fun findOne() でのthisはインスタンスが異なります(fun findOne() での this.clazz は null です)。
そのため、Spring Framework が作る Proxy サブクラスからオーバーライドできるよう、finalではなくする必要があります。
そこで明示的に open を付与して解決することができます。
余談
もともと本件は AbstractMongodbRepository<T> の T の型を取りたくて当たった問題とのことでした。
Kotlin の lazyinit や @PostConstract が原因なのではないかということで実際のメソッド(下記)に記述してテストしたところ、clazz.genericSuperClass が java.lang.Objectになっており、val parameterizedType = type as ParameterizedType で ClassCastException になったようです。
fun findOne(query: Query): T? { val clazz = this.javaClass val type = clazz.genericSuperclass val parameterizedType = type as ParameterizedType val actualTypeArguments = parameterizedType.actualTypeArguments val entityClass = actualTypeArguments[0] as Class<*> return this.getMongoTemplate().findOne(query, entityClass as Class) }
これも Spring Framework の DI の影響と考えられますね。