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