Programmering

Det står i kontrakten! Objektversjoner for JavaBeans

I løpet av de siste to månedene har vi gått litt i dybden med hensyn til hvordan vi kan serieisere objekter i Java. (Se "Serialization and the JavaBeans Specification" og "Do it the Nescafé 'way - with frystørket JavaBeans.") Denne månedens artikkel antar at du enten allerede har lest disse artiklene eller at du forstår emnene de dekker. Du bør forstå hva serialisering er, hvordan du bruker Serialiserbar grensesnitt, og hvordan du bruker java.io.ObjectOutputStream og java.io.ObjectInputStream klasser.

Hvorfor du trenger versjonering

Hva en datamaskin gjør, bestemmes av programvaren, og programvaren er ekstremt enkel å endre. Denne fleksibiliteten, vanligvis ansett som en eiendel, har sine forpliktelser. Noen ganger ser det ut til at programvare er det også lett å endre. Du har utvilsomt kommet inn i minst en av følgende situasjoner:

  • En dokumentfil du mottok via e-post vil ikke lese riktig i tekstbehandleren, fordi din er en eldre versjon med et inkompatibelt filformat

  • En webside fungerer annerledes i forskjellige nettlesere, fordi forskjellige nettleserversjoner støtter forskjellige funksjonssett

  • Et program kan ikke kjøres fordi du har feil versjon av et bestemt bibliotek

  • C ++ din kompileres ikke fordi overskrift og kildefiler er av inkompatible versjoner

Alle disse situasjonene er forårsaket av inkompatible versjoner av programvare og / eller data programvaren manipulerer. I likhet med bygninger, personlige filosofier og elvesenger, endres programmene kontinuerlig som svar på de skiftende forholdene rundt dem. (Hvis du ikke tror bygninger endrer seg, kan du lese Stewart Brands fremragende bok Hvordan bygninger lærer, en diskusjon av hvordan strukturer transformerer seg over tid. Se Ressurser for mer informasjon.) Uten en struktur for å kontrollere og håndtere denne endringen, utarter til enhver tid programvaresystem av hvilken som helst nyttig størrelse til kaos. Målet i programvare versjonering er å sikre at versjonen av programvaren du bruker for øyeblikket, gir riktige resultater når den møter data produsert av andre versjoner av seg selv.

Denne måneden skal vi diskutere hvordan versjonering av Java-klasser fungerer, slik at vi kan gi versjonskontroll av JavaBeans. Versjonsstrukturen for Java-klasser tillater deg å indikere for serialiseringsmekanismen om en bestemt datastrøm (det vil si et serieobjekt) kan leses av en bestemt versjon av en Java-klasse. Vi snakker om "kompatible" og "inkompatible" endringer i klasser, og hvorfor disse endringene påvirker versjonering. Vi vil gå over målene med versjonsstrukturen, og hvordan java.io pakken oppfyller disse målene. Og vi lærer å sette beskyttelsesforanstaltninger i koden vår for å sikre at når vi leser objektstrømmer av forskjellige versjoner, er dataene alltid konsistente etter at objektet er lest.

Versjonsaversjon

Det er forskjellige typer versjonsproblemer i programvare, som alle gjelder kompatibilitet mellom biter av data og / eller kjørbar kode:

  • Ulike versjoner av den samme programvaren kan eller kan ikke håndtere hverandres datalagringsformater

  • Programmer som laster kjørbar kode på kjøretid, må kunne identifisere riktig versjon av programvareobjektet, det lastbare biblioteket eller objektfilen for å gjøre jobben

  • En klasses metoder og felt må ha samme betydning som klassen utvikler seg, ellers kan eksisterende programmer brytes på steder der disse metodene og feltene brukes

  • Kildekode, headerfiler, dokumentasjon og build-skript må alle koordineres i et programvaremiljø for å sikre at binære filer bygges fra de riktige versjonene av kildefilene.

Denne artikkelen om versjonering av Java-objekter adresserer bare de tre første - det vil si versjonskontroll av binære objekter og deres semantikk i et kjøretidsmiljø. (Det er et stort utvalg av programvare tilgjengelig for versjonering av kildekode, men vi dekker ikke det her.)

Det er viktig å huske at serielle Java-objektstrømmer ikke inneholder bytekoder. De inneholder bare informasjonen som er nødvendig for å rekonstruere et objekt antar du har klassefilene tilgjengelig for å bygge objektet. Men hva skjer hvis klassefilene til de to virtuelle Java-maskinene (JVM) (skribenten og leseren) har forskjellige versjoner? Hvordan vet vi om de er kompatible?

En klassedefinisjon kan betraktes som en "kontrakt" mellom klassen og koden som kaller klassen. Denne kontrakten inkluderer klassen API (Applikasjonsprogrammeringsgrensesnitt). Endring av API tilsvarer endring av kontrakten. (Andre endringer i en klasse kan også innebære endringer i kontrakten, som vi ser.) Når en klasse utvikler seg, er det viktig å opprettholde oppførselen til tidligere versjoner av klassen for ikke å bryte programvaren på steder som var avhengig av gitt oppførsel.

Et versjonsendringseksempel

Tenk deg at du hadde en metode som heter getItemCount () i en klasse, noe som betydde få det totale antallet gjenstander dette objektet inneholder, og denne metoden ble brukt et dusin steder i hele systemet ditt. Tenk deg senere at du endrer deg getItemCount () å mene få maksimalt antall gjenstander dette objektet har noensinne inneholdt. Programvaren din vil mest sannsynlig bryte de fleste steder der denne metoden ble brukt, fordi metoden plutselig vil rapportere annen informasjon. I hovedsak har du brutt kontrakten; så det tjener deg riktig at programmet ditt nå har feil i seg.

Det er ingen måte, kort å ikke tillate endringer helt, å fullstendig automatisere oppdagelsen av denne typen endringer, fordi det skjer på nivået med hva et program midler, ikke bare på nivået med hvordan den betydningen kommer til uttrykk. (Hvis du tenker på en måte å gjøre dette enkelt og generelt, vil du bli rikere enn Bill.) Så i fravær av en komplett, generell og automatisert løsning på dette problemet, hva kan gjør vi for å unngå å komme i varmt vann når vi bytter klasse (som vi selvfølgelig må)?

Det enkleste svaret på dette spørsmålet er å si at hvis en klasse endres i det hele tatt, det skal ikke være "klarert" for å opprettholde kontrakten. Tross alt kan en programmerer ha gjort noe med klassen, og hvem vet om klassen fremdeles fungerer som annonsert? Dette løser problemet med versjonering, men det er en upraktisk løsning fordi den er altfor restriktiv. Hvis klassen er modifisert for å forbedre ytelsen, si det er ingen grunn til å ikke tillate å bruke den nye versjonen av klassen bare fordi den ikke samsvarer med den gamle. Ethvert antall endringer kan gjøres i en klasse uten å bryte kontrakten.

På den annen side garanterer noen endringer i klassene praktisk talt at kontrakten blir brutt: for eksempel å slette et felt. Hvis du sletter et felt fra en klasse, vil du fremdeles kunne lese strømmer skrevet av tidligere versjoner, fordi leseren alltid kan ignorere verdien for det feltet. Men tenk på hva som skjer når du skriver en strøm som er ment å bli lest av tidligere versjoner av klassen. Verdien for det feltet vil være fraværende i strømmen, og den eldre versjonen vil tildele en (muligens logisk inkonsekvent) standardverdi til det feltet når den leser strømmen. Voilà!: Du har en ødelagt klasse.

Kompatible og inkompatible endringer

Trikset med å administrere objektversjonskompatibilitet er å identifisere hvilke typer endringer som kan forårsake inkompatibilitet mellom versjoner og hvilke som ikke vil, og å behandle disse sakene annerledes. På Java-språk kalles endringer som ikke forårsaker kompatibilitetsproblemer kompatibel Endringer; de som måtte kalles uforenlig Endringer.

Designerne av serialiseringsmekanismen for Java hadde følgende mål i tankene da de opprettet systemet:

  1. Å definere en måte som en nyere versjon av en klasse kan lese og skrive strømmer som en tidligere versjon av klassen også kan "forstå" og bruke riktig

  2. Å tilby en standardmekanisme som serierer objekter med god ytelse og rimelig størrelse. Dette er serialiseringsmekanisme Vi har allerede diskutert i de to forrige JavaBeans-kolonnene nevnt i begynnelsen av denne artikkelen

  3. For å minimere versjonsrelatert arbeid på klasser som ikke trenger versjonering. Ideelt sett trenger versjonsinformasjon bare å legges til i en klasse når nye versjoner legges til

  4. For å formatere objektstrømmen slik at objekter kan hoppes over uten å laste inn objektets klassefil. Denne muligheten lar et klientobjekt krysse en objektstrøm som inneholder objekter den ikke forstår

La oss se hvordan serialiseringsmekanismen adresserer disse målene i lys av situasjonen beskrevet ovenfor.

Forlikelige forskjeller

Noen endringer som er gjort i en klassefil, kan være avhengig av at kontrakten mellom klassen og hva andre klasser ikke kaller, endres. Som nevnt ovenfor kalles disse kompatible endringer i Java-dokumentasjonen. Et hvilket som helst antall kompatible endringer kan gjøres i en klassefil uten å endre kontrakten. Med andre ord, to versjoner av en klasse som bare skiller seg ved kompatible endringer, er kompatible klasser: Den nyere versjonen vil fortsette å lese og skrive objektstrømmer som er kompatible med tidligere versjoner.

Klassene java.io.ObjectInputStream og java.io.ObjectOutputStream ikke stol på deg. De er designet for å være som standard ekstremt mistenkelige for endringer i grensesnittet til en klassefil til verden - noe som betyr noe synlig for andre klasser som kan bruke klassen: signaturene til offentlige metoder og grensesnitt og typene og modifikatorene av offentlige felt. De er faktisk så paranoide at du knapt kan endre noe på en klasse uten å forårsake java.io.ObjectInputStream å nekte å laste inn en strøm skrevet av en tidligere versjon av klassen din.

La oss se på et eksempel. av en klasse inkompatibilitet, og deretter løse det resulterende problemet. Si at du har et objekt som heter Varelager, som opprettholder delenumre og mengden av den aktuelle delen som er tilgjengelig på et lager. En enkel form for objektet som en JavaBean kan se ut slik:

001002 importere java.bønner. *; 003 importere java.io. *; 004 import Utskriftsvennlig; 005 006 // 007 // Versjon 1: lagre ganske enkelt antall på hånden og delenummer 008 // 009 010 offentlig klasse InventoryItem implementerer Serializable, Printable {011 012 013 014 015 016 // felt 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 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 offentlig streng getPartNo () 043 {044 retur sPartNo_; 045} 046047 offentlig ugyldig setPartNo (String _sPartNo) 048 {049 sPartNo_ = _sPartNo; 050} 051052 // ... implementerer utskrivbar 053 public void print () 054 {055 System.out.println ("Part:" + getPartNo () + "\ nMengde på hånden:" + 056 getQuantityOnHand () + "\ n \ n "); 057} 058}; 059 

(Vi har også et enkelt hovedprogram, kalt Demo8a, som leser og skriver Varelager til og fra en fil ved hjelp av objektstrømmer og grensesnitt Utskriftsvennlig, hvilken Varelager redskaper og Demo8a bruker til å skrive ut objektene. Du kan finne kilden til disse her.) Å kjøre demo-programmet gir rimelige, om ikke spennende, resultater:

C: \ bønner> java Demo8a w-fil SA0091-001 33 Skrev objekt: Del: SA0091-001 Antall på hånden: 33 C: \ bønner> java Demo8a r-fil Les objekt: Del: SA0091-001 Antall på hånden: 33 

Programmet serialiserer og deserialiserer objektet riktig. La oss nå gjøre en liten endring i klassefilen. Systembrukerne har gjort en inventar og har funnet avvik mellom databasen og den faktiske varetellingen. De har bedt om muligheten til å spore antall varer som er tapt fra lageret. La oss legge til et enkelt offentlig felt i Varelager som indikerer antall gjenstander som mangler fra lageret. Vi setter inn følgende linje i Varelager klasse og kompilere på nytt:

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

Filen kompilerer bra, men se på hva som skjer når vi prøver å lese strømmen fra forrige versjon:

C: \ mj-java \ Kolonne8> java Demo8a r fil IO Unntak: InventoryItem; Lokal klasse er 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) java.io.ObjectInputStream.inputObject (ObjectInputStream.java:820) på java.io.ObjectInputStream.readObject (ObjectInputStream.java:284) på ​​Demo8a.main (Demo8a.java:56) 

Vel, fyr! Hva skjedde?

java.io.ObjectInputStream skriver ikke klasseobjekter når den oppretter en strøm av byte som representerer et objekt. I stedet skriver det a java.io.ObjectStreamClass, hvilken er en beskrivelse av klassen. Destinasjonen JVMs klasselaster bruker denne beskrivelsen for å finne og laste bytekodene for klassen. Det oppretter og inkluderer også et 64-biters heltall kalt a SerialVersionUID, som er en slags nøkkel som unikt identifiserer en klassefilversjon.

De SerialVersionUID er opprettet ved å beregne en 64-biters sikker hash av følgende informasjon om klassen. Serialiseringsmekanismen vil være i stand til å oppdage endring i noen av følgende ting:

$config[zx-auto] not found$config[zx-overlay] not found