Programmering

Det er i kontrakten! Objektversioner til JavaBeans

I løbet af de sidste to måneder er vi gået i dybden med hensyn til, hvordan man serierer objekter i Java. (Se "Serialization and the JavaBeans Specification" og "Do it the` Nescafé 'way - with frysetørrede JavaBeans. ") Denne måneds artikel antager, at du enten allerede har læst disse artikler, eller at du forstår de emner, de dækker. Du skal forstå, hvad serialisering er, hvordan man bruger Serialiserbar interface, og hvordan man bruger java.io.ObjectOutputStream og java.io.ObjectInputStream klasser.

Hvorfor har du brug for versionering

Hvad en computer gør bestemmes af dens software, og software er ekstremt let at ændre. Denne fleksibilitet, der normalt betragtes som et aktiv, har sine forpligtelser. Nogle gange ser det ud til, at software er det også let at ændre. Du er utvivlsomt stødt på mindst en af ​​følgende situationer:

  • En dokumentfil, du har modtaget via e-mail, læses ikke korrekt i din tekstbehandler, fordi din er en ældre version med et inkompatibelt filformat

  • En webside fungerer forskelligt i forskellige browsere, fordi forskellige browserversioner understøtter forskellige funktionssæt

  • En applikation kører ikke, fordi du har den forkerte version af et bestemt bibliotek

  • Din C ++ kompileres ikke, fordi header og kildefiler er af inkompatible versioner

Alle disse situationer er forårsaget af inkompatible versioner af software og / eller de data, softwaren manipulerer. Ligesom bygninger, personlige filosofier og flodlejer ændres programmer konstant som reaktion på de skiftende forhold omkring dem. (Hvis du ikke tror, ​​bygninger ændrer sig, skal du læse Stewart Brands fremragende bog Hvordan bygninger lærer, en diskussion af hvordan strukturer transformeres over tid. Se ressourcer for at få flere oplysninger.) Uden en struktur, der styrer og styrer denne ændring, degraderer ethvert softwaresystem af enhver anvendelig størrelse til sidst til kaos. Målet i software versionering er at sikre, at den version af software, du bruger i øjeblikket, giver korrekte resultater, når den støder på data, der er produceret af andre versioner af sig selv.

Denne måned vil vi diskutere, hvordan Java-klasseversionering fungerer, så vi kan levere versionskontrol af vores JavaBeans. Versionstrukturen for Java-klasser giver dig mulighed for at angive til serialiseringsmekanismen, om en bestemt datastrøm (det vil sige et seriel objekt) kan læses af en bestemt version af en Java-klasse. Vi taler om "kompatible" og "inkompatible" ændringer til klasser, og hvorfor disse ændringer påvirker versionering. Vi gennemgår målene med versioneringsstrukturen, og hvordan java.io pakke opfylder disse mål. Og vi lærer at placere beskyttelsesforanstaltninger i vores kode for at sikre, at når vi læser objektstrømme i forskellige versioner, er dataene altid konsistente, når objektet er læst.

Versionaversion

Der er forskellige former for versioneringsproblemer i software, som alle vedrører kompatibilitet mellem klumper af data og / eller eksekverbar kode:

  • Forskellige versioner af den samme software kan muligvis håndtere hinandens datalagringsformater

  • Programmer, der indlæser eksekverbar kode under kørsel, skal kunne identificere den korrekte version af softwareobjektet, det indlæselige bibliotek eller objektfilen for at udføre jobbet

  • En klasses metoder og felter skal opretholde den samme betydning som klassen udvikler sig, ellers kan eksisterende programmer gå i stykker steder, hvor disse metoder og felter bruges

  • Kildekode, headerfiler, dokumentation og build-scripts skal alle koordineres i et softwarebygningsmiljø for at sikre, at binære filer bygges ud fra de korrekte versioner af kildefilerne

Denne artikel om version af Java-objekt adresserer kun de første tre - det vil sige versionskontrol af binære objekter og deres semantik i et runtime-miljø. (Der er et stort udvalg af software tilgængelig til versionering af kildekode, men vi dækker ikke det her.)

Det er vigtigt at huske, at serielle Java-objektstrømme ikke indeholder bytekoder. De indeholder kun de oplysninger, der er nødvendige for at rekonstruere et objekt antager du har klassefiler til rådighed til at opbygge objektet. Men hvad sker der, hvis klassefilerne på de to virtuelle Java-maskiner (JVM'er) (forfatteren og læseren) har forskellige versioner? Hvordan ved vi, om de er kompatible?

En klassedefinition kan betragtes som en "kontrakt" mellem klassen og den kode, der kalder klassen. Denne kontrakt inkluderer klassens API (applikationsprogrammeringsgrænseflade). Ændring af API svarer til ændring af kontrakten. (Andre ændringer i en klasse kan også antyde ændringer i kontrakten, som vi vil se.) Efterhånden som en klasse udvikler sig, er det vigtigt at opretholde opførelsen af ​​tidligere versioner af klassen for ikke at bryde softwaren på steder, der var afhængige af givet adfærd.

Et eksempel på versionskift

Forestil dig, at du havde en metode kaldet getItemCount () i en klasse, hvilket betød få det samlede antal varer, som dette objekt indeholder, og denne metode blev brugt et dusin steder i hele dit system. Forestil dig så på et senere tidspunkt, at du ændrer dig getItemCount () at mene få det maksimale antal varer, som dette objekt har nogensinde indeholdt. Din software vil sandsynligvis gå i stykker de fleste steder, hvor denne metode blev brugt, fordi metoden pludselig rapporterer forskellige oplysninger. I det væsentlige har du brudt kontrakten; så det tjener dig rigtigt, at dit program nu har fejl i det.

Der er ingen måde, uden at tillade ændringer helt, at automatisere detekteringen af ​​denne form for ændring fuldstændigt, fordi det sker på niveauet med, hvad et program midler, ikke blot på det niveau, hvor denne betydning udtrykkes. (Hvis du tænker på en måde at gøre dette let og generelt, bliver du rigere end Bill.) Så i mangel af en komplet, generel og automatiseret løsning på dette problem, hvad kan gør vi for at undgå at komme i varmt vand, når vi skifter klasse (hvilket vi selvfølgelig skal)?

Det nemmeste svar på dette spørgsmål er at sige, at hvis en klasse skifter overhovedet, det skulle ikke være "tillid til" at opretholde kontrakten. Når alt kommer til alt kan en programmør muligvis have gjort noget for klassen, og hvem ved, om klassen stadig fungerer som annonceret? Dette løser problemet med versionering, men det er en upraktisk løsning, fordi den er alt for restriktiv. Hvis klassen er ændret for at forbedre ydeevnen, siger, er der ingen grund til at afvise at bruge den nye version af klassen, simpelthen fordi den ikke matcher den gamle. Ethvert antal ændringer kan foretages i en klasse uden at bryde kontrakten.

På den anden side garanterer nogle ændringer i klasser praktisk talt, at kontrakten er brudt: f.eks. Sletning af et felt. Hvis du sletter et felt fra en klasse, kan du stadig læse streams skrevet af tidligere versioner, fordi læseren altid kan ignorere værdien for dette felt. Men tænk over, hvad der sker, når du skriver en stream, der er beregnet til at blive læst af tidligere versioner af klassen. Værdien for dette felt vil være fraværende i strømmen, og den ældre version tildeler en (muligvis logisk inkonsekvent) standardværdi til dette felt, når den læser strømmen. Voilà!: Du har en brudt klasse.

Kompatible og inkompatible ændringer

Tricket til at styre kompatibilitet med objektversion er at identificere, hvilke typer ændringer der kan forårsage inkompatibilitet mellem versioner, og hvilke der ikke vil, og at behandle disse tilfælde forskelligt. I Java-sprog kaldes ændringer, der ikke forårsager kompatibilitetsproblemer kompatibel ændringer; de der måtte kaldes uforenelig ændringer.

Designerne af serialiseringsmekanismen til Java havde følgende mål i tankerne, da de oprettede systemet:

  1. At definere en måde, hvorpå en nyere version af en klasse kan læse og skrive streams, som en tidligere version af klassen også kan "forstå" og bruge korrekt

  2. At levere en standardmekanisme, der serierer objekter med god ydelse og rimelig størrelse. Dette er serialiseringsmekanisme Vi har allerede diskuteret i de to tidligere JavaBeans-kolonner, der er nævnt i begyndelsen af ​​denne artikel

  3. For at minimere versionsrelateret arbejde på klasser, der ikke har behov for versionering. Ideelt set behøver versionsoplysninger kun føjes til en klasse, når nye versioner tilføjes

  4. At formatere objektstrømmen, så objekter kan springes over uden at indlæse objektets klassefil. Denne mulighed gør det muligt for et klientobjekt at krydse en objektstrøm, der indeholder objekter, den ikke forstår

Lad os se, hvordan serialiseringsmekanismen adresserer disse mål i lyset af situationen beskrevet ovenfor.

Forenelige forskelle

Nogle ændringer foretaget i en klassefil kan afhænge af ikke at ændre kontrakten mellem klassen og hvad andre klasser måtte kalde den. Som nævnt ovenfor kaldes disse kompatible ændringer i Java-dokumentationen. Ethvert antal kompatible ændringer kan foretages i en klassefil uden at ændre kontrakten. Med andre ord er to versioner af en klasse, der kun adskiller sig ved kompatible ændringer, kompatible klasser: Den nyere version fortsætter med at læse og skrive objektstrømme, der er kompatible med tidligere versioner.

Klasserne java.io.ObjectInputStream og java.io.ObjectOutputStream stol ikke på dig. De er designet til at være som standard ekstremt mistænkelige over for ændringer i en klassefils grænseflade til verden - hvilket betyder alt synligt for enhver anden klasse, der måtte bruge klassen: underskrifterne af offentlige metoder og grænseflader og typerne og modifikatorerne af offentlige felter. De er faktisk så paranoide, at du næppe kan ændre noget ved en klasse uden at forårsage java.io.ObjectInputStream at nægte at indlæse en stream skrevet af en tidligere version af din klasse.

Lad os se på et eksempel. af en klasse uforenelighed og derefter løse det resulterende problem. Sig, at du har et objekt kaldet Varebeholdning, der opretholder varenumre og mængden af ​​den pågældende del, der er tilgængelig på et lager. En simpel form for det objekt som en JavaBean kan se sådan ud:

001002 import java.bønner. *; 003 import java.io. *; 004 import Printbar; 005 006 // 007 // Version 1: Gem kun mængde ved hånden og varenummer 008 // 009 010 offentlig klasse InventoryItem implementerer Serializable, Printable {011 012 013 014 015 016 // felter 017 beskyttet int iQuantityOnHand_; 018 beskyttet streng sPartNo_; 019 020 offentlig InventoryItem () 021 {022 iQuantityOnHand_ = -1; 023 sPartNo_ = ""; 024} 025 026 public InventoryItem (String _sPartNo, int _iQuantityOnHand) 027 {028 setQuantityOnHand (_iQuantityOnHand); 029 sætPartNo (_sPartNo); 030} 031 032 public int getQuantityOnHand () 033 {034 return iQuantityOnHand_; 035} 036 037 offentligt ugyldigt setQuantityOnHand (int _iQuantityOnHand) 038 {039 iQuantityOnHand_ = _iQuantityOnHand; 040} 041 042 offentlig String getPartNo () 043 {044 return sPartNo_; 045} 046047 offentligt ugyldigt setPartNo (String _sPartNo) 048 {049 sPartNo_ = _sPartNo; 050} 051052 // ... implementerer udskrivelig 053 offentlig ugyldig udskrivning () 054 {055 System.out.println ("Del:" + getPartNo () + "\ nMængde ved hånden:" + 056 getQuantityOnHand () + "\ n \ n "); 057} 058}; 059 

(Vi har også et simpelt hovedprogram kaldet Demo8a, som læser og skriver Beholdningspunkter til og fra en fil ved hjælp af objektstrømme og interface Printbar, hvilken Varebeholdning redskaber og Demo8a bruger til at udskrive objekterne. Du kan finde kilden til disse her.) At køre demo-programmet giver rimelige, hvis ikke spændende resultater:

C: \ bønner> java Demo8a w fil SA0091-001 33 Skrev objekt: Del: SA0091-001 Mængde ved hånden: 33 C: \ bønner> java Demo8a r fil Læs objekt: Del: SA0091-001 Mængde ved hånden: 33 

Programmet serialiserer og deserialiserer objektet korrekt. Lad os nu foretage en lille ændring af klassefilen. Systembrugerne har foretaget en opgørelse og har fundet uoverensstemmelser mellem databasen og den faktiske varetælling. De har anmodet om muligheden for at spore antallet af tabte varer fra lageret. Lad os tilføje et enkelt offentligt felt til Varebeholdning der angiver antallet af varer, der mangler fra lageret. Vi indsætter følgende linje i Varebeholdning klasse og kompilere igen:

016 // felter 017 beskyttet int iQuantityOnHand_; 018 beskyttet streng sPartNo_; 019 offentlig int iQuantityLost_; 

Filen kompilerer fint, men se hvad der sker, når vi prøver at læse streamen fra den tidligere version:

C: \ mj-java \ Kolonne8> java Demo8a r fil IO Undtagelse: InventoryItem; Lokal klasse ikke kompatibel java.io.InvalidClassException: InventoryItem; Lokal klasse ikke kompatibel på java.io.ObjectStreamClass.setClass (ObjectStreamClass.java:219) på java.io.ObjectInputStream.inputClassDescriptor (ObjectInputStream.java:639) på java.io.ObjectInputStream.readObject (ObjectInputStream.java:276) at java.io.ObjectInputStream.inputObject (ObjectInputStream.java:820) på java.io.ObjectInputStream.readObject (ObjectInputStream.java:284) ved Demo8a.main (Demo8a.java:56) 

Whoa, fyr! Hvad skete der?

java.io.ObjectInputStream skriver ikke klasseobjekter, når den opretter en strøm af bytes, der repræsenterer et objekt. I stedet skriver det en java.io.ObjectStreamClass, som er en beskrivelse af klassen. Destinationen JVM's klasselæsser bruger denne beskrivelse til at finde og indlæse bytekoder for klassen. Det opretter og inkluderer også et 64-bit heltal kaldet a SerialVersionUID, som er en slags nøgle, der entydigt identificerer en klassefilversion.

Det SerialVersionUID oprettes ved at beregne en 64-bit sikker hash af følgende oplysninger om klassen. Serialiseringsmekanismen vil være i stand til at opdage ændringer i en af ​​følgende ting: