こんにちは。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 の影響と考えられますね。

