Es ist im Vertrag! Objektversionen für JavaBeans

In den letzten zwei Monaten haben wir uns eingehend mit der Serialisierung von Objekten in Java befasst. (Siehe "Serialisierung und die JavaBeans-Spezifikation" und "Machen Sie es wie Nescafé - mit gefriergetrockneten JavaBeans.") In diesem Artikel wird davon ausgegangen, dass Sie diese Artikel entweder bereits gelesen haben oder die darin behandelten Themen verstehen. Sie sollten verstehen, was Serialisierung ist, wie die SerializableSchnittstelle verwendet wird und wie die Klassen java.io.ObjectOutputStreamund java.io.ObjectInputStreamverwendet werden.

Warum brauchen Sie eine Versionierung?

Was ein Computer tut, hängt von seiner Software ab, und die Software ist äußerst einfach zu ändern. Diese Flexibilität, die normalerweise als Vermögenswert betrachtet wird, hat ihre Verbindlichkeiten. Manchmal scheint es, dass Software zu einfach zu ändern ist. Sie sind zweifellos auf mindestens eine der folgenden Situationen gestoßen:

  • Eine Dokumentdatei, die Sie per E-Mail erhalten haben, wird in Ihrem Textverarbeitungsprogramm nicht richtig gelesen, da es sich bei Ihrer um eine ältere Version mit einem inkompatiblen Dateiformat handelt

  • Eine Webseite funktioniert in verschiedenen Browsern unterschiedlich, da verschiedene Browserversionen unterschiedliche Funktionssätze unterstützen

  • Eine Anwendung wird nicht ausgeführt, weil Sie die falsche Version einer bestimmten Bibliothek haben

  • Ihr C ++ wird nicht kompiliert, da die Header- und Quelldateien nicht kompatible Versionen haben

Alle diese Situationen werden durch inkompatible Softwareversionen und / oder die von der Software manipulierten Daten verursacht. Wie Gebäude, persönliche Philosophien und Flussbetten ändern sich Programme ständig als Reaktion auf die sich ändernden Bedingungen um sie herum. (Wenn Sie nicht glauben, dass sich Gebäude ändern, lesen Sie das herausragende Buch How Buildings Learn von Stewart Brand, in dem erläutert wird , wie sich Strukturen im Laufe der Zeit verändern. Weitere Informationen finden Sie unter Ressourcen.) Ohne eine Struktur zur Steuerung und Verwaltung dieser Änderung kann jedes Softwaresystem eines beliebigen Systems verwendet werden nützliche Größe degeneriert schließlich in Chaos. Das Ziel in der Software - Versionsverwaltung ist es , sicherzustellen , dass die Version der Software verwenden Sie derzeit korrekte Ergebnisse erzeugt , wenn er durch andere Versionen von selbst erzeugten Daten trifft.

In diesem Monat werden wir diskutieren, wie die Versionierung von Java-Klassen funktioniert, damit wir die Versionskontrolle unserer JavaBeans bereitstellen können. Mit der Versionsstruktur für Java-Klassen können Sie dem Serialisierungsmechanismus angeben, ob ein bestimmter Datenstrom (dh ein serialisiertes Objekt) von einer bestimmten Version einer Java-Klasse gelesen werden kann. Wir werden über "kompatible" und "inkompatible" Änderungen an Klassen sprechen und warum diese Änderungen die Versionierung beeinflussen. Wir werden die Ziele der Versionsstruktur erläutern und erläutern , wie das Paket java.io diese Ziele erreicht. Außerdem lernen wir, Schutzmaßnahmen in unseren Code aufzunehmen, um sicherzustellen, dass beim Lesen von Objektströmen verschiedener Versionen die Daten nach dem Lesen des Objekts immer konsistent sind.

Versionsaversion

Es gibt verschiedene Arten von Versionsproblemen in der Software, die sich alle auf die Kompatibilität zwischen Datenblöcken und / oder ausführbarem Code beziehen:

  • Unterschiedliche Versionen derselben Software können möglicherweise die Datenspeicherformate der anderen Software verarbeiten oder nicht

  • Programme, die zur Laufzeit ausführbaren Code laden, müssen in der Lage sein, die richtige Version des Softwareobjekts, der ladbaren Bibliothek oder der Objektdatei zu identifizieren, um den Job auszuführen

  • Die Methoden und Felder einer Klasse müssen dieselbe Bedeutung haben wie die Entwicklung der Klasse. Andernfalls können vorhandene Programme an Stellen unterbrochen werden, an denen diese Methoden und Felder verwendet werden

  • Quellcode, Header-Dateien, Dokumentation und Build-Skripte müssen in einer Software-Build-Umgebung koordiniert werden, um sicherzustellen, dass Binärdateien aus den richtigen Versionen der Quelldateien erstellt werden

Dieser Artikel zur Versionierung von Java-Objekten befasst sich nur mit den ersten drei, dh der Versionskontrolle von Binärobjekten und ihrer Semantik in einer Laufzeitumgebung. (Für die Versionierung des Quellcodes steht eine Vielzahl von Software zur Verfügung, die wir hier jedoch nicht behandeln.)

Es ist wichtig zu beachten, dass serialisierte Java-Objektströme keine Bytecodes enthalten. Sie enthalten nur die Informationen, die zum Rekonstruieren eines Objekts erforderlich sind, vorausgesetzt, Sie verfügen über die Klassendateien, die zum Erstellen des Objekts verfügbar sind. Was passiert jedoch, wenn die Klassendateien der beiden Java Virtual Machines (JVMs) (Writer und Reader) unterschiedliche Versionen haben? Woher wissen wir, ob sie kompatibel sind?

Eine Klassendefinition kann als "Vertrag" zwischen der Klasse und dem Code betrachtet werden, der die Klasse aufruft. Dieser Vertrag enthält die API (Application Programming Interface) der Klasse . Das Ändern der API entspricht dem Ändern des Vertrags. (Andere Änderungen an einer Klasse können auch Änderungen am Vertrag bedeuten, wie wir sehen werden.) Während sich eine Klasse weiterentwickelt, ist es wichtig, das Verhalten früherer Versionen der Klasse beizubehalten, um die Software nicht an Orten zu beschädigen, die davon abhängen gegebenes Verhalten.

Ein Beispiel für eine Versionsänderung

Stellen Sie sich vor, Sie hätten eine Methode getItemCount()in einer Klasse aufgerufen , was bedeutet , die Gesamtzahl der in diesem Objekt enthaltenen Elemente abzurufen , und diese Methode wurde an einem Dutzend Stellen in Ihrem System verwendet. Stellen Sie sich dann zu einem späteren Zeitpunkt vor, Sie ändern getItemCount(), um die maximale Anzahl von Elementen zu erhalten, die dieses Objekt jemals enthalten hat. Ihre Software wird höchstwahrscheinlich an den meisten Stellen, an denen diese Methode verwendet wurde, fehlerhaft sein, da die Methode plötzlich andere Informationen meldet. Im Wesentlichen haben Sie den Vertrag gebrochen; Sie haben also Recht, dass Ihr Programm jetzt Fehler enthält.

There's no way, short of disallowing changes altogether, to completely automate the detection of this sort of change, because it happens at the level of what a program means, not simply at the level of how that meaning is expressed. (If you do think of a way to do this easily and generally, you're going to be richer than Bill.) So, in the absence of a complete, general, and automated solution to this problem, what can we do to avoid getting into hot water when we change our classes (which, of course, we must)?

The easiest answer to this question is to say that if a class changes at all, it shouldn't be "trusted" to maintain the contract. After all, a programmer might have done anything to the class, and who knows if the class still works as advertised? This solves the problem of versioning, but it's an impractical solution because it's way too restrictive. If the class is modified to improve performance, say, there's no reason to disallow using the new version of the class simply because it doesn't match the old one. Any number of changes may be made to a class without breaking the contract.

On the other hand, some changes to classes practically guarantee that the contract is broken: deleting a field, for example. If you delete a field from a class, you'll still be able to read streams written by previous versions, because the reader can always ignore the value for that field. But think about what happens when you write a stream intended to be read by previous versions of the class. The value for that field will be absent from the stream, and the older version will assign a (possibly logically inconsistent) default value to that field when it reads the stream. Voilà!: You've got a broken class.

Compatible and incompatible changes

The trick to managing object version compatibility is to identify which kinds of changes may cause incompatibilities between versions and which won't, and to treat these cases differently. In Java parlance, changes that don't cause compatibility problems are called compatible changes; those that may are called incompatible changes.

The designers of the serialization mechanism for Java had the following goals in mind when they created the system:

  1. To define a way in which a newer version of a class can read and write streams that a previous version of the class can also "understand" and use correctly

  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.ObjectInputStreamschreibt keine Klassenobjekte, wenn ein Strom von Bytes erstellt wird, die ein Objekt darstellen. Stattdessen wird a geschrieben java.io.ObjectStreamClass, eine Beschreibung der Klasse. Der Klassenlader der Ziel-JVM verwendet diese Beschreibung, um die Bytecodes für die Klasse zu finden und zu laden. Außerdem wird eine 64-Bit-Ganzzahl namens SerialVersionUID erstellt und eingeschlossen. Hierbei handelt es sich um eine Art Schlüssel, der eine Klassendateiversion eindeutig identifiziert.

Das SerialVersionUIDwird erstellt, indem ein sicherer 64-Bit-Hash der folgenden Informationen über die Klasse berechnet wird. Der Serialisierungsmechanismus möchte in der Lage sein, Änderungen in einem der folgenden Dinge zu erkennen: