Java中的序列化 – Java序列化

Java中的序列化是在JDK 1.1中引入的,它是核心Java的重要特性之一。

Java中的序列化

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

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

Java中的可序列化

如果你想让一个类对象变得可序列化,你只需要实现java.io.Serializable接口。在Java中,可序列化是一个标记接口,没有需要实现的字段或方法。这就像一个选择性流程,通过它我们使我们的类可序列化。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;
//	}
	
}

请注意,这是一个带有一些属性和getter-setter方法的简单Java bean。如果你不希望对象属性被序列化到流中,你可以使用transient关键字,就像我在salary变量上所做的那样。现在假设我们想将对象写入文件,然后从同一文件反序列化它。因此,我们需要使用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();
	}

}

请注意,方法参数使用的是任何Java对象的基类Object。它以这种方式编写是为了具有通用性。现在让我们编写一个测试程序来看看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}

由于薪水是一个transient变量,其值未保存到文件中,因此在新对象中未检索到。类似地,static变量值也未序列化,因为它们属于类而不是对象。

使用序列化和serialVersionUID进行类重构

Java中的序列化允许对Java类进行一些更改,如果可以忽略这些更改。一些不会影响反序列化过程的类中的更改包括:

  • 向类中添加新变量
  • 将变量从transient更改为non-transient,对于序列化来说就像有了一个新字段。
  • 将变量从static更改为non-static,对于序列化来说就像有了一个新字段。

但要使所有这些更改生效,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未声明静态final serialVersionUID字段,类型为long”。我们可以使用Java实用程序“serialver”生成类的serialVersionUID,对于Employee类,我们可以使用以下命令运行它。

SerializationExample/bin$serialver -classpath . com.journaldev.serialization.Employee

请注意,不一定要从此程序生成serial版本,我们可以根据需要分配此值。它只需存在,以便让反序列化过程知道新类是相同类的新版本,并且应尽可能反序列化。例如,仅取消注释Employee类中的serialVersionUID字段,然后运行SerializationTest程序。现在取消注释Employee类中的密码字段,并运行DeserializationTest程序,您将看到对象流成功反序列化,因为对Employee类的更改与序列化过程兼容。

Java Externalizable接口

如果您注意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自动生成的catch块
			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