kotlinとSpring、デフォルトクラス、メソッド、propertyがfinalに与える問題--依存注入失効、NullPointerException異常

8249 ワード

問題の由来
Springでkotlinを使用すると、依存注入は有効ではないようで、NPEにつながることが分かった.例えば以下のコードは、メール送信という機能については、メインストリームで同期送信する必要がないのでSpringの@Async注記を用いる、この方法をAsyncExecutorの構成で実行させることができる.しかし実際に呼び出すとcontactDaoがnullであることが判明し、プログラムはNullPointerException異常を報告する.しかしjavaで書いても問題ありません.
@Service
open class MessageService {
    //    id         Dao
    @Autowired
    lateinit var contactDao: ContactDao

    // message        ids
    @Async
    fun sendMessage(contactIds: List, message: String) {
        //   id        
        contactIds.map { contactDao.getContactById(it) }
                //       message
                .forEach { contact ->
                    sendPhoneMessage(contact.phone,message)
                }
    }

    fun sendPhoneMessage(phoneNumber:String, message:String){
        ...
    }
}

げんり
この問題を解決するには、Springのソースコードについて一定の理解が必要です.簡単に言えばkotlinのクラス、方法、propertyのデフォルトはfinalで、クラスを変更することはできません.Springのcglibエージェントには非finalクラス、方法が必要である.
  • Springの非同期注記(@Async)、トランザクション(@Transactional)、キャッシュ(@Cacheable,@CachePut,@CacheEveictなど)、コンフィギュレーションはメソッドの実行フローを変更する、つまりエージェントを使用して変更する.
  • 特定のエージェントメソッドが指定されず、インタフェースが実装されていないクラスについてspringはcglibを使用してエージェント
  • を行うことを選択する.
  • cglibエージェントプロセスは、Beanインスタンスのクラスに基づいてサブクラス(ブロッカーが構成する)を生成する後、コンストラクタを探してサブクラスインスタンスを構築する(インスタンスの属性はnullである).
  • エージェント後に生成するインスタンス非finalメソッド呼び出しは元のbeanに転送され、依存注入の属性値を得ることができる.しかしfinalメソッドはエージェントされないので、上のkotlin beanに対して操作するとpropertyはnullに違いない.

  • 解決策
    @Async注記のメソッドのkotlinクラスのクラスを手動で、すべてのメソッド、すべてのプロパティをopenに変更することができます.これによりcglibエージェントに問題はない.
    open class MessageService {
    
        @Autowired
        lateinit open var contactCache: ContactCache
    
        @Async
        open fun sendMessage(contactIds: List, message: AlarmMessage) {
            contactIds.map { contactCache.getContactById(it) }
                    .forEach { contact ->
                        sendPhoneMessage(contact.phone,message)
                    }
        }
    
        open fun sendPhoneMessage(phoneNumber:String, message:String){
            ...
        }
    }

    あるいは公式のmavenプラグイン、kotlin all openプラグインを使用します.All-open compiler pluginはall open依存を配置し、spring pluginを用いる、@Component,@Async,@Transactional,@Cacheableなどのクラスをopenにコンパイルすることができる.
    <plugin>
        <artifactId>kotlin-maven-pluginartifactId>
        <groupId>org.jetbrains.kotlingroupId>
        <version>${kotlin.version}version>
        <configuration>
            <compilerPlugins>
                <plugin>springplugin>
            compilerPlugins>
            <jvmTarget>1.8jvmTarget>
        configuration>
        <executions>
            <execution>
                <id>compileid>
                <phase>compilephase>
                <goals>
                    <goal>compilegoal>
                goals>
            execution>
            <execution>
                <id>test-compileid>
                <phase>test-compilephase>
                <goals>
                    <goal>test-compilegoal>
                goals>
            execution>
        executions>
        <dependencies>
            <dependency>
                <groupId>org.jetbrains.kotlingroupId>
                <artifactId>kotlin-maven-allopenartifactId>
                <version>${kotlin.version}version>
            dependency>
        dependencies>
    plugin>

    まとめ:
    Effective Java第2版第17条:継承のために設計し、ドキュメントの説明を提供するか、継承を禁止する.kotlinクラス、メソッドのデフォルトfinalはこれに由来するはずです.この提案は确かにとても良いです...しかし私达のこのような普通の开発者にとって、この変化、确かに多くの不便をもたらして、特にAOPのフレームワークを使って、ソースコードに対して熟知していないならば、间违いやすいです.フレームワークの開発者にとって、これは確かに良い実践である.
    この問題の議論はここを見ることができて、kotlin言語の開発者はなぜkotlinクラスと方法をfinalに設計するのかを議論しています.https://discuss.kotlinlang.org/t/a-bit-about-picking-defaults/1418