Programování

Je to ve smlouvě! Objektové verze pro JavaBeans

Za poslední dva měsíce jsme se dostali do určité hloubky ohledně toho, jak serializovat objekty v Javě. (Viz „Specifikace serializace a JavaBeans“ a „Udělejte to způsobem„ Nescafé “- s lyofilizovanými JavaBeans.“) Článek v tomto měsíci předpokládá, že jste si tyto články již přečetli, nebo rozumíte tématům, kterým se věnují. Měli byste pochopit, co je to serializace, jak používat Serializovatelné rozhraní a jak používat java.io.ObjectOutputStream a java.io.ObjectInputStream třídy.

Proč potřebujete verzování

To, co počítač dělá, je určováno jeho softwarem a jeho software se velmi snadno mění. Tato flexibilita, obvykle považovaná za aktivum, má své závazky. Někdy se zdá, že software ano také snadno se mění. Nepochybně jste narazili alespoň na jednu z následujících situací:

  • Soubor dokumentu, který jste obdrželi e-mailem, se ve vašem textovém editoru nebude správně číst, protože vaše je starší verze s nekompatibilním formátem souboru

  • Webová stránka funguje odlišně v různých prohlížečích, protože různé verze prohlížeče podporují různé sady funkcí

  • Aplikace se nespustí, protože máte nesprávnou verzi konkrétní knihovny

  • Váš C ++ nebude kompilován, protože záhlaví a zdrojové soubory jsou nekompatibilní verze

Všechny tyto situace jsou způsobeny nekompatibilními verzemi softwaru nebo daty, se kterými software manipuluje. Stejně jako budovy, osobní filozofie a koryta řek se programy neustále mění v reakci na měnící se podmínky kolem nich. (Pokud si nemyslíte, že se budovy mění, přečtěte si vynikající knihu Stewarta Branda Jak se budovy učí, diskuse o tom, jak se struktury v průběhu času transformují. Další informace najdete v části Zdroje.) Bez struktury pro řízení a správu této změny se jakýkoli softwarový systém jakékoli užitečné velikosti nakonec zvrhne v chaos. Cíl v softwaru správa verzí je zajistit, aby verze softwaru, který aktuálně používáte, poskytovala správné výsledky, když narazí na data vytvořená jinými jeho verzemi.

Tento měsíc budeme diskutovat o tom, jak funguje správa verzí třídy Java, abychom mohli zajistit správu verzí našich JavaBeans. Struktura správy verzí pro třídy Java umožňuje určit mechanismu serializace, zda je konkrétní datový proud (tj. Serializovaný objekt) čitelný konkrétní verzí třídy Java. Budeme hovořit o „kompatibilních“ a „nekompatibilních“ změnách tříd a o tom, proč tyto změny ovlivňují správu verzí. Projdeme si cíle struktury verzí a jak java.io balíček tyto cíle splňuje. Naučíme se vkládat do našeho kódu ochranná opatření, abychom zajistili, že při čtení proudů objektů různých verzí budou data po načtení objektu vždy konzistentní.

Averze k verzi

V softwaru existují různé druhy problémů s verzováním, které se všechny týkají kompatibility mezi bloky dat a / nebo spustitelným kódem:

  • Rozdílné verze stejného softwaru mohou nebo nemusí být schopny zpracovat formáty pro ukládání dat navzájem

  • Programy, které načtou spustitelný kód za běhu, musí být schopny identifikovat správnou verzi softwarového objektu, načtitelné knihovny nebo souboru objektu, který tuto práci provede

  • Metody a pole třídy musí udržovat stejný význam, jak se třída vyvíjí, nebo se mohou existující programy rozbít na místech, kde se tyto metody a pole používají

  • Zdrojový kód, hlavičkové soubory, dokumentace a skripty sestavení musí být všechny koordinovány v prostředí sestavení softwaru, aby bylo zajištěno, že binární soubory jsou vytvářeny ze správných verzí zdrojových souborů

Tento článek o správě verzí objektů Java se zabývá pouze prvními třemi - tedy řízením verzí binárních objektů a jejich sémantikou v běhovém prostředí. (K dispozici je obrovské množství softwaru pro správu verzí zdrojového kódu, ale zde se tím nezabýváme.)

Je důležité si uvědomit, že serializované proudy objektů Java neobsahují bytové kódy. Obsahují pouze informace nezbytné k rekonstrukci objektu za předpokladu k vytvoření objektu máte k dispozici soubory třídy. Co se ale stane, pokud jsou soubory třídy dvou virtuálních strojů Java (JVM) (zapisovatel a čtenář) různých verzí? Jak zjistíme, zda jsou kompatibilní?

Definici třídy lze považovat za „smlouvu“ mezi třídou a kódem, který třídu volá. Tato smlouva zahrnuje třídu API (aplikační programovací rozhraní). Změna API je ekvivalentní změně smlouvy. (Další změny ve třídě mohou také znamenat změny ve smlouvě, jak uvidíme.) Jak se třída vyvíjí, je důležité zachovat chování předchozích verzí třídy, aby nedošlo k porušení softwaru na místech závislých na dané chování.

Příklad změny verze

Představte si, že jste volali metodu getItemCount () ve třídě, což znamenalo získejte celkový počet položek, které tento objekt obsahuje, a tato metoda byla použita na tuctu míst v celém vašem systému. Pak si představte, že se později změníte getItemCount () znamenat získejte maximální počet položek, které tento objekt má kdy obsažené. Váš software se s největší pravděpodobností rozbije na většině míst, kde byla tato metoda použita, protože tato metoda najednou bude hlásit různé informace. V podstatě jste porušili smlouvu; takže vám dobře slouží, že váš program má nyní chyby.

Neexistuje způsob, jak úplně zakázat změny, zcela automatizovat detekci tohoto druhu změn, protože se to děje na úrovni toho, co program prostředek, nejen na úrovni toho, jak je tento význam vyjádřen. (Pokud si vymyslíte způsob, jak to udělat snadno a obecně, budete bohatší než Bill.) Takže, při absenci úplného, ​​obecného a automatizovaného řešení tohoto problému, co umět děláme proto, abychom se při změně vyučování nedostali do horké vody (což samozřejmě musíme)?

Nejjednodušší odpovědí na tuto otázku je říci, že pokud se třída změní vůbec, nemělo by být „důvěryhodné“ udržovat smlouvu. Koneckonců, programátor mohl třídě něco udělat a kdo ví, jestli třída stále funguje tak, jak je inzerováno? To řeší problém správy verzí, ale je to nepraktické řešení, protože je příliš restriktivní. Pokud je třída upravena tak, aby zlepšila výkon, řekněme, není důvod nepovolovat používání nové verze třídy jednoduše proto, že neodpovídá té staré. Ve třídě lze provést libovolný počet změn, aniž by došlo k porušení smlouvy.

Na druhou stranu některé změny tříd prakticky zaručují porušení smlouvy: například odstranění pole. Pokud odstraníte pole z třídy, budete stále moci číst streamy napsané předchozími verzemi, protože čtečka může vždy ignorovat hodnotu pro toto pole. Ale přemýšlejte o tom, co se stane, když napíšete stream určený ke čtení předchozími verzemi třídy. Hodnota pro toto pole bude v proudu chybět a starší verze přiřadí tomuto poli při čtení proudu (pravděpodobně logicky nekonzistentní) výchozí hodnotu. Voilà!: Máte rozbitou třídu.

Kompatibilní a nekompatibilní změny

Trik ke správě kompatibility verzí objektů spočívá v identifikaci, které druhy změn mohou způsobit nekompatibilitu mezi verzemi a které nikoli, a v těchto případech zacházet odlišně. V jazyce Java se nazývají změny, které nezpůsobují problémy s kompatibilitou kompatibilní Změny; ty, které mohou být volány nekompatibilní Změny.

Návrháři mechanismu serializace pro Javu měli při vytváření systému na mysli následující cíle:

  1. Definovat způsob, jakým může novější verze třídy číst a zapisovat streamy, kterým předchozí verze třídy také může „rozumět“ a správně je používat

  2. Poskytnout výchozí mechanismus, který serializuje objekty s dobrým výkonem a přiměřenou velikostí. To je mechanismus serializace jsme již diskutovali ve dvou předchozích sloupcích JavaBeans zmíněných na začátku tohoto článku

  3. Chcete-li minimalizovat práci související s verzí u tříd, které verzování nepotřebují. V ideálním případě je nutné informace o verzích přidat do třídy, až se přidají nové verze

  4. Chcete-li naformátovat proud objektu tak, aby bylo možné objekty přeskočit bez načtení souboru třídy objektu. Tato funkce umožňuje klientskému objektu procházet proud objektu obsahující objekty, kterým nerozumí

Podívejme se, jak mechanismus serializace řeší tyto cíle ve světle výše uvedené situace.

Odsouhlasitelné rozdíly

Některé změny provedené v souboru třídy mohou záviset na tom, aby se nezměnila smlouva mezi třídou a jakýmkoli jiným názvem třídy. Jak je uvedeno výše, v dokumentaci Java se tyto změny nazývají kompatibilní. V souboru třídy lze provést libovolný počet kompatibilních změn bez změny smlouvy. Jinými slovy, dvě verze třídy, které se liší pouze kompatibilními změnami, jsou kompatibilní třídy: Novější verze bude i nadále číst a zapisovat datové proudy objektů, které jsou kompatibilní s předchozími verzemi.

Třídy java.io.ObjectInputStream a java.io.ObjectOutputStream nevěřím ti. Ve výchozím nastavení jsou extrémně podezřelé z jakýchkoli změn rozhraní souboru třídy do světa - to znamená vše, co je viditelné pro jakoukoli jinou třídu, která může třídu používat: podpisy veřejných metod a rozhraní a typy a modifikátory veřejných polí. Ve skutečnosti jsou tak paranoidní, že sotva můžete něco změnit ve třídě, aniž byste to způsobili java.io.ObjectInputStream odmítnout načíst stream napsaný předchozí verzí vaší třídy.

Podívejme se na příklad. nekompatibility třídy a poté vyřešit výsledný problém. Řekněme, že máte objekt, který se jmenuje InventoryItem, která udržuje čísla dílů a množství daného dílu k dispozici ve skladu. Jednoduchá forma tohoto objektu jako JavaBean může vypadat asi takto:

001 002 import java.beans. *; 003 import java.io. *; 004 import Tisknutelné; 005 006 // 007 // Verze 1: jednoduše skladujte množství po ruce a číslo dílu 008 // 009 010 veřejná třída InventoryItem implementuje Serializable, Printable {011 012 013 014 015 016 // pole 017 protected int iQuantityOnHand_; 018 chráněný řetězec 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} 031032 public int getQuantityOnHand () 033 {034 vrátit 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 // ... implementuje tisknutelné 053 public void print () 054 {055 System.out.println ("Část:" + getPartNo () + "\ nKvalita po ruce:" + 056 getQuantityOnHand () + "\ n \ n "); 057} 058}; 059 

(Máme také jednoduchý hlavní program, tzv Demo8a, který čte a píše InventoryItems do a ze souboru pomocí proudů objektů a rozhraní Tisknutelné, který InventoryItem nářadí a Demo8a používá k tisku objektů. Zdroj těchto informací najdete zde.) Spuštění ukázkového programu přináší rozumné, i když ne vzrušující výsledky:

C: \ fazole> java Demo8a se souborem SA0091-001 33 Napsaný objekt: Část: SA0091-001 Množství po ruce: 33 C: \ fazole> java Demo8a r soubor Číst objekt: Část: SA0091-001 Množství po ruce: 33 

Program správně serializuje a deserializuje objekt. Nyní provedeme malou změnu v souboru třídy. Uživatelé systému provedli inventuru a zjistili nesrovnalosti mezi databází a skutečným počtem položek. Požádali o možnost sledovat počet ztracených položek ze skladu. Pojďme přidat jedno veřejné pole InventoryItem , který udává počet položek, které ve skladu chybí. Vložíme následující řádek do InventoryItem třída a překompilovat:

016 // pole 017 protected int iQuantityOnHand_; 018 chráněný řetězec sPartNo_; 019 public int iQuantityLost_; 

Soubor se dobře sestavuje, ale podívejte se, co se stane, když se pokusíme přečíst stream z předchozí verze:

C: \ mj-java \ Column8> java Demo8a r soubor IO Výjimka: InventoryItem; Místní třída není kompatibilní java.io.InvalidClassException: InventoryItem; Místní třída není kompatibilní na java.io.ObjectStreamClass.setClass (ObjectStreamClass.java:219) na java.io.ObjectInputStream.inputClassDescriptor (ObjectInputStream.java:639) na java.io.ObjectInputStream.readObject (ObjectInputStream.java) java.io.ObjectInputStream.inputObject (ObjectInputStream.java:820) na java.io.ObjectInputStream.readObject (ObjectInputStream.java:284) na Demo8a.main (Demo8a.java:56) 

Whoa, vole! Co se stalo?

java.io.ObjectInputStream nepíše objekty třídy, když vytváří proud bajtů představujících objekt. Místo toho píše a java.io.ObjectStreamClass, což je popis třídy. Načítač třídy cílového JVM používá tento popis k vyhledání a načtení bajtových kódů pro třídu. Také vytváří a zahrnuje 64bitové celé číslo zvané a SerialVersionUID, což je druh klíče, který jednoznačně identifikuje verzi souboru třídy.

The SerialVersionUID je vytvořen výpočtem 64bitové bezpečné hodnoty hash následujících informací o třídě. Mechanismus serializace chce být schopen detekovat změnu v kterékoli z následujících věcí: