Serializacja obiektów Externalizable - obejście problemu zmiany nazwy klasy

Serializacja obiektów Externalizable - obejście problemu zmiany nazwy klasy
VG
  • Rejestracja:ponad 15 lat
  • Ostatnio:prawie 8 lat
  • Postów:79
0

Badam aktualnie temat porządnego implementowania serializacji obiektów, które następnie powinny być utrzymywane przez cały czas życia rozwijanej aplikacji, co oznacza skrupulatne wersjonowanie i wsparcie dla deserializacji wcześniejszych wersji.

Implementowanie interfejsu Externalizable wyglądało obiecująco, gdyż daje większą kontrolę nad tym, jak serializowany jest obiekt. Biorąc pod uwagę, że w metodzie writeExternal(ObjectOutput out) można kontrolować sposób serializacji całego obiektu (łącznie z hierarchią dziedziczenia) aż do pojedynczych bajtów, liczyłem na to, że nie będzie problemu nawet ze wsparciem dla obiektów, które w międzyczasie zmieniły nazwę klasy, czy pakiet, w którym się znajdują. W końcu mam komplet niezbędnych danych do ich odtworzenia i skoro mam pełną kontrolę nad procesem, wiem jak odtworzyć obiekt niezależnie od tego, jak kiedyś nazywała się klasa, której ten obiekt jest instancją. Tutaj jednak pojawia się zawód.

Stworzyłem sobie dwie testowe klasy o dokładnie takiej samej zawartości OldDataObject i NewDataObject. Różnią się tylko nazwą, aby zasymulować zmianę nazwy klasy w czasie. Następnie próbuję wykonać taki kod:

Kopiuj
OldDataObject objectToSerialize = new OldDataObject();
objectToSerialize.setNumber(42);
File defaultSaveFile = new File(FILE_PATH_1);
try (ObjectOutputStream oos = new ObjectOutputStream(new BufferedOutputStream(new FileOutputStream(defaultSaveFile)))) {
	oos.writeObject(objectToSerialize);
}
File defaultLoadFile = new File(FILE_PATH_1);
try (ObjectInputStream ois = new ObjectInputStream(new BufferedInputStream(new FileInputStream(defaultLoadFile)))) {
	NewDataObject defaultLoadedObject = (NewDataObject) ois.readObject();
	System.out.println(defaultLoadedObject);
}

Jednak rezultatem jest ClassCastException (lub ClassNotFoundException, gdybym faktycznie wykonywał zmianę nazwy).

Gdy zajrzę do pliku z zapisanym obiektem, staje się jasne, że mimo lepszej kontroli nad serializacją, niż w metodzie writeObject, metadane samej klasy nadal są zapisywane

Kopiuj
00000000  ac ed 00 05 73 72 00 1f  74 65 73 74 2e 44 61 74  |....sr..test.Dat|
00000010  61 54 65 73 74 4d 61 69  6e 24 4f 6c 64 44 61 74  |aTestMain$OldDat|
00000020  61 4f 62 6a 65 63 74 00  00 00 00 00 00 00 01 0c  |aObject.........|
00000030  00 00 78 70 77 04 00 00  00 2a 78                 |..xpw....*x|
0000003b

Ale wystarczy jedna drobna zmiana w kodzie.

Zamiast

Kopiuj
//save
oos.writeObject(objectToSerialize);
//load
NewDataObject defaultLoadedObject = (NewDataObject) ois.readObject();

Stosuję odpowiednio:

Kopiuj
//save
objectToSerialize.writeExternal(oos);
//load
NewDataObject loadedObject = new NewDataObject();
loadedObject.readExternal(ois);

I w tym momencie zmiana nazwy klasy przestaje być problemem.

Hexdump pliku wygląda tak:

Kopiuj
00000000  ac ed 00 05 77 04 00 00  00 2a                    |....w....*|
0000000a

Czyli wylądowało w nim tylko to, co potrzebne i bez problemu odtwarzam obiekt pod nową nazwą klasy.

I teraz pytanie. Czy takie wykorzystanie serializacji jest akceptowalne/bezpieczne, czy gdzieś w przyszłości może mnie to zaboleć? Mam wrażenie, że jest to swego rodzaju hack, wykorzystanie luki w celu obejścia ograniczeń funkcji serializowania. Ale zaletą jest, że rozwiązuje spory z mojego punktu widzenia problem.

Poniżej pełen kod testowy pozwalający odtworzyć mój PoC

Kopiuj
package test;

import java.io.*;

public class DataTestMain {

	private static final String FILE_PATH_1 = "/home/vgt/serialized-1.bin";
	private static final String FILE_PATH_2 = "/home/vgt/serialized-2.bin";

	public static void main(String[] args) throws IOException, ClassNotFoundException {
		OldDataObject objectToSerialize = new OldDataObject();
		objectToSerialize.setNumber(42);

		//Default usage
		try {
			File defaultSaveFile = new File(FILE_PATH_1);
			try (ObjectOutputStream oos = new ObjectOutputStream(new BufferedOutputStream(new FileOutputStream(defaultSaveFile)))) {
				oos.writeObject(objectToSerialize);
			}
			File defaultLoadFile = new File(FILE_PATH_1);
			try (ObjectInputStream ois = new ObjectInputStream(new BufferedInputStream(new FileInputStream(defaultLoadFile)))) {
				NewDataObject defaultLoadedObject = (NewDataObject) ois.readObject();
				System.out.println(defaultLoadedObject);
			}
		} catch (ClassCastException e) {
			System.out.println("Default usage failure");
		}

		//Questionable usage
		File questionableSaveFile = new File(FILE_PATH_2);
		try (ObjectOutputStream oos = new ObjectOutputStream(new BufferedOutputStream(new FileOutputStream(questionableSaveFile)))) {
			objectToSerialize.writeExternal(oos);
		}
		File questionableLoadFile = new File(FILE_PATH_2);
		try (ObjectInputStream ois = new ObjectInputStream(new BufferedInputStream(new FileInputStream(questionableLoadFile)))) {
			NewDataObject questionableLoadedObject = new NewDataObject();
			questionableLoadedObject.readExternal(ois);
			System.out.println(questionableLoadedObject);
		}
	}

	public static class OldDataObject implements Externalizable {

		private static final long serialVersionUID = 1L;

		private int number;

		public int getNumber() {
			return number;
		}

		public void setNumber(int number) {
			this.number = number;
		}

		@Override
		public void writeExternal(ObjectOutput out) throws IOException {
			out.writeInt(number);
		}

		@Override
		public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
			number = in.readInt();
		}

		@Override
		public String toString() {
			return "Object{" +
					"number=" + number +
					'}';
		}

	}

	public static class NewDataObject implements Externalizable {

		private static final long serialVersionUID = 1L;

		private int number;

		public int getNumber() {
			return number;
		}

		public void setNumber(int number) {
			this.number = number;
		}

		@Override
		public void writeExternal(ObjectOutput out) throws IOException {
			out.writeInt(number);
		}

		@Override
		public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
			number = in.readInt();
		}

		@Override
		public String toString() {
			return "Object{" +
					"number=" + number +
					'}';
		}

	}

}

edytowany 3x, ostatnio: VGT
jarekr000000
  • Rejestracja:ponad 8 lat
  • Ostatnio:34 minuty
  • Lokalizacja:U krasnoludów - pod górą
  • Postów:4707
1

A co powiesz na to:

Kopiuj
class  OldDataObject  {
....
  public Object readResolve() throws ObjectStreamException {
            NewDataObject result =  new NewDataObject();
            result.setNumber(this.number);
            return result;
        }
....
}

To externalizable może Ci zrobić problem jak będziesz miał dużą strukturę hierarchiczną obiektów do odczytu i zapisu- możesz się pogubić kiedyś.


jeden i pół terabajta powinno wystarczyć każdemu
edytowany 1x, ostatnio: jarekr000000
VG
  • Rejestracja:ponad 15 lat
  • Ostatnio:prawie 8 lat
  • Postów:79
0

Faktycznie po dodaniu tego do PoC obydwa scenariusze działają prawidłowo. Jedną wadą jest to, że tworzy to konieczność zachowania wszystkich archiwalnych wersji klas, aby móc w nich utrzymywać te metody deserializujące obiekt do najnowszej wersji. Mój przykład zawiera obydwie wersje klasy, ale zrobiłem tak w celu łatwiejszego odtworzenia kodu do mojego pytania. Produkcyjnie bardziej rozważałem scenariusz, gdzie będzie to jedna klasa ewoluująca w czasie. Ale obadam temat, czy takie podejście mi się sprawdzi. Dzięki za pomysł.


Kliknij, aby dodać treść...

Pomoc 1.18.8

Typografia

Edytor obsługuje składnie Markdown, w której pojedynczy akcent *kursywa* oraz _kursywa_ to pochylenie. Z kolei podwójny akcent **pogrubienie** oraz __pogrubienie__ to pogrubienie. Dodanie znaczników ~~strike~~ to przekreślenie.

Możesz dodać formatowanie komendami , , oraz .

Ponieważ dekoracja podkreślenia jest przeznaczona na linki, markdown nie zawiera specjalnej składni dla podkreślenia. Dlatego by dodać podkreślenie, użyj <u>underline</u>.

Komendy formatujące reagują na skróty klawiszowe: Ctrl+B, Ctrl+I, Ctrl+U oraz Ctrl+S.

Linki

By dodać link w edytorze użyj komendy lub użyj składni [title](link). URL umieszczony w linku lub nawet URL umieszczony bezpośrednio w tekście będzie aktywny i klikalny.

Jeżeli chcesz, możesz samodzielnie dodać link: <a href="link">title</a>.

Wewnętrzne odnośniki

Możesz umieścić odnośnik do wewnętrznej podstrony, używając następującej składni: [[Delphi/Kompendium]] lub [[Delphi/Kompendium|kliknij, aby przejść do kompendium]]. Odnośniki mogą prowadzić do Forum 4programmers.net lub np. do Kompendium.

Wspomnienia użytkowników

By wspomnieć użytkownika forum, wpisz w formularzu znak @. Zobaczysz okienko samouzupełniające nazwy użytkowników. Samouzupełnienie dobierze odpowiedni format wspomnienia, zależnie od tego czy w nazwie użytkownika znajduje się spacja.

Znaczniki HTML

Dozwolone jest używanie niektórych znaczników HTML: <a>, <b>, <i>, <kbd>, <del>, <strong>, <dfn>, <pre>, <blockquote>, <hr/>, <sub>, <sup> oraz <img/>.

Skróty klawiszowe

Dodaj kombinację klawiszy komendą notacji klawiszy lub skrótem klawiszowym Alt+K.

Reprezentuj kombinacje klawiszowe używając taga <kbd>. Oddziel od siebie klawisze znakiem plus, np <kbd>Alt+Tab</kbd>.

Indeks górny oraz dolny

Przykład: wpisując H<sub>2</sub>O i m<sup>2</sup> otrzymasz: H2O i m2.

Składnia Tex

By precyzyjnie wyrazić działanie matematyczne, użyj składni Tex.

<tex>arcctg(x) = argtan(\frac{1}{x}) = arcsin(\frac{1}{\sqrt{1+x^2}})</tex>

Kod źródłowy

Krótkie fragmenty kodu

Wszelkie jednolinijkowe instrukcje języka programowania powinny być zawarte pomiędzy obróconymi apostrofami: `kod instrukcji` lub ``console.log(`string`);``.

Kod wielolinijkowy

Dodaj fragment kodu komendą . Fragmenty kodu zajmujące całą lub więcej linijek powinny być umieszczone w wielolinijkowym fragmencie kodu. Znaczniki ``` lub ~~~ umożliwiają kolorowanie różnych języków programowania. Możemy nadać nazwę języka programowania używając auto-uzupełnienia, kod został pokolorowany używając konkretnych ustawień kolorowania składni:

```javascript
document.write('Hello World');
```

Możesz zaznaczyć również już wklejony kod w edytorze, i użyć komendy  by zamienić go w kod. Użyj kombinacji Ctrl+`, by dodać fragment kodu bez oznaczników języka.

Tabelki

Dodaj przykładową tabelkę używając komendy . Przykładowa tabelka składa się z dwóch kolumn, nagłówka i jednego wiersza.

Wygeneruj tabelkę na podstawie szablonu. Oddziel komórki separatorem ; lub |, a następnie zaznacz szablonu.

nazwisko;dziedzina;odkrycie
Pitagoras;mathematics;Pythagorean Theorem
Albert Einstein;physics;General Relativity
Marie Curie, Pierre Curie;chemistry;Radium, Polonium

Użyj komendy by zamienić zaznaczony szablon na tabelkę Markdown.

Lista uporządkowana i nieuporządkowana

Możliwe jest tworzenie listy numerowanych oraz wypunktowanych. Wystarczy, że pierwszym znakiem linii będzie * lub - dla listy nieuporządkowanej oraz 1. dla listy uporządkowanej.

Użyj komendy by dodać listę uporządkowaną.

1. Lista numerowana
2. Lista numerowana

Użyj komendy by dodać listę nieuporządkowaną.

* Lista wypunktowana
* Lista wypunktowana
** Lista wypunktowana (drugi poziom)

Składnia Markdown

Edytor obsługuje składnię Markdown, która składa się ze znaków specjalnych. Dostępne komendy, jak formatowanie , dodanie tabelki lub fragmentu kodu są w pewnym sensie świadome otaczającej jej składni, i postarają się unikać uszkodzenia jej.

Dla przykładu, używając tylko dostępnych komend, nie możemy dodać formatowania pogrubienia do kodu wielolinijkowego, albo dodać listy do tabelki - mogłoby to doprowadzić do uszkodzenia składni.

W pewnych odosobnionych przypadkach brak nowej linii przed elementami markdown również mógłby uszkodzić składnie, dlatego edytor dodaje brakujące nowe linie. Dla przykładu, dodanie formatowania pochylenia zaraz po tabelce, mogłoby zostać błędne zinterpretowane, więc edytor doda oddzielającą nową linię pomiędzy tabelką, a pochyleniem.

Skróty klawiszowe

Skróty formatujące, kiedy w edytorze znajduje się pojedynczy kursor, wstawiają sformatowany tekst przykładowy. Jeśli w edytorze znajduje się zaznaczenie (słowo, linijka, paragraf), wtedy zaznaczenie zostaje sformatowane.

  • Ctrl+B - dodaj pogrubienie lub pogrub zaznaczenie
  • Ctrl+I - dodaj pochylenie lub pochyl zaznaczenie
  • Ctrl+U - dodaj podkreślenie lub podkreśl zaznaczenie
  • Ctrl+S - dodaj przekreślenie lub przekreśl zaznaczenie

Notacja Klawiszy

  • Alt+K - dodaj notację klawiszy

Fragment kodu bez oznacznika

  • Alt+C - dodaj pusty fragment kodu

Skróty operujące na kodzie i linijkach:

  • Alt+L - zaznaczenie całej linii
  • Alt+, Alt+ - przeniesienie linijki w której znajduje się kursor w górę/dół.
  • Tab/⌘+] - dodaj wcięcie (wcięcie w prawo)
  • Shit+Tab/⌘+[ - usunięcie wcięcia (wycięcie w lewo)

Dodawanie postów:

  • Ctrl+Enter - dodaj post
  • ⌘+Enter - dodaj post (MacOS)