Java中的序列化是在JDK 1.1中引入的,也是Core Java的重要功能之一。
Java中的序列化
Java中的序列化允许我们将对象转换为流,以便通过网络发送,保存为文件或在后续使用中存储在数据库中。反序列化是将对象流转换为实际Java对象以在程序中使用的过程。起初,Java中的序列化似乎很容易使用,但它却带有一些微不足道的安全性和完整性问题,我们将在本文的后部分进行讨论。在本教程中,我们将研究以下主题。
Java 中的可序列化
如果你想讓一個類別物件可序列化,你只需要實作 java.io.Serializable
介面。在 Java 中,Serializable 是一個標記介面,沒有任何需要實作的欄位或方法。這就像是一個選擇性參與的過程,通過它我們可以使我們的類別可序列化。在 Java 中,序列化是通過 ObjectInputStream
和 ObjectOutputStream
實現的,所以我們只需要一個封裝器來將它們保存到文件或者發送到網絡上。讓我們來看一個簡單的 Java 程序序列化的例子。
package com.journaldev.serialization;
import java.io.Serializable;
public class Employee implements Serializable {
// private static final long serialVersionUID = -6470090944414208496L;
private String name;
private int id;
transient private int salary;
// private String password;
@Override
public String toString(){
return "Employee{name="+name+",id="+id+",salary="+salary+"}";
}
//getter 和 setter 方法
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public int getSalary() {
return salary;
}
public void setSalary(int salary) {
this.salary = salary;
}
// public String getPassword() {
// return password;
// }
//
// public void setPassword(String password) {
// this.password = password;
// }
}
請注意,這只是一個簡單的 Java Bean,帶有一些屬性和 getter-setter 方法。如果你不想讓物件屬性被序列化到流中,你可以使用 transient 關鍵字,就像我用薪水變數做的一樣。現在假設我們想將我們的物件寫入文件,然後從同一個文件反序列化它。因此,我們需要使用 ObjectInputStream
和 ObjectOutputStream
進行序列化的實用方法。
package com.journaldev.serialization;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
/**
* A simple class with generic serialize and deserialize method implementations
*
* @author pankaj
*
*/
public class SerializationUtil {
// 從給定文件反序列化為物件
public static Object deserialize(String fileName) throws IOException,
ClassNotFoundException {
FileInputStream fis = new FileInputStream(fileName);
ObjectInputStream ois = new ObjectInputStream(fis);
Object obj = ois.readObject();
ois.close();
return obj;
}
// 將給定物件序列化並保存到文件
public static void serialize(Object obj, String fileName)
throws IOException {
FileOutputStream fos = new FileOutputStream(fileName);
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(obj);
fos.close();
}
}
請注意,方法參數適用於Object,該類是任何Java對象的基類。以這種方式編寫是為了具有通用性。現在讓我們編寫一個測試程序來看看Java序列化的運作方式。
package com.journaldev.serialization;
import java.io.IOException;
public class SerializationTest {
public static void main(String[] args) {
String fileName="employee.ser";
Employee emp = new Employee();
emp.setId(100);
emp.setName("Pankaj");
emp.setSalary(5000);
// 序列化到文件
try {
SerializationUtil.serialize(emp, fileName);
} catch (IOException e) {
e.printStackTrace();
return;
}
Employee empNew = null;
try {
empNew = (Employee) SerializationUtil.deserialize(fileName);
} catch (ClassNotFoundException | IOException e) {
e.printStackTrace();
}
System.out.println("emp Object::"+emp);
System.out.println("empNew Object::"+empNew);
}
}
當我們在Java中運行上述序列化測試程序時,我們會得到以下輸出。
emp Object::Employee{name=Pankaj,id=100,salary=5000}
empNew Object::Employee{name=Pankaj,id=100,salary=0}
由於薪水是一個暫時變量,它的值未保存到文件中,因此未在新對象中檢索到。同樣,靜態 變量的值也未序列化,因為它們屬於類而不是對象。
使用序列化和serialVersionUID重構類
Java中的序列化允許對java類進行一些更改,如果可以忽略這些更改。不會影響反序列化過程的類中的一些更改包括:
- 向類添加新變量
- 將變量從暫時變為非暫時,對於序列化而言就像添加了一個新字段。
- 將變量從靜態變為非靜態,對於序列化而言就像添加了一個新字段。
但是,要使所有这些更改生效,Java类应该为该类定义serialVersionUID。让我们编写一个测试类,专门用于反序列化先前测试类中已序列化的文件。
package com.journaldev.serialization;
import java.io.IOException;
public class DeserializationTest {
public static void main(String[] args) {
String fileName="employee.ser";
Employee empNew = null;
try {
empNew = (Employee) SerializationUtil.deserialize(fileName);
} catch (ClassNotFoundException | IOException e) {
e.printStackTrace();
}
System.out.println("empNew Object::"+empNew);
}
}
现在取消注释Employee类中的“password”变量及其getter-setter方法并运行它。您将会得到以下异常:
java.io.InvalidClassException: com.journaldev.serialization.Employee; local class incompatible: stream classdesc serialVersionUID = -6470090944414208496, local class serialVersionUID = -6234198221249432383
at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:604)
at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1601)
at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1514)
at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1750)
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1347)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:369)
at com.journaldev.serialization.SerializationUtil.deserialize(SerializationUtil.java:22)
at com.journaldev.serialization.DeserializationTest.main(DeserializationTest.java:13)
empNew Object::null
原因很明显,先前类和新类的serialVersionUID是不同的。实际上,如果类没有定义serialVersionUID,它会自动计算并分配给该类。Java使用类变量、方法、类名、包等生成这个唯一的长数字。如果您使用任何IDE,您将自动收到一个警告,指出“可序列化类Employee未声明一个类型为long的静态final serialVersionUID字段”。我们可以使用Java工具“serialver”生成类的serialVersionUID,例如,对于Employee类,我们可以使用下面的命令运行它。
SerializationExample/bin$serialver -classpath . com.journaldev.serialization.Employee
请注意,不需要从此程序自动生成序列版本号,我们可以根据需要分配该值。它只需要存在,以便让反序列化过程知道新类是同一类的新版本,并应该进行可能的反序列化。例如,只取消注释Employee
类中的serialVersionUID字段,并运行SerializationTest
程序。现在取消注释Employee类中的password字段并运行DeserializationTest
程序,您将看到对象流成功反序列化,因为Employee类的更改与序列化过程兼容。
Java Externalizable Interface
如果注意Java序列化过程,它是自动完成的。有时,我们希望模糊化对象数据以保持其完整性。我们可以通过实现java.io.Externalizable
接口并提供writeExternal()和readExternal()方法的实现来实现这一点,这将在序列化过程中使用。
package com.journaldev.externalization;
import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
public class Person implements Externalizable{
private int id;
private String name;
private String gender;
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeInt(id);
out.writeObject(name+"xyz");
out.writeObject("abc"+gender);
}
@Override
public void readExternal(ObjectInput in) throws IOException,
ClassNotFoundException {
id=in.readInt();
//按写入的顺序读取
name=(String) in.readObject();
if(!name.endsWith("xyz")) throw new IOException("corrupted data");
name=name.substring(0, name.length()-3);
gender=(String) in.readObject();
if(!gender.startsWith("abc")) throw new IOException("corrupted data");
gender=gender.substring(3);
}
@Override
public String toString(){
return "Person{id="+id+",name="+name+",gender="+gender+"}";
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getGender() {
return gender;
}
public void setGender(String gender) {
this.gender = gender;
}
}
请注意,在将其转换为流之前,我已更改了字段的值,然后在读取时撤销了这些更改。通过这种方式,我们可以保持某种程度的数据完整性。如果在读取流数据后完整性检查失败,我们可以抛出异常。让我们编写一个测试程序来看看它的运行情况。
package com.journaldev.externalization;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
public class ExternalizationTest {
public static void main(String[] args) {
String fileName = "person.ser";
Person person = new Person();
person.setId(1);
person.setName("Pankaj");
person.setGender("Male");
try {
FileOutputStream fos = new FileOutputStream(fileName);
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(person);
oos.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
FileInputStream fis;
try {
fis = new FileInputStream(fileName);
ObjectInputStream ois = new ObjectInputStream(fis);
Person p = (Person)ois.readObject();
ois.close();
System.out.println("Person Object Read="+p);
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
当我们运行上面的程序时,我们得到以下输出。
Person Object Read=Person{id=1,name=Pankaj,gender=Male}
那么在Java中,使用哪种序列化方式更好呢?实际上,最好使用Serializable接口,当我们阅读完本文末尾时,您将会明白为什么。
Java序列化方法
我们发现在Java中,序列化是自动的,我们只需实现Serializable接口。该实现存在于ObjectInputStream和ObjectOutputStream类中。但是,如果我们想要更改保存数据的方式,例如在对象中有一些敏感信息,并且在保存/检索之前我们想要对其进行加密/解密,那么我们可以在类中提供四种方法来更改序列化行为。如果这些方法存在于类中,它们将用于序列化目的。
- readObject(ObjectInputStream ois):如果类中存在此方法,则ObjectInputStream readObject()方法将使用此方法从流中读取对象。
- writeObject(ObjectOutputStream oos):如果类中存在此方法,则ObjectOutputStream writeObject()方法将使用此方法将对象写入流。其中一种常见用法是混淆对象变量以维护数据完整性。
- Object writeReplace():如果存在此方法,则在序列化过程之后将调用此方法,并将返回的对象序列化到流中。
- Object readResolve():如果存在此方法,则在反序列化过程之后将调用此方法,以将最终对象返回给调用程序。此方法的一种用法是在序列化类中实现Singleton模式。在Serialization and Singleton中了解更多。
通常在實現以上方法時,它們被保持為私有,以便子類無法覆蓋它們。它們僅用於序列化目的,將它們保持為私有可以避免任何安全問題。
繼承序列化
有時我們需要擴展一個未實現Serializable接口的類。如果我們依賴於自動序列化行為並且超類具有一些狀態,那麼它們將不會轉換為流,因此稍後將無法檢索。這是一個地方,readObject()和writeObject()方法確實很有幫助。通過提供它們的實現,我們可以將超類狀態保存到流中,然後稍後檢索它。讓我們看看這在實際操作中是如何工作的。
package com.journaldev.serialization.inheritance;
public class SuperClass {
private int id;
private String value;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
}
SuperClass是一個簡單的Java bean,但它沒有實現Serializable接口。
package com.journaldev.serialization.inheritance;
import java.io.IOException;
import java.io.InvalidObjectException;
import java.io.ObjectInputStream;
import java.io.ObjectInputValidation;
import java.io.ObjectOutputStream;
import java.io.Serializable;
public class SubClass extends SuperClass implements Serializable, ObjectInputValidation{
private static final long serialVersionUID = -1322322139926390329L;
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString(){
return "SubClass{id="+getId()+",value="+getValue()+",name="+getName()+"}";
}
//添加用於序列化的輔助方法以保存/初始化超類狀態
private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException{
ois.defaultReadObject();
//請注意,讀和寫的順序應該相同
setId(ois.readInt());
setValue((String) ois.readObject());
}
private void writeObject(ObjectOutputStream oos) throws IOException{
oos.defaultWriteObject();
oos.writeInt(getId());
oos.writeObject(getValue());
}
@Override
public void validateObject() throws InvalidObjectException {
//在這裡驗證對象
if(name == null || "".equals(name)) throw new InvalidObjectException("name can't be null or empty");
if(getId() <=0) throw new InvalidObjectException("ID can't be negative or zero");
}
}
請注意寫入和讀取額外數據到流的順序應該保持一致。我們可以在讀取和寫入數據時添加一些邏輯以確保安全。同時注意,該類實現了 ObjectInputValidation
接口。通過實現 validateObject() 方法,我們可以添加一些業務驗證以確保數據的完整性不受損。讓我們編寫一個測試類,看看我們是否可以從序列化數據中檢索到超類狀態。
package com.journaldev.serialization.inheritance;
import java.io.IOException;
import com.journaldev.serialization.SerializationUtil;
public class InheritanceSerializationTest {
public static void main(String[] args) {
String fileName = "subclass.ser";
SubClass subClass = new SubClass();
subClass.setId(10);
subClass.setValue("Data");
subClass.setName("Pankaj");
try {
SerializationUtil.serialize(subClass, fileName);
} catch (IOException e) {
e.printStackTrace();
return;
}
try {
SubClass subNew = (SubClass) SerializationUtil.deserialize(fileName);
System.out.println("SubClass read = "+subNew);
} catch (ClassNotFoundException | IOException e) {
e.printStackTrace();
}
}
}
當我們運行上面的類時,我們得到以下輸出。
SubClass read = SubClass{id=10,value=Data,name=Pankaj}
因此,通過這種方式,我們可以序列化超類狀態,即使它沒有實現 Serializable 接口。當超類是我們無法更改的第三方類時,這種策略很方便。
序列化代理模式
在 Java 中進行序列化存在一些嚴重的缺陷,例如;
- 類結構不能太多地更改,否則會破壞 Java 序列化過程。因此,即使我們以後不需要某些變量,我們也需要保留它們以確保向後兼容性。
- 序列化會帶來巨大的安全風險,攻擊者可以更改流序列並對系統造成損害。例如,用戶角色被序列化,攻擊者更改流值以使其成為管理員並運行惡意代碼。
Java 序列化代理模式是一种实现更高安全性的序列化方法。在这种模式中,一个内部私有的静态类被用作序列化目的的代理类。这个类的设计方式是为了维护主类的状态。这个模式通过适当实现 readResolve() 和 writeReplace() 方法来实现。让我们首先编写一个实现序列化代理模式的类,然后我们会进行分析以便更好地理解。
package com.journaldev.serialization.proxy;
import java.io.InvalidObjectException;
import java.io.ObjectInputStream;
import java.io.Serializable;
public class Data implements Serializable{
private static final long serialVersionUID = 2087368867376448459L;
private String data;
public Data(String d){
this.data=d;
}
public String getData() {
return data;
}
public void setData(String data) {
this.data = data;
}
@Override
public String toString(){
return "Data{data="+data+"}";
}
// 序列化代理类
private static class DataProxy implements Serializable{
private static final long serialVersionUID = 8333905273185436744L;
private String dataProxy;
private static final String PREFIX = "ABC";
private static final String SUFFIX = "DEFG";
public DataProxy(Data d){
// 为了安全性而对数据进行混淆
this.dataProxy = PREFIX + d.data + SUFFIX;
}
private Object readResolve() throws InvalidObjectException {
if(dataProxy.startsWith(PREFIX) && dataProxy.endsWith(SUFFIX)){
return new Data(dataProxy.substring(3, dataProxy.length() -4));
}else throw new InvalidObjectException("data corrupted");
}
}
// 将序列化对象替换为 DataProxy 对象
private Object writeReplace(){
return new DataProxy(this);
}
private void readObject(ObjectInputStream ois) throws InvalidObjectException{
throw new InvalidObjectException("Proxy is not used, something fishy");
}
}
-
Data
和DataProxy
类都应该实现 Serializable 接口。 DataProxy
应该能够维护 Data 对象的状态。DataProxy
是内部私有的静态类,所以其他类无法访问它。DataProxy
应该有一个接受 Data 作为参数的单一构造函数。Data
类应该提供 writeReplace() 方法,返回DataProxy
实例。因此,当 Data 对象被序列化时,返回的流是 DataProxy 类的。然而,DataProxy 类在外部不可见,所以不能直接使用。DataProxy
类应该实现 readResolve() 方法,返回Data
对象。因此,当 Data 类被反序列化时,内部会反序列化 DataProxy,并在调用其 readResolve() 方法时获得 Data 对象。- 最後在Data類別中實現readObject()方法,並拋出
InvalidObjectException
以避免黑客攻擊試圖製造Data物件流並解析它。
讓我們編寫一個小測試來檢查實作是否有效。
package com.journaldev.serialization.proxy;
import java.io.IOException;
import com.journaldev.serialization.SerializationUtil;
public class SerializationProxyTest {
public static void main(String[] args) {
String fileName = "data.ser";
Data data = new Data("Pankaj");
try {
SerializationUtil.serialize(data, fileName);
} catch (IOException e) {
e.printStackTrace();
}
try {
Data newData = (Data) SerializationUtil.deserialize(fileName);
System.out.println(newData);
} catch (ClassNotFoundException | IOException e) {
e.printStackTrace();
}
}
}
當我們運行上述類別時,在控制台中會得到以下輸出。
Data{data=Pankaj}
如果你打開data.ser文件,你會看到DataProxy物件以流的形式保存在文件中。
關於Java序列化就是這些,看起來很簡單,但我們應該明智地使用它,最好不要依賴默認實現。從上面的鏈接下載項目並進行更多操作以學習更多。
Source:
https://www.digitalocean.com/community/tutorials/serialization-in-java