4
3

More than 3 years have passed since last update.

SerializableのserialVersionUIDが原因で発生するInvalidClassExceptionの対処方法

Posted at

serialVersionUIDとは

serialVersionUIDはSerializableなオブジェクトに定義するバージョン番号のようなものです。データのシリアライズ/デシリアライズする際に、互換性を確認するためにこのserialVersionUIDを使用します。

また、serialVersionUIDを定義しない場合、自動で計算されることになります。

InvalidClassException

これは、Serializableなオブジェクトをデシリアライズする際、serialVersionUIDが異なる場合に発生します。

java.io.InvalidClassException
com.sample.data.SampleSerializableData; 
Incompatible class (SUID): 
    com.sample.data.SampleSerializableData: 
    static final long serialVersionUID =4092797215335171425L;
but expected com.sample.data.SampleSerializableData:
    static final long serialVersionUID =5140160798577733211L;

今回、SerializableなJavaクラスをKotlin化した際、誤ってserialVersionUIDを削除してしまったため、アプリバージョンアップの際にデータのデシリアライズができなくなってしまいました。

回避方法

今回、既にアプリのバージョンアップを何度か挟んでしまっていたため、serialVersionUIDを復活させてしまうと、今後はKotlin版のSerializableオブジェクトがデシリアライズできなくなってしまいます。そのため、以下の記事を参考にデシリアライザをカスタマイズすることで回避しました。

カスタムInputStream

ObjectInputStream をextendsして readClassDescriptorをoverrideしています。戻り値の
ObjectStreamClass はSerializationの情報を持つオブジェクトです。クラス名とserialVersionUIDを含んでいます。

内容はコード内のコメントに記載しました。

CustomInputStream
class CustomInputStream(inputStream: InputStream) : ObjectInputStream(inputStream) {
    @Throws(IOException::class, ClassNotFoundException::class)
    override fun readClassDescriptor(): ObjectStreamClass? {
        // streamからクラス記述子を読み出します
        val resultClassDescriptor = super.readClassDescriptor() ?: return null

        // Class名を取得
        val localClass = try {
            Class.forName(resultClassDescriptor.name)
        } catch (e: ClassNotFoundException) {
            return resultClassDescriptor
        }

        // クラス名を、現在のアプリバイナリの中から探します
        val localClassDescriptor = ObjectStreamClass.lookup(localClass)
        // only if class implements serializable
        if (localClassDescriptor != null) {
            // 現在のアプリバイナリと、streamの中のそれぞれのserialVersionUIDを比較します
            val localSUID = localClassDescriptor.serialVersionUID
            val streamSUID = resultClassDescriptor.serialVersionUID
            // serialVersionUIDが異なる場合
            if (streamSUID != localSUID) {
                // streamのserialVersionUIDが正として、localClassDescriptorを使います
                return localClassDescriptor
            }
        }
        return resultClassDescriptor
    }
}
利用するとき
val objectInputStream = CustomInputStream(byteArrayInputStream)
val data = objectInputStream.readObject() as? SampleSerializableData

かなり強引な技ではあるので、どうにも回避方法がなくなってしまった場合の奥の手として使うのが良いと思います。また、利用箇所も限定的にして、他のSerializableには影響が出ないようにしましょう。

4
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
3