Itu ada dalam kontrak! Versi objek untuk JavaBeans

Selama dua bulan terakhir, kami telah membahas secara mendalam tentang cara membuat serial objek di Java. (Lihat "Serialisasi dan Spesifikasi JavaBeans" dan "Lakukan dengan cara` Nescafé '- dengan JavaBeans kering-beku. ") Artikel bulan ini mengasumsikan Anda sudah membaca artikel ini atau memahami topik yang dibahasnya. Anda harus memahami apa itu serialisasi, cara menggunakan Serializableantarmuka, dan cara menggunakan kelas java.io.ObjectOutputStreamdan java.io.ObjectInputStream.

Mengapa Anda membutuhkan pembuatan versi

Apa yang dilakukan komputer ditentukan oleh perangkat lunaknya, dan perangkat lunak sangat mudah diubah. Fleksibilitas ini, biasanya dianggap sebagai aset, memiliki kewajibannya. Terkadang perangkat lunak tampaknya terlalu mudah diubah. Anda pasti pernah mengalami setidaknya satu dari situasi berikut:

  • File dokumen yang Anda terima melalui email tidak akan terbaca dengan benar di pengolah kata Anda, karena milik Anda adalah versi yang lebih lama dengan format file yang tidak kompatibel

  • Halaman Web beroperasi secara berbeda pada browser yang berbeda karena versi browser yang berbeda mendukung kumpulan fitur yang berbeda

  • Aplikasi tidak akan berjalan karena Anda memiliki versi pustaka tertentu yang salah

  • C ++ Anda tidak akan dikompilasi karena file header dan sumber memiliki versi yang tidak kompatibel

Semua situasi ini disebabkan oleh versi perangkat lunak yang tidak kompatibel dan / atau data yang dimanipulasi oleh perangkat lunak. Seperti bangunan, filosofi pribadi, dan dasar sungai, program berubah terus-menerus sebagai respons terhadap perubahan kondisi di sekitarnya. (Jika Anda tidak berpikir bangunan berubah, baca buku Stewart Brand yang luar biasa Bagaimana Bangunan Belajar , diskusi tentang bagaimana struktur berubah dari waktu ke waktu. Lihat Sumber untuk informasi lebih lanjut.) Tanpa struktur untuk mengontrol dan mengelola perubahan ini, sistem perangkat lunak apa pun ukuran yang berguna akhirnya merosot menjadi kekacauan. Tujuan di software versi adalah untuk memastikan bahwa versi perangkat lunak yang sedang Anda gunakan menghasilkan hasil yang benar ketika bertemu data yang dihasilkan oleh versi lain dari dirinya sendiri.

Bulan ini, kita akan membahas cara kerja pembuatan versi kelas Java, sehingga kita dapat menyediakan kontrol versi JavaBeans kita. Struktur versi untuk kelas Java memungkinkan Anda untuk menunjukkan mekanisme serialisasi apakah aliran data tertentu (yaitu, objek serial) dapat dibaca oleh versi tertentu dari kelas Java. Kita akan berbicara tentang perubahan "kompatibel" dan "tidak kompatibel" pada kelas, dan mengapa perubahan ini memengaruhi pembuatan versi. Kita akan membahas tujuan dari struktur pembuatan versi, dan bagaimana paket java.io memenuhi tujuan tersebut. Dan, kita akan belajar menempatkan pengamanan ke dalam kode kita untuk memastikan bahwa saat kita membaca aliran objek dari berbagai versi, datanya selalu konsisten setelah objek dibaca.

Keengganan versi

Ada berbagai jenis masalah pembuatan versi dalam perangkat lunak, yang semuanya berkaitan dengan kompatibilitas antara potongan data dan / atau kode yang dapat dieksekusi:

  • Versi yang berbeda dari perangkat lunak yang sama mungkin atau mungkin tidak dapat menangani format penyimpanan data satu sama lain

  • Program yang memuat kode yang dapat dieksekusi pada waktu proses harus dapat mengidentifikasi versi yang benar dari objek perangkat lunak, pustaka yang dapat dimuat, atau file objek untuk melakukan pekerjaan itu

  • Metode dan bidang kelas harus mempertahankan arti yang sama saat kelas berkembang, atau program yang ada dapat rusak di tempat di mana metode dan bidang tersebut digunakan

  • Kode sumber, file header, dokumentasi, dan skrip build semuanya harus dikoordinasikan dalam lingkungan pembuatan perangkat lunak untuk memastikan bahwa file biner dibuat dari versi yang benar dari file sumber

Artikel tentang pembuatan versi objek Java ini hanya membahas tiga yang pertama - yaitu, kontrol versi objek biner dan semantiknya di lingkungan runtime. (Ada banyak sekali perangkat lunak yang tersedia untuk membuat versi kode sumber, tetapi kami tidak membahasnya di sini.)

Penting untuk diingat bahwa aliran objek Java berseri tidak berisi bytecode. Mereka hanya berisi informasi yang diperlukan untuk merekonstruksi objek dengan asumsi Anda memiliki file kelas yang tersedia untuk membangun objek. Tetapi apa yang terjadi jika file kelas dari dua mesin virtual Java (JVM) (penulis dan pembaca) memiliki versi yang berbeda? Bagaimana kita tahu apakah mereka kompatibel?

Definisi kelas dapat dianggap sebagai "kontrak" antara kelas dan kode yang memanggil kelas. Kontrak ini mencakup API kelas (antarmuka pemrograman aplikasi). Mengubah API sama dengan mengubah kontrak. (Perubahan lain pada kelas mungkin juga menyiratkan perubahan pada kontrak, seperti yang akan kita lihat.) Saat kelas berkembang, penting untuk menjaga perilaku versi kelas sebelumnya agar tidak merusak perangkat lunak di tempat yang bergantung pada diberikan perilaku.

Contoh perubahan versi

Bayangkan Anda memiliki sebuah metode yang dipanggil getItemCount()dalam sebuah kelas, yang berarti mendapatkan jumlah total item yang dikandung objek ini , dan metode ini digunakan di banyak tempat di seluruh sistem Anda. Kemudian, bayangkan di lain waktu bahwa Anda mengubahnya getItemCount()menjadi mendapatkan jumlah maksimum item yang pernah ditampung objek ini. Perangkat lunak Anda kemungkinan besar akan rusak di sebagian besar tempat di mana metode ini digunakan, karena tiba-tiba metode tersebut akan melaporkan informasi yang berbeda. Pada dasarnya, Anda telah melanggar kontrak; jadi ini membantu Anda dengan benar bahwa program Anda sekarang memiliki bug di dalamnya.

Tidak ada cara, singkat pelarangan perubahan sama sekali, untuk benar-benar mengotomatisasi deteksi semacam ini perubahan, karena terjadi pada tingkat apa program sarana , tidak hanya pada tingkat bagaimana makna yang diungkapkan. (Jika Anda memikirkan cara untuk melakukan ini dengan mudah dan umum, Anda akan menjadi lebih kaya daripada Bill.) Jadi, dengan tidak adanya solusi yang lengkap, umum, dan otomatis untuk masalah ini, apa yang dapat kita lakukan untuk menghindari masuk ke air panas ketika kita mengubah kelas kita (yang, tentu saja, harus kita lakukan)?

Jawaban termudah untuk pertanyaan ini adalah dengan mengatakan bahwa jika kelas berubah sama sekali , seharusnya tidak "dipercaya" untuk mempertahankan kontrak. Bagaimanapun, seorang programmer mungkin telah melakukan apa saja pada kelas tersebut, dan siapa yang tahu jika kelas tersebut masih berfungsi seperti yang diiklankan? Ini memecahkan masalah pembuatan versi, tetapi ini adalah solusi yang tidak praktis karena terlalu membatasi. Jika kelas dimodifikasi untuk meningkatkan kinerja, katakanlah, tidak ada alasan untuk melarang penggunaan versi baru kelas hanya karena tidak cocok dengan yang lama. Sejumlah perubahan dapat dilakukan pada kelas tanpa melanggar kontrak.

Di sisi lain, beberapa perubahan kelas praktis menjamin bahwa kontrak tersebut rusak: menghapus bidang, misalnya. Jika Anda menghapus bidang dari kelas, Anda masih dapat membaca aliran yang ditulis oleh versi sebelumnya, karena pembaca selalu dapat mengabaikan nilai untuk bidang itu. Tetapi pikirkan tentang apa yang terjadi ketika Anda menulis aliran yang dimaksudkan untuk dibaca oleh versi kelas sebelumnya. Nilai untuk bidang itu tidak akan ada dari aliran, dan versi yang lebih lama akan menetapkan nilai default (mungkin tidak konsisten secara logis) ke bidang itu ketika membaca aliran. Voilà! : Kelasmu rusak.

Perubahan yang kompatibel dan tidak kompatibel

Trik untuk mengelola kompatibilitas versi objek adalah dengan mengidentifikasi jenis perubahan mana yang dapat menyebabkan inkompatibilitas antara versi dan mana yang tidak, dan memperlakukan kasus ini secara berbeda. Dalam bahasa Java, perubahan yang tidak menyebabkan masalah kompatibilitas disebut perubahan yang kompatibel ; yang mungkin disebut perubahan yang tidak kompatibel .

Perancang mekanisme serialisasi untuk Java memiliki tujuan berikut dalam pikiran ketika mereka membuat sistem:

  1. Untuk menentukan cara agar versi kelas yang lebih baru dapat membaca dan menulis aliran yang juga dapat "dipahami" dan digunakan dengan benar oleh versi kelas sebelumnya

  2. Untuk menyediakan mekanisme default yang membuat serial objek dengan kinerja yang baik dan ukuran yang wajar. Ini adalah mekanisme serialisasi yang telah kita bahas di dua kolom JavaBeans sebelumnya yang disebutkan di awal artikel ini

  3. Untuk meminimalkan pekerjaan terkait pembuatan versi pada kelas yang tidak memerlukan pembuatan versi. Idealnya, informasi pembuatan versi hanya perlu ditambahkan ke kelas ketika versi baru ditambahkan

  4. Untuk memformat aliran objek sehingga objek dapat dilewati tanpa memuat file kelas objek. Kemampuan ini memungkinkan objek klien melintasi aliran objek yang berisi objek yang tidak dipahami

Mari kita lihat bagaimana mekanisme serialisasi menangani tujuan-tujuan ini sesuai dengan situasi yang diuraikan di atas.

Perbedaan yang dapat didamaikan

Beberapa perubahan yang dibuat pada file kelas dapat diandalkan untuk tidak mengubah kontrak antara kelas dan kelas lain apa pun yang menyebutnya. Seperti disebutkan di atas, ini disebut perubahan yang kompatibel dalam dokumentasi Java. Sejumlah perubahan yang kompatibel dapat dibuat ke file kelas tanpa mengubah kontrak. Dengan kata lain, dua versi kelas yang berbeda hanya dengan perubahan yang kompatibel adalah kelas yang kompatibel: Versi yang lebih baru akan terus membaca dan menulis aliran objek yang kompatibel dengan versi sebelumnya.

Kelas java.io.ObjectInputStreamdan java.io.ObjectOutputStreamtidak mempercayai Anda. Mereka dirancang untuk menjadi, secara default, sangat mencurigakan terhadap setiap perubahan pada antarmuka file kelas ke dunia - artinya, apa pun yang terlihat oleh kelas lain yang mungkin menggunakan kelas: tanda tangan dari metode dan antarmuka publik serta jenis dan pengubah bidang publik. Mereka sangat paranoid, bahkan, Anda hampir tidak dapat mengubah apa pun tentang kelas tanpa java.io.ObjectInputStreamharus menolak memuat aliran yang ditulis oleh versi kelas Anda sebelumnya.

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 saat membuat aliran byte yang mewakili sebuah objek. Sebaliknya, ia menulis a java.io.ObjectStreamClass, yang merupakan deskripsi kelas. Pemuat kelas JVM tujuan menggunakan deskripsi ini untuk menemukan dan memuat bytecode untuk kelas tersebut. Ini juga membuat dan menyertakan integer 64-bit yang disebut SerialVersionUID , yang merupakan semacam kunci yang secara unik mengidentifikasi versi file kelas.

The SerialVersionUIDdibuat dengan menghitung hash 64-bit yang aman dari informasi berikut tentang kelas. Mekanisme serialisasi ingin dapat mendeteksi perubahan pada salah satu hal berikut: