あるクラスのオブジェクトをひとたびシリアライズすると、将来そのクラスのコードを修正する際に問題が発生することが多い。具体的には、既存のシリアライズ形式(符号化された表現)がオブジェクトの公開されたAPIの一部になり、いつまでもサポートしなくてはならなくなる。セキュリティ上の観点から、これはやっかいな問題になる。デッドコードが発生しやすくなるだけではない。クラスの提供者は、シリアライズされたオブジェクトと互換性のあるコードベースを、製品のサポートが終了するまで維持し続けなくてはならなくなるのである。
Serializableを実装するが、その機能をオーバーライドしないクラスは、デフォルトのシリアライズ形式を使用していることになる。クラスが変更されると、変更前のクラスから生成されるバイトストリームは、新しいクラス実装と互換性がなくなってしまう。プログラムは、クラスが開発されている間、シリアライズ形式の互換性を維持しなくてはならない。これを実現するひとつのアプローチは、独自のシリアライズ形式を使用することである。このアプローチをとることで、クラスを実装するプログラマは元のシリアライズ形式とそれに対応するクラスを維持する必要から解放され、新しいバージョンだけ維持すればよくなる。
違反コード
以下の違反コード例では、シリアライズ可能なnumOfWeaopnsフィールドを持つGameWeaponクラスを実装し、デフォルトのシリアライズ形式を使用している。クラスの内部表現が少しでも変更されると、シリアライズ形式が変わってしまう可能性がある。
class GameWeapon implements Serializable { int numOfWeapons = 10; public String toString() { return String.valueOf(numOfWeapons); } }
このクラスはserialVersionUIDを提供していないため、Java仮想マシン(JVM)が実装依存の方法で、代わりに割り当てを行う。クラス定義が変更されると、おそらくserialVersionUIDの値も変更される。serialVersionUIDが異なると、JVMは、オブジェクトのシリアライズ形式とそのクラス定義を関連づけしなくなるであろう。
適合コード (serialVersionUID)
以下の解決法では、このバージョンのクラスに固有の値を持つserialVersionUIDを明示的に定義している。これにより、JVMは、同じクラス名とserialVersionUID値を持つオブジェクトを復元してくれるであろう。
class GameWeapon implements Serializable { private static final long serialVersionUID = 24L; int numOfWeapons = 10; public String toString() { return String.valueOf(numOfWeapons); } }
適合コード (serialPersistentFields)
理想を言えば、Serializableは、安定した(変更されることのない)クラスのみが実装すべきである。元になるシリアライズ形式を維持した上で、クラスの開発を継続するひとつの方法は、serialPersistentFieldsの助けを借りて、シリアライズ形式をカスタマイズすることである。serialPersistentFieldsは、どのフィールドがシリアライズされるべきであるかを指定する。また、static修飾子とtransient修飾子を使うことで、シリアライズ「してはならない」フィールドを指定することができる。その上、クラス実装の中にシリアライズフィールドを定義しなくてもよくなり、現時点のクラス実装を全体のロジックから分離することができる。つまり、互換性を維持したままで、クラスに新規のフィールドを追加しやすくなる。
class WeaponStore implements Serializable { int numOfWeapons = 10; // 武器の総数 } public class GameWeapon implements Serializable { WeaponStore ws = new WeaponStore(); private static final ObjectStreamField[] serialPersistentFields = {new ObjectStreamField("ws", WeaponStore.class)}; private void readObject(ObjectInputStream ois) throws IOException { try { ObjectInputStream.GetField gf = ois.readFields(); this.ws = (WeaponStore) gf.get("ws", ws); } catch (ClassNotFoundException e) { /* ハンドラに処理を移す */ } } private void writeObject(ObjectOutputStream oos) throws IOException { ObjectOutputStream.PutField pf = oos.putFields(); pf.put("ws", ws); oos.writeFields(); } public String toString() { return String.valueOf(ws); } }
リスク評価
クラス実装の異なるバージョンの間で互換性のあるシリアライズを行っておかないと、クラスの拡張性が制限されてしまう。クラスが拡張された場合に互換性の問題が発生する恐れがある。
ルール | 深刻度 | 可能性 | 修正コスト | 優先度 | レベル |
---|---|---|---|---|---|
SER00-J | 低 | 中 | 高 | P2 | L3 |
自動検出
デフォルトのシリアライズ形式を用いるクラスの自動検出は容易である。
関連ガイドライン
MITRE CWE | CWE-589. Call to non-ubiquitous API |
参考文献
[API 2006] | |
[Bloch 2008] | Item 74, Implement serialization judiciously |
[Harold 2006] | 13.7.5, serialPersistentFields |
[Sun 2006] | Serialization Specification, 1.5, Defining Serializable Fields for a Class, and 1.7, Accessing Serializable Fields of a Class |
翻訳元
これは以下のページを翻訳したものです。
SER00-J. Maintain serialization compatibility during class evolution (revision 49)