Zoeken…


Invoering

Java biedt een mechanisme, objectserialisatie genaamd, waarbij een object kan worden weergegeven als een reeks bytes die de gegevens van het object bevat, evenals informatie over het type van het object en de soorten gegevens die in het object zijn opgeslagen.

Nadat een geserialiseerd object in een bestand is geschreven, kan het uit het bestand worden gelezen en gedeserialiseerd, d.w.z. de type-informatie en bytes die het object vertegenwoordigen en de gegevens ervan kunnen worden gebruikt om het object in het geheugen opnieuw te creëren.

Basisserialisatie in Java

Wat is serialisatie?

Serialisatie is het proces van het omzetten van de status van een object (inclusief de referenties) naar een reeks bytes, evenals het proces om die bytes op een later tijdstip om te bouwen tot een levend object. Serialisatie wordt gebruikt wanneer u het object wilt behouden. Het wordt ook gebruikt door Java RMI om objecten tussen JVM's door te geven, hetzij als argumenten in een methode-aanroep van een client naar een server of als retourwaarden van een methode-aanroep, of als uitzonderingen die worden gegenereerd door externe methoden. In het algemeen wordt serialisatie gebruikt wanneer we willen dat het object na de levensduur van de JVM blijft bestaan.

java.io.Serializable is een markerinterface (heeft geen body). Het wordt alleen gebruikt om Java-klassen te "markeren" als serializable.

De serialisatie runtime koppelt aan elke serialiseerbare klasse een versienummer, een serialVersionUID , dat wordt gebruikt tijdens de -serialisatie om te controleren of de afzender en ontvanger van een geserialiseerd object klassen voor dat object hebben geladen die compatibel zijn met betrekking tot serialisatie. Als de ontvanger een klasse heeft geladen voor het object dat een andere serialVersionUID dan die van de klasse van de overeenkomstige afzender, resulteert deserialisatie in een InvalidClassException . Een serialiseerbare klasse kan zijn eigen serialVersionUID expliciet declareren door een veld met de naam serialVersionUID dat static, final, en van het type long :

ANY-ACCESS-MODIFIER static final long serialVersionUID = 1L;

Hoe een klasse in aanmerking te laten komen voor serialisatie

Om een object te behouden, moet de betreffende klasse de interface java.io.Serializable implementeren.

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;
    }
}

Hoe een object in een bestand te schrijven

Nu moeten we dit object naar een bestandssysteem schrijven. We gebruiken hiervoor 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();
        }
     }
 }

Een object opnieuw maken vanuit de geserialiseerde status

Het opgeslagen object kan later uit het bestandssysteem worden gelezen met behulp van java.io.ObjectInputStream zoals hieronder weergegeven:

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());
     }
 }

De geserialiseerde klasse is in binaire vorm. De deserialisatie kan problematisch zijn als de klassedefinitie verandert: zie het hoofdstuk Versiebeheer van geserialiseerde objecten van de Java Serialization Specification voor meer informatie.

Het serialiseren van een object maakt de hele objectgrafiek waarvan het de root is, serieel en werkt correct in de aanwezigheid van cyclische grafieken. Er is een methode reset() beschikbaar om de ObjectOutputStream te dwingen ObjectOutputStream te vergeten die al zijn geserialiseerd.

Tijdelijke velden - Serialisatie

Serialisatie met Gson

Serialisatie met Gson is eenvoudig en levert de juiste JSON op.

public class Employe {

    private String firstName;
    private String lastName;
    private int age;      
    private BigDecimal salary;
    private List<String> skills;

    //getters and setters
}

(Serialisatie)

//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"]}

Merk op dat u geen objecten kunt serialiseren met cirkelvormige verwijzingen, omdat dat zal resulteren in een oneindige recursie.

(Deserialisatie)

//it's very simple...
//Assuming that json is the previous String object....

Employe obj2 = gson.fromJson(json, Employe.class); // obj2 is just like obj

Serialisatie met Jackson 2

Hierna volgt een implementatie die laat zien hoe een object kan worden geserialiseerd in de bijbehorende JSON-string.

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;
    }
}

serialization:

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
}

Output:

{
  "idx" : 1,
  "name" : "abc"
}

U kunt de standaard mooie printer weglaten als u deze niet nodig hebt.

De afhankelijkheid die hier wordt gebruikt is als volgt:

<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.6.3</version>
</dependency>

Aangepaste serialisatie

In dit voorbeeld willen we een klasse maken die een console genereert en uitvoert, een willekeurig getal tussen een bereik van twee gehele getallen die tijdens de initialisatie als argumenten worden doorgegeven.

    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();
            }
        }
    }
    }

Als we deze klasse serieel willen maken, zullen er enkele problemen zijn. De thread is een van de bepaalde klassen op systeemniveau die niet te serialiseren zijn. Dus moeten we de draad als vergankelijk verklaren. Door dit te doen zullen we de objecten van deze klasse kunnen serialiseren, maar we zullen nog steeds een probleem hebben. Zoals je kunt zien in de constructor stellen we de min- en de max-waarden van onze randomizer in en daarna beginnen we de thread die verantwoordelijk is voor het genereren en afdrukken van de willekeurige waarde. Dus bij het herstellen van het blijvende object door readObject () aan te roepen , wordt de constructor niet opnieuw uitgevoerd omdat er geen nieuw object is gemaakt. In dat geval moeten we een aangepaste serialisering ontwikkelen door twee methoden binnen de klasse aan te bieden. Die methoden zijn:

private void writeObject(ObjectOutputStream out) throws IOException;
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException;

Dus door onze implementatie toe te voegen aan readObject () kunnen we onze thread starten en starten:

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();
}
}

Hier is de belangrijkste voor ons voorbeeld:

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();
           }

}
}

Als u de main uitvoert, ziet u dat er twee threads worden uitgevoerd voor elke instantie RangeRandom en dat komt omdat de methode Thread.start () zich nu zowel in de constructor als in de readObject () bevindt .

Versiebeheer en serialVersionUID

Wanneer u de interface java.io.Serializable implementeert om een klasse serializable te maken, zoekt de compiler naar een static final veld met de naam serialVersionUID van het type long . Als de klasse dit veld niet expliciet heeft opgegeven, maakt de compiler een dergelijk veld en wijst het een waarde toe die voortvloeit uit een implementatie-afhankelijke berekening van serialVersionUID . Deze berekening is afhankelijk van verschillende aspecten van de klasse en volgt de door Sun opgegeven specificaties voor objectserialisatie . Maar de waarde is niet gegarandeerd hetzelfde voor alle implementaties van de compiler.

Deze waarde wordt gebruikt om de compatibiliteit van de klassen met betrekking tot serialisatie te controleren en dit wordt gedaan tijdens het de-serialiseren van een opgeslagen object. De Serialization Runtime verifieert dat serialVersionUID gelezen uit de serialVersionUID gegevens en de serialVersionUID die in de klasse zijn opgegeven exact hetzelfde zijn. Als dat niet het geval is, wordt een InvalidClassException .

Het wordt ten zeerste aanbevolen dat u expliciet het statische, laatste veld van het type long en de naam 'serialVersionUID' declareert en initialiseert in al uw klassen die u Serializable wilt maken in plaats van te vertrouwen op de standaardberekening van de waarde voor dit veld, zelfs als u niet gebruik versiebeheer. 'serialVersionUID' berekening is extreem gevoelig en kan variëren van de ene compiler-implementatie tot de andere en daarom kun je de InvalidClassException zelfs voor dezelfde klasse, alleen omdat je verschillende compiler-implementaties op de afzender en de ontvanger hebt gebruikt tijdens het serialisatieproces.

public class Example implements Serializable {          
    static final long serialVersionUID = 1L /*or some other value*/;
    //...
}

Zolang serialVersionUID hetzelfde is, kan Java Serialization verschillende versies van een klasse aan. Compatibele en incompatibele wijzigingen zijn;

Compatibele wijzigingen

  • Velden toevoegen: wanneer de te reconstrueren klasse een veld heeft dat niet voorkomt in de stream, wordt dat veld in het object geïnitialiseerd naar de standaardwaarde voor het type. Als klassespecifieke initialisatie nodig is, kan de klasse een readObject-methode bieden waarmee het veld kan worden geïnitialiseerd naar niet-standaardwaarden.
  • Klassen toevoegen: de stream bevat de typehiërarchie van elk object in de stream. Door deze hiërarchie in de stream te vergelijken met de huidige klasse, kunnen extra klassen worden gedetecteerd. Omdat er geen informatie in de stream staat om het object te initialiseren, worden de velden van de klasse geïnitialiseerd naar de standaardwaarden.
  • Klassen verwijderen: door de klassenhiërarchie in de stream te vergelijken met die van de huidige klasse, kan worden gedetecteerd dat een klasse is verwijderd. In dit geval worden de velden en objecten die overeenkomen met die klasse uit de stream gelezen. Primitieve velden worden genegeerd, maar de objecten waarnaar door de verwijderde klasse wordt verwezen, worden gemaakt, omdat hier later in de stream naar kan worden verwezen. Ze worden vuilnis verzameld als de stroom vuilnis wordt verzameld of opnieuw wordt ingesteld.
  • De methoden writeObject / readObject toevoegen: als de versie die de stream leest deze methoden heeft, wordt van readObject zoals gebruikelijk verwacht dat het de vereiste gegevens leest die naar de stream zijn geschreven door de standaardserialisatie. Het moet eerst defaultReadObject aanroepen voordat optionele gegevens worden gelezen. Van de methode writeObject wordt verwacht dat deze zoals gewoonlijk defaultWriteObject aanroept om de vereiste gegevens te schrijven en vervolgens optionele gegevens kan schrijven.
  • Java.io.Serializable toevoegen: dit komt overeen met het toevoegen van typen. Er zijn geen waarden in de stream voor deze klasse, dus de velden worden geïnitialiseerd naar standaardwaarden. De ondersteuning voor subklasse niet-serialiseerbare klassen vereist dat het supertype van de klasse een no-arg constructor heeft en de klasse zelf wordt geïnitialiseerd naar standaardwaarden. Als de no-arg constructor niet beschikbaar is, wordt de InvalidClassException geworpen.
  • De toegang tot een veld wijzigen: de toegangsmodificatoren public, package, protected en private hebben geen effect op de mogelijkheid van serialisatie om waarden aan de velden toe te wijzen.
  • Een veld wijzigen van statisch in niet-statisch of tijdelijk in niet-tijdelijk: wanneer u op standaardserialisatie vertrouwt om de serialiseerbare velden te berekenen, is deze wijziging gelijk aan het toevoegen van een veld aan de klasse. Het nieuwe veld wordt naar de stream geschreven, maar eerdere klassen negeren de waarde, omdat serialisatie geen waarden toewijst aan statische of tijdelijke velden.

Onverenigbare wijzigingen

  • Velden verwijderen: als een veld in een klasse wordt verwijderd, bevat de geschreven stream niet de waarde. Wanneer de stream wordt gelezen door een eerdere klasse, wordt de waarde van het veld ingesteld op de standaardwaarde omdat er geen waarde beschikbaar is in de stream. Deze standaardwaarde kan echter een negatieve invloed hebben op het vermogen van de eerdere versie om zijn contract na te komen.
  • Klassen omhoog of omlaag verplaatsen in de hiërarchie: dit kan niet worden toegestaan omdat de gegevens in de stream in de verkeerde volgorde verschijnen.
  • Een niet-statisch veld wijzigen in statisch of een niet-tijdelijk veld in tijdelijk: wanneer u vertrouwt op standaardserialisatie, is deze wijziging gelijk aan het verwijderen van een veld uit de klasse. Deze versie van de klasse schrijft die gegevens niet naar de stream, zodat deze niet door eerdere versies van de klasse kunnen worden gelezen. Net als bij het verwijderen van een veld, wordt het veld van de eerdere versie geïnitialiseerd naar de standaardwaarde, waardoor de klasse op onverwachte manieren kan mislukken.
  • Het gedeclareerde type van een primitief veld wijzigen: elke versie van de klasse schrijft de gegevens met het gedeclareerde type. Eerdere versies van de klasse die het veld probeert te lezen, zullen mislukken omdat het type gegevens in de stream niet overeenkomt met het type veld.
  • De methode writeObject of readObject wijzigen zodat deze niet langer de standaardveldgegevens schrijft of leest of deze zodanig wijzigt dat wordt geprobeerd deze te schrijven of te lezen terwijl de vorige versie dat niet deed. De standaardveldgegevens moeten consistent of niet in de stream worden weergegeven.
  • Het wijzigen van een klasse van Serializable naar Externalizable of vice versa is een incompatibele wijziging, omdat de stream gegevens bevat die incompatibel zijn met de implementatie van de beschikbare klasse.
  • Een klasse wijzigen van een niet-opsommingstype naar een opsommingstype of vice versa, omdat de stream gegevens bevat die niet compatibel zijn met de implementatie van de beschikbare klasse.
  • Het verwijderen van Serializable of Externalizable is een onverenigbare wijziging, omdat het tijdens het schrijven niet langer de velden bevat die nodig zijn voor oudere versies van de klasse.
  • Het toevoegen van de methode writeReplace of readResolve aan een klasse is niet compatibel als het gedrag een object zou produceren dat niet compatibel is met een oudere versie van de klasse.

Aangepaste JSON-deserialisatie met Jackson

We verbruiken de rest-API als een JSON-formaat en unmarshal het naar een POJO. Org.codehaus.jackson.map.ObjectMapper van Jackson "werkt gewoon" uit de doos en we doen echt niets in de meeste gevallen. Maar soms hebben we een aangepaste deserializer nodig om aan onze aangepaste behoeften te voldoen en deze zelfstudie leidt u door het proces van het maken van uw eigen aangepaste deserializer.

Laten we zeggen dat we volgende entiteiten hebben.

public class User {
    private Long id;
    private String name;
    private String email;

    //getter setter are omitted for clarity 
}

En

public class Program {
    private Long id;
    private String name;
    private User createdBy;
    private String contents;

    //getter setter are omitted for clarity
}

Laten we eerst een object serialiseren / rangschikken.

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 (programma); System.out.println (json);

De bovenstaande code zal de volgende JSON- produceren

{
    "id": 1,
    "name": "Program @# 1",
    "createdBy": {
        "id": 1,
        "name": "Bazlur Rahman",
        "email": "[email protected]"
    },
    "contents": "Some contents"
}

Nu kan het tegenovergestelde heel gemakkelijk worden gedaan. Als we deze JSON hebben, kunnen we unmarshaleren naar een programmaobject met ObjectMapper als volgt -

Laten we zeggen dat dit niet het geval is, we hebben een andere JSON dan een API die niet overeenkomt met onze Program .

{
"id": 1,
"name": "Program @# 1",
"ownerId": 1
"contents": "Some contents"
}

Kijk naar de JSON-string, zie je, deze heeft een ander veld dat owenerId is.

Als je nu deze JSON wilt serialiseren zoals we eerder deden, heb je uitzonderingen.

Er zijn twee manieren om uitzonderingen te voorkomen en dit te laten serialiseren -

Negeer de onbekende velden

Negeer de onwerId . Voeg de volgende annotatie toe aan de programmaklasse

@JsonIgnoreProperties(ignoreUnknown = true)
public class Program {}

Schrijf een aangepaste deserializer

Maar er zijn gevallen waarin u dit veld owerId echt nodig hebt. Stel dat u het wilt relateren als een id van de klasse User .

In dat geval moet u een aangepaste deserializer schrijven

Zoals u ziet, moet u eerst toegang krijgen tot de JsonNode vanuit de JonsParser . En dan kunt u eenvoudig informatie uit een JsonNode met behulp van de methode get() . en je moet zeker weten wat de veldnaam is. Het zou de exacte naam moeten zijn, spelfout zal uitzonderingen veroorzaken.

En ten slotte moet u uw ProgramDeserializer bij de ObjectMapper registreren.

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);

Als alternatief kunt u annotatie gebruiken om de deserializer rechtstreeks te registreren -

@JsonDeserialize(using = ProgramDeserializer.class)
public class Program {
}


Modified text is an extract of the original Stack Overflow Documentation
Licentie onder CC BY-SA 3.0
Niet aangesloten bij Stack Overflow