Java序列化- Java序列化

Java中的序列化是在JDK 1.1中引入的,也是Core Java的重要功能之一。

Java中的序列化

Java中的序列化允许我们将对象转换为流,以便通过网络发送,保存为文件或在后续使用中存储在数据库中。反序列化是将对象流转换为实际Java对象以在程序中使用的过程。起初,Java中的序列化似乎很容易使用,但它却带有一些微不足道的安全性和完整性问题,我们将在本文的后部分进行讨论。在本教程中,我们将研究以下主题。

  1. Java中的可序列化
  2. 使用序列化和serialVersionUID的类重构
  3. Java Externalizable接口
  4. Java序列化方法
  5. 继承中的序列化
  6. 序列化代理模式

Java 中的可序列化

如果你想讓一個類別物件可序列化,你只需要實作 java.io.Serializable 介面。在 Java 中,Serializable 是一個標記介面,沒有任何需要實作的欄位或方法。這就像是一個選擇性參與的過程,通過它我們可以使我們的類別可序列化。在 Java 中,序列化是通過 ObjectInputStreamObjectOutputStream 實現的,所以我們只需要一個封裝器來將它們保存到文件或者發送到網絡上。讓我們來看一個簡單的 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 關鍵字,就像我用薪水變數做的一樣。現在假設我們想將我們的物件寫入文件,然後從同一個文件反序列化它。因此,我們需要使用 ObjectInputStreamObjectOutputStream 進行序列化的實用方法。

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类中。但是,如果我们想要更改保存数据的方式,例如在对象中有一些敏感信息,并且在保存/检索之前我们想要对其进行加密/解密,那么我们可以在类中提供四种方法来更改序列化行为。如果这些方法存在于类中,它们将用于序列化目的。

  1. readObject(ObjectInputStream ois):如果类中存在此方法,则ObjectInputStream readObject()方法将使用此方法从流中读取对象。
  2. writeObject(ObjectOutputStream oos):如果类中存在此方法,则ObjectOutputStream writeObject()方法将使用此方法将对象写入流。其中一种常见用法是混淆对象变量以维护数据完整性。
  3. Object writeReplace():如果存在此方法,则在序列化过程之后将调用此方法,并将返回的对象序列化到流中。
  4. 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");
	}
}
  • DataDataProxy 类都应该实现 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序列化專案

關於Java序列化就是這些,看起來很簡單,但我們應該明智地使用它,最好不要依賴默認實現。從上面的鏈接下載項目並進行更多操作以學習更多。

Source:
https://www.digitalocean.com/community/tutorials/serialization-in-java