kotlinとSpring、デフォルトクラス、メソッド、propertyがfinalに与える問題--依存注入失効、NullPointerException異常
問題の由来
Springでkotlinを使用すると、依存注入は有効ではないようで、NPEにつながることが分かった.例えば以下のコードは、メール送信という機能については、メインストリームで同期送信する必要がないのでSpringの@Async注記を用いる、この方法をAsyncExecutorの構成で実行させることができる.しかし実際に呼び出すとcontactDaoがnullであることが判明し、プログラムはNullPointerException異常を報告する.しかしjavaで書いても問題ありません.
げんり
この問題を解決するには、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エージェントに問題はない.
あるいは公式のmavenプラグイン、kotlin all openプラグインを使用します.All-open compiler pluginはall open依存を配置し、spring pluginを用いる、@Component,@Async,@Transactional,@Cacheableなどのクラスをopenにコンパイルすることができる.
まとめ:
Effective Java第2版第17条:継承のために設計し、ドキュメントの説明を提供するか、継承を禁止する.kotlinクラス、メソッドのデフォルトfinalはこれに由来するはずです.この提案は确かにとても良いです...しかし私达のこのような普通の开発者にとって、この変化、确かに多くの不便をもたらして、特にAOPのフレームワークを使って、ソースコードに対して熟知していないならば、间违いやすいです.フレームワークの開発者にとって、これは確かに良い実践である.
この問題の議論はここを見ることができて、kotlin言語の開発者はなぜkotlinクラスと方法をfinalに設計するのかを議論しています.https://discuss.kotlinlang.org/t/a-bit-about-picking-defaults/1418
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クラス、方法が必要である.
解決策
@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