Java序列化– Java序列化

时间:2020-02-23 14:37:25  来源:igfitidea点击:

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

Java序列化

Java中的序列化允许我们将对象转换为流,可以通过网络发送该流,也可以将其保存为文件或者存储在DB中以备后用。
反序列化是将对象流转换为要在我们的程序中使用的实际Java对象的过程。
Java的序列化开始时似乎很容易使用,但是它带有一些琐碎的安全性和完整性问题,我们将在本文的后面部分介绍。
我们将在本教程中研究以下主题。

  • Java可序列化
  • 具有序列化和serialVersionUID的类重构
  • Java外部化接口
  • Java序列化方法
  • 继承序列化
  • 序列化代理模式

Java可序列化

如果您想让一个类对象可序列化,则只需执行一个实现java.io.Serializable接口即可。
Java中的可序列化是标记接口,没有可实现的字段或者方法。
就像选择加入过程一样,通过该过程我们可以使程序可序列化。

Java中的序列化是通过ObjectInputStream和ObjectOutputStream实现的,因此我们所需要的只是对它们的包装,以将其保存到文件或者通过网络发送。
让我们看一下Java程序示例中的简单序列化。

package com.theitroad.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 and setter methods
	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方法。
如果您希望不将对象属性序列化为流,则可以像我对薪水变量所做的那样使用瞬态关键字。

现在假设我们要将对象写入文件,然后从同一文件反序列化。
因此,我们需要实用程序方法,这些方法将使用" ObjectInputStream"和" ObjectOutputStream"进行序列化。

package com.theitroad.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 hyman
 * 
 */
public class SerializationUtil {

	//deserialize to Object from given file
	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;
	}

	//serialize the given object and save it to file
	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一起使用,Object是任何java对象的基类。
这种方式本质上是通用的。

现在,让我们编写一个测试程序,以查看实际的Java序列化。

package com.theitroad.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("hyman");
		emp.setSalary(5000);
		
		//serialize to file
		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=hyman,id=100,salary=5000}
empNew Object::Employee{name=hyman,id=100,salary=0}

由于薪金是一个临时变量,因此其值未保存到文件中,因此也没有在新对象中检索到。
同样,静态变量值也不会序列化,因为它们属于类而不是对象。

具有序列化和serialVersionUID的类重构

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

  • 向类添加新变量

  • 将变量从瞬态更改为非瞬态,进行序列化就像拥有一个新字段。

  • 将变量从静态更改为非静态,以便进行序列化,就像有一个新字段一样。

但是,为了使所有这些更改生效,java类应该为该类定义了serialVersionUID。
让我们编写一个测试类,仅用于反序列化先前测试类中已经序列化的文件。

package com.theitroad.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.theitroad.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.theitroad.serialization.SerializationUtil.deserialize(SerializationUtil.java:22)
	at com.theitroad.serialization.DeserializationTest.main(DeserializationTest.java:13)
empNew Object::null

原因很明显,上一类和新类的serialVersionUID不同。
实际上,如果该类未定义serialVersionUID,则会自动进行计算并分配给该类。
Java使用类变量,方法,类名,包等来生成此唯一的长号。
如果使用任何IDE,您将自动收到一条警告:"可序列化的类Employee没有声明类型为long的静态最终serialVersionUID字段"。

我们可以使用Java实用程序" serialver"来生成类serialVersionUID,对于Employee类,可以使用以下命令运行它。

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

请注意,不需要从程序本身生成串行版本,我们可以根据需要分配该值。
只需要在那里使反序列化过程知道新类是同一类的新版本,并应尽可能进行反序列化。

例如,仅取消注释" Employee"类中的serialVersionUID字段,然后运行" SerializationTest"程序。
现在,取消对Employee类的密码字段的注释,并运行DeserializationTest程序,您将看到对象流已成功反序列化,因为Employee类的更改与序列化过程兼容。

Java外部化接口

如果您注意到Java序列化过程,它会自动完成。
有时我们想遮掩对象数据以保持其完整性。
我们可以通过实现java.io.Externalizable接口来实现,并提供在序列化过程中使用的writeExternal()和readExternal()方法的实现。

package com.theitroad.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();
		//read in the same order as written
		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;
	}

}

请注意,在将其转换为Stream之前,然后在读取撤消更改之前,已经更改了字段值。
这样,我们可以维护某种数据完整性。
如果在读取流数据后完整性检查失败,则可以引发异常。
让我们编写一个测试程序以查看其运行情况。

package com.theitroad.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("hyman");
		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=hyman,gender=Male}

因此,哪个更好用于Java的序列化。
实际上,最好使用Serializable接口,等到本文结尾处,您就会知道原因。

Java序列化方法

我们已经看到Java中的序列化是自动的,我们所需要的只是实现Serializable接口。
该实现存在于ObjectInputStream和ObjectOutputStream类中。
但是,如果我们想更改保存数据的方式,例如,对象中有一些敏感信息,而在保存/检索之前,我们想要对其进行加密/解密,该怎么办?这就是为什么我们可以在类中提供四种方法来更改序列化行为的原因。

如果这些方法存在于类中,则将它们用于序列化目的。

  • readObject(ObjectInputStream ois):如果类中存在此方法,则ObjectInputStream readObject()方法将使用此方法从流中读取对象。

  • writeObject(ObjectOutputStream oos):如果类中存在此方法,则ObjectOutputStream writeObject()方法将使用此方法将对象写入要流式传输的对象。
    常见用法之一是模糊对象变量以保持数据完整性。

  • Object writeReplace():如果存在此方法,则在序列化过程之后,将调用此方法,并将返回的对象序列化到流中。

  • Object readResolve():如果存在此方法,则在反序列化过程之后,将调用此方法以将最终对象返回给调用程序。
    此方法的用途之一是使用序列化类实现Singleton模式。
    在序列化和单例中了解更多信息。

通常,在实现上述方法时,它会保持私有状态,以使子类无法覆盖它们。
它们仅用于序列化目的,并且将它们设为私有可以避免任何安全问题。

继承序列化

有时我们需要扩展一个没有实现Serializable接口的类。
如果我们依靠自动序列化行为,并且超类具有某种状态,则它们将不会转换为流,因此以后也不会检索。

这是readObject()和writeObject()方法真正有用的地方。
通过提供它们的实现,我们可以将超类状态保存到流中,然后在以后检索它。
让我们来看看实际情况。

package com.theitroad.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.theitroad.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()+"}";
	}
	
	//adding helper method for serialization to save/initialize super class state
	private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException{
		ois.defaultReadObject();
		
		//notice the order of read and write should be same
		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 {
		//validate the object here
		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.theitroad.serialization.inheritance;

import java.io.IOException;

import com.theitroad.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("hyman");
		
		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=hyman}

因此,即使没有实现Serializable接口,我们也可以对超类状态进行序列化。
当超级类是我们无法更改的第三方类时,此策略会派上用场。

序列化代理模式

Java中的序列化带有一些严重的陷阱,例如;

  • 在不中断Java序列化过程的情况下,无法对类结构进行很多更改。
    因此,即使以后不再需要某些变量,我们也需要保留它们只是为了向后兼容。

  • 序列化会带来巨大的安全风险,攻击者可能会更改流序列并损害系统。
    例如,用户角色被序列化,攻击者更改流值以使其成为admin并运行恶意代码。

Java序列化代理模式是一种通过序列化实现更高安全性的方法。
在这种模式下,内部私有静态类用作代理类以进行序列化。
以维护主类状态的方式设计此类。
通过正确实现readResolve()和writeReplace()方法来实现此模式。

让我们首先编写一个实现序列化代理模式的类,然后我们将对其进行分析以更好地理解。

package com.theitroad.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+"}";
	}
	
	//serialization proxy class
	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){
			//obscuring data for security
			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");
		}
		
	}
	
	//replacing serialized object to DataProxy object
	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.theitroad.serialization.proxy;

import java.io.IOException;

import com.theitroad.serialization.SerializationUtil;

public class SerializationProxyTest {

	public static void main(String[] args) {
		String fileName = "data.ser";
		
		Data data = new Data("hyman");
		
		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=hyman}

如果您将打开data.ser文件,则可以看到DataProxy对象已作为流保存在文件中。