Java Language
Serializacja
Szukaj…
Wprowadzenie
Java zapewnia mechanizm zwany serializacją obiektu, w którym obiekt może być reprezentowany jako sekwencja bajtów, która zawiera dane obiektu, a także informacje o typie obiektu i typach danych przechowywanych w obiekcie.
Po zapisaniu serializowanego obiektu w pliku można go odczytać z pliku i dokonać deserializacji, to znaczy informacje o typie i bajty reprezentujące obiekt oraz jego dane można wykorzystać do odtworzenia obiektu w pamięci.
Podstawowa serializacja w Javie
Co to jest serializacja?
Serializacja to proces przekształcania stanu obiektu (w tym jego referencji) na sekwencję bajtów, a także proces odbudowywania tych bajtów w obiekt aktywny w przyszłości. Serializacja jest używana, gdy chcesz utrwalić obiekt. Jest również używany przez Java RMI do przekazywania obiektów między maszynami JVM, albo jako argumenty w wywołaniu metody od klienta do serwera, albo jako wartości zwracane z wywołania metody, lub jako wyjątki zgłaszane przez metody zdalne. Ogólnie rzecz biorąc, serializacja jest stosowana, gdy chcemy, aby obiekt istniał poza okresem istnienia JVM.
java.io.Serializable
to interfejs znaczników (nie ma treści). Służy jedynie do „oznaczania” klas Java jako możliwych do serializacji.
Współpracownicy serializacji uruchomieniowe z każdym serializable klasy numer wersji, zwany serialVersionUID
, który jest używany podczas de -serialization aby sprawdzić, czy nadawca i odbiorca szeregowany obiekt został załadowany klasy dla tego obiektu, które są kompatybilne w stosunku do serializacji. Jeśli odbiorca załadował klasę dla obiektu, który ma inny identyfikator serialVersionUID
niż klasy odpowiedniego nadawcy, deserializacja spowoduje InvalidClassException
. Do postaci szeregowej klasa może zadeklarować własną serialVersionUID
wyraźnie oświadczając pole o nazwie serialVersionUID
, który musi być static, final,
jak i typu long
:
ANY-ACCESS-MODIFIER static final long serialVersionUID = 1L;
Jak sprawić, by klasa kwalifikowała się do serializacji
Aby utrwalić obiekt, odpowiednia klasa musi zaimplementować interfejs java.io.Serializable
.
import java.io.Serializable;
public class SerialClass implements Serializable {
private static final long serialVersionUID = 1L;
private Date currentTime;
public SerialClass() {
currentTime = Calendar.getInstance().getTime();
}
public Date getCurrentTime() {
return currentTime;
}
}
Jak zapisać obiekt do pliku
Teraz musimy zapisać ten obiekt w systemie plików. W tym celu używamy java.io.ObjectOutputStream
.
import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
import java.io.IOException;
public class PersistSerialClass {
public static void main(String [] args) {
String filename = "time.ser";
SerialClass time = new SerialClass(); //We will write this object to file system.
try {
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(filename));
out.writeObject(time); //Write byte stream to file system.
out.close();
} catch(IOException ex){
ex.printStackTrace();
}
}
}
Jak odtworzyć obiekt ze stanu zserializowanego
Zapisany obiekt można później odczytać z systemu plików za pomocą java.io.ObjectInputStream
jak pokazano poniżej:
import java.io.FileInputStream;
import java.io.ObjectInputStream;
import java.io.IOException;
import java.io.java.lang.ClassNotFoundException;
public class ReadSerialClass {
public static void main(String [] args) {
String filename = "time.ser";
SerialClass time = null;
try {
ObjectInputStream in = new ObjectInputStream(new FileInputStream(filename));
time = (SerialClass)in.readObject();
in.close();
} catch(IOException ex){
ex.printStackTrace();
} catch(ClassNotFoundException cnfe){
cnfe.printStackTrace();
}
// print out restored time
System.out.println("Restored time: " + time.getTime());
}
}
Zserializowana klasa ma postać binarną. Deserializacja może być problematyczna, jeśli zmieni się definicja klasy: szczegółowe informacje można znaleźć w rozdziale Wersjonowanie obiektów seryjnych w specyfikacji Java Serialization Specification .
Serializacja obiektu szereguje cały wykres obiektu, którego jest pierwiastkiem, i działa poprawnie w obecności wykresów cyklicznych. Udostępniono metodę reset()
, aby zmusić ObjectOutputStream
do zapomnienia o obiektach, które zostały już zserializowane.
Pola przejściowe - serializacja
Serializacja za pomocą Gson
Serializacja w Gson jest łatwa i spowoduje wyświetlenie poprawnego JSON.
public class Employe {
private String firstName;
private String lastName;
private int age;
private BigDecimal salary;
private List<String> skills;
//getters and setters
}
(Serializacja)
//Skills
List<String> skills = new LinkedList<String>();
skills.add("leadership");
skills.add("Java Experience");
//Employe
Employe obj = new Employe();
obj.setFirstName("Christian");
obj.setLastName("Lusardi");
obj.setAge(25);
obj.setSalary(new BigDecimal("10000"));
obj.setSkills(skills);
//Serialization process
Gson gson = new Gson();
String json = gson.toJson(obj); //{"firstName":"Christian","lastName":"Lusardi","age":25,"salary":10000,"skills":["leadership","Java Experience"]}
Pamiętaj, że nie można serializować obiektów z referencjami kołowymi, ponieważ spowoduje to nieskończoną rekurencję.
(Deserializacja)
//it's very simple...
//Assuming that json is the previous String object....
Employe obj2 = gson.fromJson(json, Employe.class); // obj2 is just like obj
Serializacja z Jackson 2
Poniżej znajduje się implementacja, która pokazuje, w jaki sposób obiekt może zostać przekształcony do postaci szeregowej w odpowiadający mu ciąg JSON.
class Test {
private int idx;
private String name;
public int getIdx() {
return idx;
}
public void setIdx(int idx) {
this.idx = idx;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
Serializacja:
Test test = new Test();
test.setIdx(1);
test.setName("abc");
ObjectMapper mapper = new ObjectMapper();
String jsonString;
try {
jsonString = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(test);
System.out.println(jsonString);
} catch (JsonProcessingException ex) {
// Handle Exception
}
Wynik:
{
"idx" : 1,
"name" : "abc"
}
Możesz pominąć domyślną ładną drukarkę, jeśli jej nie potrzebujesz.
Stosowana tutaj zależność jest następująca:
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.6.3</version>
</dependency>
Niestandardowa serializacja
W tym przykładzie chcemy stworzyć klasę, która wygeneruje i wyśle do konsoli losową liczbę między zakresem dwóch liczb całkowitych, które są przekazywane jako argumenty podczas inicjalizacji.
public class SimpleRangeRandom implements Runnable {
private int min;
private int max;
private Thread thread;
public SimpleRangeRandom(int min, int max){
this.min = min;
this.max = max;
thread = new Thread(this);
thread.start();
}
@Override
private void WriteObject(ObjectOutputStreamout) throws IO Exception;
private void ReadObject(ObjectInputStream in) throws IOException, ClassNotFoundException;
public void run() {
while(true) {
Random rand = new Random();
System.out.println("Thread: " + thread.getId() + " Random:" + rand.nextInt(max - min));
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
Teraz, jeśli chcemy uczynić tę klasę możliwą do szeregowania, pojawią się pewne problemy. Wątek jest jedną z określonych klas na poziomie systemu, których nie można szeregować. Musimy więc zadeklarować wątek jako przejściowy . W ten sposób będziemy mogli serializować obiekty tej klasy, ale nadal będziemy mieć problem. Jak widać w konstruktorze, ustawiamy wartości minimalne i maksymalne naszego randomizatora, a następnie uruchamiamy wątek odpowiedzialny za generowanie i drukowanie wartości losowej. Dlatego podczas przywracania utrwalonego obiektu przez wywołanie readObject () konstruktor nie uruchomi się ponownie, ponieważ nie zostanie utworzony nowy obiekt. W takim przypadku musimy opracować niestandardową serializację , udostępniając dwie metody wewnątrz klasy. Te metody to:
private void writeObject(ObjectOutputStream out) throws IOException;
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException;
Zatem dodając naszą implementację do readObject () możemy zainicjować i uruchomić nasz wątek:
class RangeRandom implements Serializable, Runnable {
private int min;
private int max;
private transient Thread thread;
//transient should be any field that either cannot be serialized e.g Thread or any field you do not want serialized
public RangeRandom(int min, int max){
this.min = min;
this.max = max;
thread = new Thread(this);
thread.start();
}
@Override
public void run() {
while(true) {
Random rand = new Random();
System.out.println("Thread: " + thread.getId() + " Random:" + rand.nextInt(max - min));
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private void writeObject(ObjectOutputStream oos) throws IOException {
oos.defaultWriteObject();
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
thread = new Thread(this);
thread.start();
}
}
Oto główna część naszego przykładu:
public class Main {
public static void main(String[] args) {
System.out.println("Hello");
RangeRandom rangeRandom = new RangeRandom(1,10);
FileOutputStream fos = null;
ObjectOutputStream out = null;
try
{
fos = new FileOutputStream("test");
out = new ObjectOutputStream(fos);
out.writeObject(rangeRandom);
out.close();
}
catch(IOException ex)
{
ex.printStackTrace();
}
RangeRandom rangeRandom2 = null;
FileInputStream fis = null;
ObjectInputStream in = null;
try
{
fis = new FileInputStream("test");
in = new ObjectInputStream(fis);
rangeRandom2 = (RangeRandom)in.readObject();
in.close();
}
catch(IOException ex)
{
ex.printStackTrace();
}
catch(ClassNotFoundException ex)
{
ex.printStackTrace();
}
}
}
Jeśli uruchomisz main, zobaczysz, że dla każdej instancji RangeRandom działają dwa wątki, a to dlatego, że metoda Thread.start () jest teraz zarówno w konstruktorze, jak i readObject () .
Wersjonowanie i serialVersionUID
Po zaimplementowaniu interfejsu java.io.Serializable
celu umożliwienia serializacji klasy kompilator szuka static final
pola static final
nazwie serialVersionUID
typu long
. Jeśli klasa nie ma tego pola zadeklarowanego jawnie, kompilator utworzy jedno takie pole i przypisze mu wartość, która wynika z zależnego od implementacji obliczenia serialVersionUID
. Obliczenia te zależą od różnych aspektów tej klasy i są zgodne ze specyfikacjami serializacji obiektów podanymi przez firmę Sun. Nie gwarantuje się jednak, że wartość będzie taka sama we wszystkich implementacjach kompilatora.
Ta wartość służy do sprawdzania zgodności klas w odniesieniu do serializacji i odbywa się to podczas usuwania serializacji zapisanego obiektu. Środowisko uruchomieniowe serializacji weryfikuje, czy serialVersionUID
odczytany z danych bez serializacji i serialVersionUID
zadeklarowane w klasie są dokładnie takie same. Jeśli tak nie jest, zgłasza InvalidClassException
.
Zdecydowanie zaleca się, aby jawnie zadeklarować i zainicjować statyczne, końcowe pole typu long i o nazwie „serialVersionUID” we wszystkich klasach, które mają być ustawione na Serializację, zamiast polegać na domyślnym obliczeniu wartości dla tego pola, nawet jeśli nie zamierzasz użyj wersji. Obliczenia „serialVersionUID” są niezwykle wrażliwe i mogą różnić się w zależności od implementacji kompilatora, dlatego może pojawić się InvalidClassException
nawet dla tej samej klasy tylko dlatego, że użyłeś różnych implementacji kompilatora u nadawcy, a odbiorca kończy proces serializacji.
public class Example implements Serializable {
static final long serialVersionUID = 1L /*or some other value*/;
//...
}
Tak długo, jak serialVersionUID
jest taki sam, Java Serialization może obsługiwać różne wersje klasy. Zgodne i niekompatybilne zmiany to;
Zgodne zmiany
- Dodawanie pól: Gdy odtwarzana klasa ma pole, które nie występuje w strumieniu, to pole w obiekcie zostanie zainicjowane na wartość domyślną dla swojego typu. Jeśli wymagana jest inicjalizacja specyficzna dla klasy, klasa może zapewnić metodę readObject, która może zainicjować pole na wartości inne niż domyślne.
- Dodawanie klas: strumień będzie zawierał hierarchię typów każdego obiektu w strumieniu. Porównanie tej hierarchii w strumieniu z bieżącą klasą może wykryć dodatkowe klasy. Ponieważ w strumieniu nie ma informacji, z której można zainicjować obiekt, pola klasy zostaną zainicjowane do wartości domyślnych.
- Usuwanie klas: porównanie hierarchii klas w strumieniu z bieżącą klasą może wykryć, że klasa została usunięta. W takim przypadku pola i obiekty odpowiadające tej klasie są odczytywane ze strumienia. Pola pierwotne są odrzucane, ale tworzone są obiekty, do których odwołuje się usunięta klasa, ponieważ można do nich odwoływać się później w strumieniu. Zostaną one wyrzucone, gdy strumień zostanie wyrzucony lub wyzerowany.
- Dodawanie metod writeObject / readObject: Jeśli wersja czytająca strumień ma te metody, oczekuje się, że readObject, jak zwykle, odczyta wymagane dane zapisane w strumieniu przez domyślną serializację. Powinien on wywołać defaultReadObject przed odczytem jakichkolwiek danych opcjonalnych. Oczekuje się, że metoda writeObject jak zwykle wywoła defaultWriteObject, aby zapisać wymagane dane, a następnie może zapisać dane opcjonalne.
- Dodawanie java.io.Serializable: Odpowiada to dodawaniu typów. Nie będzie żadnych wartości w strumieniu dla tej klasy, więc jej pola zostaną zainicjowane do wartości domyślnych. Obsługa podklas klas nieserializowalnych wymaga, aby nadtyp klasy miał konstruktora no-arg, a sama klasa zostanie zainicjowana do wartości domyślnych. Jeśli konstruktor no-arg nie jest dostępny, zgłaszany jest wyjątek InvalidClassException.
- Zmiana dostępu do pola: Modyfikatory dostępu publiczny, pakietowy, chroniony i prywatny nie mają wpływu na zdolność serializacji do przypisywania wartości do pól.
- Zmiana pola ze statycznego na niestatyczne lub przejściowego na nieprzemijające: W przypadku obliczania pól szeregowalnych na podstawie domyślnej serializacji zmiana ta jest równoważna z dodaniem pola do klasy. Nowe pole zostanie zapisane w strumieniu, ale wcześniejsze klasy zignorują wartość, ponieważ serializacja nie przypisze wartości do pól statycznych lub przejściowych.
Niezgodne zmiany
- Usuwanie pól: jeśli pole zostanie usunięte w klasie, zapisany strumień nie będzie zawierał swojej wartości. Gdy strumień zostanie odczytany przez wcześniejszą klasę, wartość pola zostanie ustawiona na wartość domyślną, ponieważ żadna wartość nie jest dostępna w strumieniu. Jednak ta domyślna wartość może negatywnie wpłynąć na zdolność wcześniejszej wersji do wypełnienia umowy.
- Przenoszenie klas w górę lub w dół hierarchii: Nie jest to dozwolone, ponieważ dane w strumieniu pojawiają się w niewłaściwej kolejności.
- Zmiana pola niestatycznego na statyczne lub nieprzemijającego na przejściowe: W przypadku domyślnej serializacji ta zmiana jest równoważna usunięciu pola z klasy. Ta wersja klasy nie zapisuje tych danych w strumieniu, więc nie będzie dostępna do odczytu przez wcześniejsze wersje klasy. Podobnie jak podczas usuwania pola, pole wcześniejszej wersji zostanie zainicjowane na wartość domyślną, co może spowodować nieoczekiwane uszkodzenie klasy.
- Zmiana zadeklarowanego typu pola pierwotnego: Każda wersja klasy zapisuje dane z zadeklarowanym typem. Wcześniejsze wersje klasy próbujące odczytać pole zakończą się niepowodzeniem, ponieważ typ danych w strumieniu nie odpowiada typowi pola.
- Zmiana metody writeObject lub readObject, aby nie zapisywała i nie odczytywała domyślnych danych pól, lub zmieniana, aby próbowała je zapisać lub odczytać, gdy poprzednia wersja tego nie zrobiła. Domyślne dane pola muszą konsekwentnie pojawiać się lub nie pojawiać w strumieniu.
- Zmiana klasy z Serializable na Externalizable lub odwrotnie jest niezgodną zmianą, ponieważ strumień będzie zawierał dane niezgodne z implementacją dostępnej klasy.
- Zmiana klasy z typu nie-wyliczeniowego na typ wyliczeniowy lub odwrotnie, ponieważ strumień będzie zawierał dane niezgodne z implementacją dostępnej klasy.
- Usunięcie opcji Serializable lub Externalizable jest niezgodną zmianą, ponieważ po napisaniu nie będzie już zawierała pól wymaganych przez starsze wersje klasy.
- Dodanie metody writeReplace lub readResolve do klasy jest niezgodne, jeśli zachowanie spowodowałoby powstanie obiektu niezgodnego z jakąkolwiek starszą wersją klasy.
Niestandardowa deserializacja JSON z Jacksonem
Używamy API spoczynkowego w formacie JSON, a następnie rozprowadzamy go w POJO. Org.codehaus.jackson.map.ObjectMapper Jacksona „po prostu działa” od razu po wyjęciu z pudełka i w większości przypadków nie robimy nic. Ale czasami potrzebujemy niestandardowego deserializatora, aby spełnić nasze niestandardowe potrzeby, a ten samouczek poprowadzi Cię przez proces tworzenia własnego niestandardowego deserializatora.
Powiedzmy, że mamy następujące podmioty.
public class User {
private Long id;
private String name;
private String email;
//getter setter are omitted for clarity
}
I
public class Program {
private Long id;
private String name;
private User createdBy;
private String contents;
//getter setter are omitted for clarity
}
Najpierw zserializuj / uporządkuj obiekt.
User user = new User();
user.setId(1L);
user.setEmail("[email protected]");
user.setName("Bazlur Rahman");
Program program = new Program();
program.setId(1L);
program.setName("Program @# 1");
program.setCreatedBy(user);
program.setContents("Some contents");
ObjectMapper objectMapper = new ObjectMapper();
final String json = objectMapper.writeValueAsString (program); System.out.println (json);
Powyższy kod wygeneruje następujący JSON-
{
"id": 1,
"name": "Program @# 1",
"createdBy": {
"id": 1,
"name": "Bazlur Rahman",
"email": "[email protected]"
},
"contents": "Some contents"
}
Teraz można bardzo łatwo zrobić coś przeciwnego. Jeśli mamy ten JSON, możemy odmarsić obiekt programu za pomocą ObjectMapper w następujący sposób -
Powiedzmy, że to nie jest prawdziwy przypadek, będziemy mieć inny JSON niż interfejs API, który nie pasuje do naszej klasy Program
.
{
"id": 1,
"name": "Program @# 1",
"ownerId": 1
"contents": "Some contents"
}
Spójrz na ciąg JSON, jak widzisz, ma on inne pole, które jest owenerId.
Teraz, jeśli chcesz serializować ten JSON, jak to zrobiliśmy wcześniej, będziesz mieć wyjątki.
Są dwa sposoby na uniknięcie wyjątków i zsekwencjonowanie -
Zignoruj nieznane pola
Zignoruj wartość onwerId
. Dodaj następującą adnotację do klasy Program
@JsonIgnoreProperties(ignoreUnknown = true)
public class Program {}
Napisz niestandardowy deserializator
Ale zdarzają się przypadki, gdy faktycznie potrzebujesz tego pola owerId
. Powiedzmy, że chcesz powiązać to jako identyfikator klasy User
.
W takim przypadku musisz napisać niestandardowy deserializator
Jak widać, najpierw musisz uzyskać dostęp do JsonNode
z JonsParser
. Następnie możesz łatwo wyodrębnić informacje z JsonNode
za pomocą metody get()
. i musisz się upewnić co do nazwy pola. Powinna to być dokładna nazwa, błąd w pisowni spowoduje wyjątki.
I na koniec, musisz zarejestrować swój ProgramDeserializer w ObjectMapper
.
ObjectMapper mapper = new ObjectMapper();
SimpleModule module = new SimpleModule();
module.addDeserializer(Program.class, new ProgramDeserializer());
mapper.registerModule(module);
String newJsonString = "{\"id\":1,\"name\":\"Program @# 1\",\"ownerId\":1,\"contents\":\"Some contents\"}";
final Program program2 = mapper.readValue(newJsonString, Program.class);
Alternatywnie możesz użyć adnotacji, aby bezpośrednio zarejestrować deserializer -
@JsonDeserialize(using = ProgramDeserializer.class)
public class Program {
}