手记

Java中的序列化 - Java序列化


Java中的序列化 - Java序列化


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


Java中的序列化

Java中的序列化允许我们将Object转换为可以通过网络发送的流,或者将其保存为文件或存储在DB中以供以后使用。反序列化是将对象流转换为在我们的程序中使用的实际Java对象的过程。Java中的序列化起初看起来非常容易使用,但它带来了一些简单的安全性和完整性问题,我们将在本文的后面部分介绍这些问题。我们将在本教程中研究以下主题。

  1. 可在Java中序列化

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

  3. Java可外化接口

  4. Java序列化方法

  5. 继承序列化

  6. 序列化代理模式

可在Java中序列化

如果您希望类对象可序列化,那么您只需要实现该java.io.Serializable接口即可。java中的Serializable是一个标记接口,没有要实现的字段或方法。这就像一个Opt-In过程,通过它我们可以将类序列化。

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

请注意,它是一个带有一些属性和getter-setter方法的简单java bean。如果您希望将对象属性序列化为流,则可以像使用薪水变量一样使用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 {
	// 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,它是任何java对象的基类。它以这种方式编写,本质上是通用的。

现在让我们编写一个测试程序来查看Java Serialization的实际运行情况。

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);
		
		//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类中进行一些更改。类中的一些不会影响反序列化过程的更改是:

  • 在类中添加新变量

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

  • 将变量从静态更改为非静态,对于序列化,就像拥有一个新字段一样。

但是要使所有这些更改生效,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);
		
	}}

现在取消注释“password”变量及其来自Employee类的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类型的静态最终serialVersionUID字段”的警告。

我们可以使用java实用程序“serialver”生成类serialVersionUID,对于Employee类,我们可以使用下面的命令运行它。

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

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

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

Java可外化接口

如果你注意到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();
		//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.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和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()+"}";
	}
	
	//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.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 Serialization Proxy模式是一种通过Serialization实现更高安全性的方法。在此模式中,内部私有静态类用作序列化目的的代理类。此类的设计方式是维护主类的状态。通过正确实现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+"}";
	}
	
	//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");
	}}
  • 两者DataDataProxy类都应该实现Serializable接口。

  • DataProxy 应该能够维护Data对象的状态。

  • DataProxy 是内部私有静态类,因此其他类无法访问它。

  • DataProxy 应该有一个将Data作为参数的构造函数。

  • Dataclass应该提供writeReplace()方法返回DataProxy实例。因此,当序列化Data对象时,返回的流是DataProxy类。但是DataProxy类在外部不可见,因此无法直接使用。

  • DataProxyclass应该实现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中的序列化,它看起来很简单,但我们应该明智地使用它,最好不要依赖默认实现。从上面的链接下载项目并使用它来了解更多信息。



1人推荐
随时随地看视频
慕课网APP