Java Language
serialisering
Sök…
Introduktion
Java tillhandahåller en mekanism, kallad objektserialisering där ett objekt kan representeras som en sekvens av byte som inkluderar objektets data såväl som information om objektets typ och typer av data lagrade i objektet.
Efter att ett seriellt objekt har skrivits in i en fil kan det läsas från filen och deserialiseras, det vill säga typinformation och byte som representerar objektet och dess data kan användas för att återskapa objektet i minnet.
Grundläggande serialisering i Java
Vad är serialisering
Serialisering är processen för att konvertera ett objekts tillstånd (inklusive referenser) till en sekvens av byte, liksom processen för att bygga om dessa bytes till ett liveobjekt vid en framtida tidpunkt. Serialisering används när du vill fortsätta objektet. Det används också av Java RMI för att skicka objekt mellan JVM: er, antingen som argument i en metodkallning från en klient till en server eller som returvärden från en metodkallning, eller som undantag som kastas av fjärrmetoder. I allmänhet används serialisering när vi vill att objektet ska existera längre än JVM: s livstid.
java.io.Serializable
är ett markörgränssnitt (har ingen kropp). Det används bara för att "markera" Java-klasser som seriellt.
Serialisering runtime associerar med varje serializable klass ett versionsnummer, en så kallad serialVersionUID
, som används under de -serialization att verifiera att avsändaren och mottagaren av en serie föremål har laddat klasser för objektet som är kompatibla med avseende på serialisering. Om mottagaren har laddat en klass för objektet som har en annan serialVersionUID
än den för motsvarande avsändares klass, kommer deserialisering att resultera i en InvalidClassException
. En klassificerbar klass kan deklarera sitt eget serialVersionUID
uttryckligen genom att förklara ett fält som heter serialVersionUID
som måste vara static, final,
och av typen long
:
ANY-ACCESS-MODIFIER static final long serialVersionUID = 1L;
Hur man gör en klass kvalificerad för serialisering
För att bestå ett objekt måste respektive klass implementera gränssnittet 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;
}
}
Hur man skriver ett objekt till en fil
Nu måste vi skriva detta objekt till ett filsystem. Vi använder java.io.ObjectOutputStream
för detta ändamål.
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();
}
}
}
Hur man återskapar ett objekt från det seriella tillståndet
Det lagrade objektet kan läsas från filsystemet senare med hjälp av java.io.ObjectInputStream
som visas nedan:
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());
}
}
Den klassificerade klassen är i binär form. Deserialiseringen kan vara problematisk om klassdefinitionen ändras: se kapitel Versionering av serialiserade objekt i Java Serialization Specification för detaljer.
Serialisering av ett objekt serialiserar hela objektgrafen som det är roten och fungerar korrekt i närvaro av cykliska grafer. En reset()
tillhandahålls för att tvinga ObjectOutputStream
att glömma bort objekt som redan har serialiserats.
Övergående fält - Serialisering
Serialisering med Gson
Serialisering med Gson är enkel och ger korrekt JSON.
public class Employe {
private String firstName;
private String lastName;
private int age;
private BigDecimal salary;
private List<String> skills;
//getters and setters
}
(Serialization)
//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"]}
Observera att du inte kan serialisera objekt med cirkulära referenser eftersom det kommer att resultera i oändlig rekursion.
(Deserialiseringsundantag)
//it's very simple...
//Assuming that json is the previous String object....
Employe obj2 = gson.fromJson(json, Employe.class); // obj2 is just like obj
Serialisering med Jackson 2
Följande är en implementering som visar hur ett objekt kan serialiseras i dess motsvarande JSON-sträng.
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
}
Produktion:
{
"idx" : 1,
"name" : "abc"
}
Du kan utelämna den vackra skrivaren för standard om du inte behöver den.
Beroendet som används här är som följer:
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.6.3</version>
</dependency>
Anpassad serialisering
I det här exemplet vill vi skapa en klass som kommer att generera och mata ut till konsolen, ett slumpmässigt nummer mellan ett intervall av två heltal som passeras som argument under initialiseringen.
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();
}
}
}
}
Om vi nu vill göra denna klass serialiserbar kommer det att finnas några problem. Tråden är en av de klasser på systemnivå som inte kan serialiseras. Så vi måste förklara tråden som kortvarig . Genom att göra detta kommer vi att kunna serialisera objekt i denna klass men vi har fortfarande ett problem. Som ni ser i konstruktören ställer vi in min- och maxvärden för vår randomiserare och efter det startar vi tråden som är ansvarig för att generera och skriva ut det slumpmässiga värdet. När återställning av det uthärdade objektet genom att ringa readObject () kommer konstruktören således inte att köra igen eftersom det inte finns något skapande av ett nytt objekt. I så fall måste vi utveckla en anpassad serialisering genom att tillhandahålla två metoder i klassen. Dessa metoder är:
private void writeObject(ObjectOutputStream out) throws IOException;
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException;
Således genom att lägga till vår implementering i readObject () kan vi initiera och starta vår tråd:
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();
}
}
Här är det viktigaste för vårt exempel:
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();
}
}
}
Om du kör huvudet ser du att det finns två trådar som körs för varje RangeRandom-instans och det beror på att Thread.start () -metoden nu finns i både konstruktören och readObject () .
Versionering och serialVersionUID
När du implementerar java.io.Serializable
gränssnitt för att göra en klass seriebar, letar kompilatorn efter ett static final
serialVersionUID
namnet serialVersionUID
av typen long
. Om klassen inte har det här fältet uttryckligen deklarerat kommer kompilatorn att skapa ett sådant fält och tilldela det med ett värde som kommer från en implementeringsberoende beräkning av serialVersionUID
. Denna beräkning beror på olika aspekter av klassen och följer specifikationerna för objektserialisering som ges av Sun. Men värdet garanteras inte vara detsamma för alla kompilatorimplementeringar.
Det här värdet används för att kontrollera kompatibiliteten för klasserna med avseende på serialisering och detta görs under avserialisering av ett sparat objekt. Serialization Runtime verifierar att serialVersionUID
läst från de-serialiserade data och serialVersionUID
deklarerats i klassen är exakt samma. Om så inte är fallet kastar det en InvalidClassException
.
Det rekommenderas starkt att du uttryckligen förklarar och initierar det statiska, slutliga fältet av typen långt och benämnt 'serialVersionUID' i alla dina klasser som du vill göra Serializable istället för att förlita dig på standardberäkningen av värdet för detta fält även om du inte ska använd versionering. Beräkningen 'serialVersionUID' är extremt känslig och kan variera från en kompilatorimplementering till en annan och därför kan du komma att få InvalidClassException
med för samma klass bara för att du använde olika kompilatorimplementeringar på avsändaren och mottagarens slut i serieringsprocessen.
public class Example implements Serializable {
static final long serialVersionUID = 1L /*or some other value*/;
//...
}
Så länge serialVersionUID
är densamma, kan Java Serialization hantera olika versioner av en klass. Kompatibla och inkompatibla förändringar är;
Kompatibla ändringar
- Lägga till fält: När klassen som rekonstitueras har ett fält som inte förekommer i strömmen kommer det fältet i objektet att initialiseras till standardvärdet för dess typ. Om klassspecifik initialisering behövs kan klassen tillhandahålla en readObject-metod som kan initiera fältet till icke-defekta värden.
- Lägga till klasser: Strömmen innehåller typhierarkin för varje objekt i strömmen. Att jämföra denna hierarki i strömmen med den aktuella klassen kan upptäcka ytterligare klasser. Eftersom det inte finns någon information i strömmen som objektet ska initialiseras initieras klassens fält till standardvärdena.
- Ta bort klasser: Att jämföra klasshierarkin i strömmen med den för den aktuella klassen kan upptäcka att en klass har tagits bort. I detta fall läses fälten och objekt som motsvarar den klassen från strömmen. Primitiva fält kasseras, men de objekt som refereras till av den raderade klassen skapas eftersom de kan hänvisas till senare i strömmen. De kommer att samlas in när skräpet samlas in eller återställs.
- Lägga till writeObject / readObject-metoder: Om versionen som läser strömmen har dessa metoder, förväntas readObject, som vanligt, att läsa nödvändig data skrivet till strömmen med standardserien. Det bör ringa defaultReadObject först innan du läser valfri data. Metoden writeObject förväntas som vanligt att anropa defaultWriteObject för att skriva önskad data och sedan kan skriva valfri data.
- Lägga till java.io.Serializable: Detta motsvarar att lägga till typer. Det finns inga värden i strömmen för den här klassen så dess fält kommer att initialiseras till standardvärden. Stödet för underklassificering av icke-klassificerbara klasser kräver att klassens supertyp har en no-arg-konstruktör och klassen själv kommer att initialiseras till standardvärden. Om no-arg-konstruktorn inte är tillgänglig kastas InvalidClassException.
- Ändra åtkomst till ett fält: Tillgångsmodifierarna offentliga, paket, skyddade och privata har ingen inverkan på möjligheten att serialisera att tilldela värden till fälten.
- Ändra ett fält från statisk till icke-statisk eller övergående till icke-transient: När man litar på standardserien för att beräkna de serierbara fälten, är denna förändring likvärdig med att lägga till ett fält i klassen. Det nya fältet kommer att skrivas till strömmen men tidigare klasser kommer att ignorera värdet eftersom serialisering inte kommer att tilldela värden till statiska eller övergående fält.
Oförenliga förändringar
- Radera fält: Om ett fält raderas i en klass kommer den skrivna strömmen inte att innehålla dess värde. När strömmen läses av en tidigare klass kommer fältets värde att ställas in på standardvärdet eftersom inget värde är tillgängligt i strömmen. Detta standardvärde kan emellertid negativt försämra förmågan hos den tidigare versionen att uppfylla sitt kontrakt.
- Flytta klasser upp eller ner i hierarkin: Detta kan inte tillåtas eftersom data i strömmen visas i fel sekvens.
- Ändra ett icke-statiskt fält till statisk eller ett oöverskådligt fält till övergående: När man förlitar sig på standardserialisering motsvarar denna förändring raderingen av ett fält från klassen. Denna version av klassen kommer inte att skriva den informationen till strömmen, så den kommer inte att vara tillgänglig för läsning av tidigare versioner av klassen. Som vid radering av ett fält kommer fältet i den tidigare versionen att initialiseras till standardvärdet, vilket kan göra att klassen misslyckas på oväntade sätt.
- Ändra den deklarerade typen av ett primitivt fält: Varje klassklassversion skriver uppgifterna med den deklarerade typen. Tidigare versioner av klassen som försöker läsa fältet kommer att misslyckas eftersom typen av data i strömmen inte stämmer med fältet.
- Ändra metoden writeObject eller readObject så att den inte längre skriver eller läser standardfältsdata eller ändrar den så att den försöker skriva den eller läsa den när den tidigare versionen inte gjorde det. Standardfältsdata måste konsekvent antingen visas eller inte visas i strömmen.
- Att ändra en klass från Serializable till Externalizable eller vice versa är en inkompatibel förändring eftersom strömmen kommer att innehålla data som är oförenliga med implementeringen av den tillgängliga klassen.
- Ändra en klass från en icke-enumtyp till enumtyp eller vice versa eftersom strömmen kommer att innehålla data som är oförenliga med implementeringen av den tillgängliga klassen.
- Att ta bort antingen serialiserbar eller eksternaliserbar är en oförenlig förändring eftersom den inte skrivs längre än de fält som behövs av äldre versioner av klassen.
- Lägga till metoden writeReplace eller readResolve i en klass är oförenlig om beteendet skulle ge ett objekt som är oförenligt med någon äldre version av klassen.
Anpassad JSON Deserialization med Jackson
Vi konsumerar resten API som ett JSON-format och avmarkerar det sedan till en POJO. Jacksons org.codehaus.jackson.map.ObjectMapper "fungerar bara" ur rutan och vi gör verkligen ingenting i de flesta fall. Men ibland behöver vi anpassade deserializer för att uppfylla våra anpassade behov och denna handledning leder dig genom processen att skapa din egen anpassade deserializer.
Låt oss säga att vi har följande enheter.
public class User {
private Long id;
private String name;
private String email;
//getter setter are omitted for clarity
}
Och
public class Program {
private Long id;
private String name;
private User createdBy;
private String contents;
//getter setter are omitted for clarity
}
Låt oss först serialisera / marschera ett objekt.
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);
Ovanstående kod kommer att producera efter JSON-
{
"id": 1,
"name": "Program @# 1",
"createdBy": {
"id": 1,
"name": "Bazlur Rahman",
"email": "[email protected]"
},
"contents": "Some contents"
}
Nu kan man göra motsatsen mycket enkelt. Om vi har denna JSON kan vi avmarkera ett programobjekt med hjälp av ObjectMapper enligt följande -
Låt oss nu säga, detta är inte det verkliga fallet, vi kommer att ha en annan JSON från ett API som inte matchar vår Program
.
{
"id": 1,
"name": "Program @# 1",
"ownerId": 1
"contents": "Some contents"
}
Titta på JSON-strängen, du kan se, den har ett annat fält som är owenerId.
Om du vill serialisera denna JSON som vi gjorde tidigare har du undantag.
Det finns två sätt att undvika undantag och göra det seriellt -
Ignorera de okända fälten
Ignorera onwerId
. Lägg till följande kommentar i klassen Program
@JsonIgnoreProperties(ignoreUnknown = true)
public class Program {}
Skriv anpassad deserializer
Men det finns fall när du faktiskt behöver denna owerId
område. Låt oss säga att du vill att relatera det som ett ID för User
I sådant fall måste du skriva en anpassad deserializer-
Som ni ser måste du först komma åt JsonNode
från JonsParser
. Och sedan kan du enkelt extrahera information från en JsonNode
med metoden get()
. och du måste se till om fältnamnet. Det borde vara det exakta namnet, stavfel orsakar undantag.
Och slutligen måste du registrera din ProgramDeserializer till 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);
Alternativt kan du använda en kommentar för att registrera deserializer direkt -
@JsonDeserialize(using = ProgramDeserializer.class)
public class Program {
}