Ada dalam kontrak! Versi objek untuk JavaBeans

Selama dua bulan terakhir, kami telah membahas bagaimana untuk membuat siri objek di Jawa. (Lihat "Serialisasi dan Spesifikasi JavaBeans" dan "Lakukan dengan cara` Nescafé '- dengan JavaBeans yang telah dibekukan. ") Artikel bulan ini mengandaikan bahawa anda sudah membaca artikel ini atau anda memahami topik yang dibahasnya. Anda harus memahami apa itu serialisasi, bagaimana menggunakan Serializableantara muka, dan bagaimana menggunakan java.io.ObjectOutputStreamdan java.io.ObjectInputStreamkelas.

Mengapa anda memerlukan versi

Apa yang dilakukan oleh komputer ditentukan oleh perisiannya, dan perisiannya sangat mudah diubah. Fleksibiliti ini, biasanya dianggap sebagai aset, mempunyai liabiliti. Kadang-kadang nampaknya perisian terlalu mudah diubah. Anda pasti menghadapi sekurang-kurangnya salah satu daripada situasi berikut:

  • Fail dokumen yang anda terima melalui e-mel tidak akan dibaca dengan betul di pemproses kata anda, kerana versi anda lebih lama dengan format fail yang tidak sesuai

  • Halaman Web beroperasi secara berbeza pada penyemak imbas yang berbeza kerana versi penyemak imbas yang berbeza menyokong set ciri yang berbeza

  • Aplikasi tidak akan berjalan kerana anda mempunyai versi perpustakaan tertentu yang salah

  • C ++ anda tidak akan dikompilasi kerana tajuk dan fail sumber adalah versi yang tidak serasi

Semua situasi ini disebabkan oleh versi perisian yang tidak serasi dan / atau data yang dimanipulasi oleh perisian. Seperti bangunan, falsafah peribadi, dan dasar sungai, program berubah terus sebagai tindak balas kepada perubahan keadaan di sekitarnya. (Sekiranya anda tidak fikir bangunan berubah, baca buku Stewart Brand yang luar biasa How Buildings Learn , perbincangan tentang bagaimana struktur berubah dari masa ke masa. Lihat Sumber untuk maklumat lebih lanjut.) Tanpa struktur untuk mengawal dan menguruskan perubahan ini, sistem perisian apa pun ukuran berguna akhirnya merosot menjadi huru-hara. Tujuan dalam pembuatan perisian adalah untuk memastikan bahawa versi perisian yang sedang anda gunakan menghasilkan hasil yang betul ketika bertemu dengan data yang dihasilkan oleh versi lain itu sendiri.

Bulan ini, kita akan membincangkan cara kerja versi Java classing, sehingga kita dapat memberikan kontrol versi JavaBeans kita. Struktur versi untuk kelas Java memungkinkan anda untuk menunjukkan kepada mekanisme serialisasi apakah aliran data tertentu (yaitu, objek bersiri) dapat dibaca oleh versi tertentu dari kelas Java. Kami akan membincangkan perubahan "serasi" dan "tidak serasi" pada kelas, dan mengapa perubahan ini mempengaruhi versi. Kami akan membahas matlamat struktur versi, dan bagaimana pakej java.io memenuhi matlamat tersebut. Dan, kita akan belajar memasukkan perlindungan ke dalam kod kita untuk memastikan bahawa ketika kita membaca aliran objek dari pelbagai versi, data akan selalu konsisten setelah objek dibaca.

Keengganan versi

Terdapat pelbagai jenis masalah versi dalam perisian, yang semuanya berkaitan dengan keserasian antara potongan data dan / atau kod yang dapat dilaksanakan:

  • Berbagai versi perisian yang sama mungkin atau mungkin tidak dapat menangani format penyimpanan data masing-masing

  • Program yang memuatkan kod yang dapat dieksekusi pada waktu proses mesti dapat mengenal pasti versi yang tepat dari objek perisian, perpustakaan yang dapat dimuat, atau file objek untuk melakukan pekerjaan

  • Kaedah dan medan kelas mesti mengekalkan makna yang sama dengan kelas yang berkembang, atau program yang ada mungkin pecah di tempat di mana kaedah dan bidang tersebut digunakan

  • Kod sumber, fail header, dokumentasi, dan skrip binaan semuanya mesti diselaraskan dalam persekitaran pembuatan perisian untuk memastikan bahawa fail binari dibina dari versi fail sumber yang betul

Artikel ini mengenai pemodelan objek Java hanya membahas tiga yang pertama - iaitu, kawalan versi objek binari dan semantiknya dalam lingkungan waktu proses. (Terdapat sebilangan besar perisian yang tersedia untuk kod sumber versi, tetapi kami tidak membahasnya di sini.)

Penting untuk diingat bahawa aliran objek Java bersiri tidak mengandungi kod bytek. Mereka hanya mengandungi maklumat yang diperlukan untuk membina semula objek dengan andaian anda mempunyai fail kelas yang tersedia untuk membina objek tersebut. Tetapi apa yang berlaku jika fail kelas dua mesin maya Java (JVM) (penulis dan pembaca) mempunyai versi yang berbeza? Bagaimana kita tahu jika ia serasi?

Definisi kelas boleh dianggap sebagai "kontrak" antara kelas dan kod yang memanggil kelas. Kontrak ini merangkumi API kelas (antara muka pengaturcaraan aplikasi). Menukar API sama dengan menukar kontrak. (Perubahan lain pada kelas mungkin juga menyiratkan perubahan pada kontrak, seperti yang akan kita lihat.) Semasa kelas berkembang, penting untuk menjaga tingkah laku versi sebelumnya dari kelas agar tidak merusak perisian di tempat yang bergantung pada tingkah laku yang diberikan.

Contoh perubahan versi

Bayangkan anda mempunyai kaedah yang disebut getItemCount()dalam kelas, yang bermaksud mendapatkan jumlah item yang mengandungi objek ini , dan kaedah ini digunakan di selusin tempat di seluruh sistem anda. Kemudian, bayangkan di kemudian hari bahawa anda mengubah getItemCount()bermaksud mendapatkan jumlah maksimum item yang pernah terdapat dalam objek ini . Perisian anda kemungkinan besar akan hancur di kebanyakan tempat di mana kaedah ini digunakan, kerana tiba-tiba kaedah tersebut akan melaporkan maklumat yang berbeza. Pada dasarnya, anda telah melanggar kontrak; oleh itu, program anda sekarang mempunyai bug di dalamnya.

Tidak ada cara, pendek tidak membenarkan perubahan sama sekali, untuk sepenuhnya mengautomasikan pengesanan seperti ini perubahan, kerana ia berlaku pada tahap apa program cara , bukan hanya pada tahap bagaimana makna yang dinyatakan. (Sekiranya anda memikirkan cara untuk melakukan ini dengan mudah dan umum, anda akan menjadi lebih kaya daripada Bill.) Oleh itu, sekiranya tidak ada penyelesaian yang lengkap, umum, dan automatik untuk masalah ini, apa yang boleh kita lakukan untuk mengelakkannya masuk ke air panas ketika kita menukar kelas (yang tentu saja mesti kita lakukan)?

Jawapan paling mudah untuk soalan ini adalah dengan mengatakan bahawa jika kelas berubah sama sekali , tidak boleh "dipercayai" untuk mengekalkan kontrak. Lagipun, pengaturcara mungkin melakukan apa sahaja pada kelas, dan siapa yang tahu jika kelas itu masih berfungsi seperti yang diiklankan? Ini menyelesaikan masalah versi, tetapi ini adalah penyelesaian yang tidak praktikal kerana terlalu ketat. Sekiranya kelas diubahsuai untuk meningkatkan prestasi, katakanlah, tidak ada alasan untuk tidak membenarkan penggunaan versi baru kelas hanya kerana tidak sesuai dengan yang lama. Sebilangan besar perubahan boleh dilakukan ke kelas tanpa melanggar kontrak.

Sebaliknya, beberapa perubahan pada kelas secara praktikal menjamin bahawa kontrak itu rosak: misalnya, memadam medan. Sekiranya anda memadamkan medan dari kelas, anda masih dapat membaca aliran yang ditulis oleh versi sebelumnya, kerana pembaca sentiasa dapat mengabaikan nilai untuk bidang tersebut. Tetapi fikirkan apa yang berlaku semasa anda menulis aliran yang ingin dibaca oleh versi kelas sebelumnya. Nilai untuk medan itu tidak akan hadir dari aliran, dan versi yang lebih lama akan memberikan nilai lalai (mungkin tidak logik) pada medan tersebut ketika membaca aliran. Voilà! : Anda mempunyai kelas yang rosak.

Perubahan yang serasi dan tidak serasi

Caranya untuk menguruskan keserasian versi objek adalah dengan mengenal pasti jenis perubahan yang dapat menyebabkan ketidaksesuaian antara versi dan mana yang tidak, dan memperlakukan kes ini secara berbeza. Dalam bahasa Java, perubahan yang tidak menyebabkan masalah keserasian disebut perubahan yang serasi ; yang mungkin disebut perubahan tidak serasi .

Perancang mekanisme siri untuk Java mempunyai tujuan berikut dalam pemikiran ketika mereka membuat sistem:

  1. Untuk menentukan cara di mana versi kelas yang lebih baru dapat membaca dan menulis aliran yang versi sebelumnya dari kelas juga dapat "memahami" dan menggunakan dengan betul

  2. To provide a default mechanism that serializes objects with good performance and reasonable size. This is the serialization mechanism we've already discussed in the two previous JavaBeans columns mentioned at the beginning of this article

  3. To minimize versioning-related work on classes that don't need versioning. Ideally, versioning information need only be added to a class when new versions are added

  4. To format the object stream so that objects can be skipped without loading the object's class file. This capability allows a client object to traverse an object stream containing objects it doesn't understand

Let's see how the serialization mechanism addresses these goals in light of the situation outlined above.

Reconcilable differences

Some changes made to a class file can be depended on not to change the contract between the class and whatever other classes may call it. As noted above, these are called compatible changes in the Java documentation. Any number of compatible changes may be made to a class file without changing the contract. In other words, two versions of a class that differ only by compatible changes are compatible classes: The newer version will continue to read and write object streams that are compatible with previous versions.

The classes java.io.ObjectInputStream and java.io.ObjectOutputStream don't trust you. They are designed to be, by default, extremely suspicious of any changes to a class file's interface to the world -- meaning, anything visible to any other class that may use the class: the signatures of public methods and interfaces and the types and modifiers of public fields. They're so paranoid, in fact, that you can scarcely change anything about a class without causing java.io.ObjectInputStream to refuse to load a stream written by a previous version of your class.

Let's look at an example. of a class incompatibility, and then solve the resulting problem. Say you've got an object called InventoryItem, which maintains part numbers and the quantity of that particular part available in a warehouse. A simple form of that object as a JavaBean might look something like this:

001 002 import java.beans.*; 003 import java.io.*; 004 import Printable; 005 006 // 007 // Version 1: simply store quantity on hand and part number 008 // 009 010 public class InventoryItem implements Serializable, Printable { 011 012 013 014 015 016 // fields 017 protected int iQuantityOnHand_; 018 protected String sPartNo_; 019 020 public InventoryItem() 021 { 022 iQuantityOnHand_ = -1; 023 sPartNo_ = ""; 024 } 025 026 public InventoryItem(String _sPartNo, int _iQuantityOnHand) 027 { 028 setQuantityOnHand(_iQuantityOnHand); 029 setPartNo(_sPartNo); 030 } 031 032 public int getQuantityOnHand() 033 { 034 return iQuantityOnHand_; 035 } 036 037 public void setQuantityOnHand(int _iQuantityOnHand) 038 { 039 iQuantityOnHand_ = _iQuantityOnHand; 040 } 041 042 public String getPartNo() 043 { 044 return sPartNo_; 045 } 046 047 public void setPartNo(String _sPartNo) 048 { 049 sPartNo_ = _sPartNo; 050 } 051 052 // ... implements printable 053 public void print() 054 { 055 System.out.println("Part: " + getPartNo() + "\nQuantity on hand: " + 056 getQuantityOnHand() + "\n\n"); 057 } 058 }; 059 

(We also have a simple main program, called Demo8a, which reads and writes InventoryItems to and from a file using object streams, and interface Printable, which InventoryItem implements and Demo8a uses to print the objects. You can find the source for these here.) Running the demo program produces reasonable, if unexciting, results:

C:\beans>java Demo8a w file SA0091-001 33 Wrote object: Part: SA0091-001 Quantity on hand: 33 C:\beans>java Demo8a r file Read object: Part: SA0091-001 Quantity on hand: 33 

The program serializes and deserializes the object correctly. Now, let's make a tiny change to the class file. The system users have done an inventory and have found discrepancies between the database and the actual item counts. They've requested the ability to track the number of items lost from the warehouse. Let's add a single public field to InventoryItem that indicates the number of items missing from the storeroom. We insert the following line into the InventoryItem class and recompile:

016 // fields 017 protected int iQuantityOnHand_; 018 protected String sPartNo_; 019 public int iQuantityLost_; 

The file compiles fine, but look at what happens when we try to read the stream from the previous version:

C:\mj-java\Column8>java Demo8a r file IO Exception: InventoryItem; Local class not compatible java.io.InvalidClassException: InventoryItem; Local class not compatible at java.io.ObjectStreamClass.setClass(ObjectStreamClass.java:219) at java.io.ObjectInputStream.inputClassDescriptor(ObjectInputStream.java:639) at java.io.ObjectInputStream.readObject(ObjectInputStream.java:276) at java.io.ObjectInputStream.inputObject(ObjectInputStream.java:820) at java.io.ObjectInputStream.readObject(ObjectInputStream.java:284) at Demo8a.main(Demo8a.java:56) 

Whoa, dude! What happened?

java.io.ObjectInputStreamtidak menulis objek kelas semasa membuat aliran bait yang mewakili objek. Sebaliknya, ia menulis a java.io.ObjectStreamClass, yang merupakan penerangan kelas. Pemuat kelas JVM destinasi menggunakan penerangan ini untuk mencari dan memuatkan kod byt untuk kelas. Ia juga membuat dan menyertakan bilangan bulat 64-bit yang disebut SerialVersionUID , yang merupakan sejenis kunci yang secara unik mengenal pasti versi fail kelas.

Ini SerialVersionUIDdibuat dengan mengira hash selamat 64-bit dari maklumat berikut mengenai kelas. Mekanisme serialisasi ingin dapat mengesan perubahan dalam salah satu perkara berikut: