Java中的序列化 – Java序列化

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

Java中的序列化

在Java中的序列化可以将一个对象转化为流,然后我们可以通过网络发送、保存为文件或存储在数据库中以便后续使用。反序列化则是将对象流转化为实际的Java对象,以供程序使用。起初,Java中的序列化似乎很容易使用,但它也存在一些细微的安全和完整性问题,我们将在本文的后半部分进行讨论。本教程将涵盖以下主题。

    在Java中可序列化
    利用序列化和serialVersionUID进行类重构
    Java的Externalizable接口
    Java的序列化方法
    继承中的序列化
    序列化代理模式

在Java中可序列化

如果你想让一个类对象可序列化,你只需要实现java.io.Serializable接口。在Java中,Serializable是一个标记接口,没有需要实现的字段或方法。它类似于一个Opt-In过程,通过它使我们的类可序列化。Java中的序列化是由ObjectInputStream和ObjectOutputStream实现的,所以我们只需要一个对它们的包装器,以便将其保存到文件或通过网络发送。让我们来看一个简单的Java序列化程序示例。

package com.Olivia.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,它包含一些属性和获取器-设置器方法。如果你想让一个对象属性不被序列化到流中,你可以使用transient关键字,就像我在salary变量中所做的一样。现在假设我们希望将我们的对象写入文件,然后从同一文件中进行反序列化。因此,我们需要使用ObjectInputStream和ObjectOutputStream进行序列化的实用方法。

package com.Olivia.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 {

	// 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();
	}

}

请注意,方法参数使用的是任何java对象的基类Object。这样编写的目的是为了具有通用性。现在让我们编写一个测试程序来看看Java序列化的工作原理。

package com.Olivia.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);
		
		//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=Pankaj,id=100,salary=5000}
empNew Object::Employee{name=Pankaj,id=100,salary=0}

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

使用序列化和serialVersionUID重构类

如果可以被忽略的话,在Java中进行序列化允许对Java类进行一些改动。其中那些不会影响反序列化过程的类的改动有:

  • Adding new variables to the class
  • Changing the variables from transient to non-transient, for serialization it’s like having a new field.
  • Changing the variable from static to non-static, for serialization it’s like having a new field.

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

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

原因很明确,以前的类和新类的serialVersionUID是不同的。实际上,如果类没有定义serialVersionUID,它会被自动计算并分配给该类。Java使用类变量、方法、类名、包等来生成这个唯一的长数字。如果你在使用任何集成开发环境,你将自动收到一个警告:“可序列化的类Employee没有声明一个类型为long的静态final serialVersionUID字段”。我们可以使用Java工具“serialver”来生成类的serialVersionUID,对于Employee类,可以使用以下命令运行它。

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

请注意,不一定要从该程序本身生成序列版本,我们可以根据需要指定该值。它只需要在那里,以让反序列化过程知道新类是同一类的新版本,并且应该进行反序列化处理。例如,取消注释Employee类中的serialVersionUID字段,并运行SerializationTest程序。现在取消注释Employee类中的password字段,并运行DeserializationTest程序,您将看到对象流成功反序列化,因为Employee类的更改与序列化过程兼容。

Java的Externalizable接口

如果你注意到Java的序列化过程,它是自动完成的。有时候,我们想要隐藏对象数据以保持其完整性。我们可以通过实现java.io.Externalizable接口,并提供writeExternal()和readExternal()方法的实现来实现这一点,以用于序列化过程。

package com.Olivia.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;
	}

}

请注意,在将其转换为流之前,我已经改变了字段的值,然后在读取时将更改还原。通过这种方式,我们可以保持某种形式的数据完整性。如果在读取流数据后完整性检查失败,我们可以抛出异常。让我们编写一个测试程序来观察它的工作情况。

package com.Olivia.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模式。详细了解序列化和Singleton模式,请阅读更多信息。

通常在实施上述方法时,它们通常被保留为私有,以防止子类对其进行覆盖。它们仅用于序列化目的,保持私有可避免任何安全问题。

继承实现的序列化

有时我们需要扩展一个没有实现Serializable接口的类。如果我们依赖于自动序列化的行为,并且超类有一些状态,那么它们将不会转换为流,并且后续无法检索。这是readObject()和writeObject()方法真正有作用的地方。通过提供它们的实现,我们可以将超类的状态保存到流中,然后在以后检索它。让我们看看这个实例。

package com.Olivia.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.Olivia.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.Olivia.serialization.inheritance;

import java.io.IOException;

import com.Olivia.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中,序列化存在一些严重的陷阱,比如;

  • The class structure can’t be changed a lot without breaking the java serialization process. So even though we don’t need some variables later on, we need to keep them just for backward compatibility.
  • Serialization causes huge security risks, an attacker can change the stream sequence and cause harm to the system. For example, user role is serialized and an attacker change the stream value to make it admin and run malicious code.

Java序列化代理模式是一种通过序列化实现更高安全性的方法。在这种模式中,一个内部私有静态类被用作序列化目的的代理类。该类的设计方式是为了维护主类的状态。通过正确实现readResolve()和writeReplace()方法来实现该模式。让我们首先编写一个实现序列化代理模式的类,然后我们将对其进行分析以便更好地理解。

package com.Olivia.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");
	}
}
  • Both Data and DataProxy class should implement Serializable interface.
  • DataProxy should be able to maintain the state of Data object.
  • DataProxy is inner private static class, so that other classes can’t access it.
  • DataProxy should have a single constructor that takes Data as argument.
  • Data class should provide writeReplace() method returning DataProxy instance. So when Data object is serialized, the returned stream is of DataProxy class. However DataProxy class is not visible outside, so it can’t be used directly.
  • DataProxy class should implement readResolve() method returning Data object. So when Data class is deserialized, internally DataProxy is deserialized and when it’s readResolve() method is called, we get Data object.
  • Finally implement readObject() method in Data class and throw InvalidObjectException to avoid hackers attack trying to fabricate Data object stream and parse it.

我们来写一个小测试来检查实现是否正常工作。

package com.Olivia.serialization.proxy;

import java.io.IOException;

import com.Olivia.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对象被保存为流在文件中。