Java 中的序列化 - Java 序列化

JDK 1.1 引入了 Java 中的序列化,它是 [Core Java] 的重要功能之一( / 社区 / 教程 / 核心 - java - 采访 - 问题 - 答案 "核心 Java 面试 - 问题和答案").

Java 中的序列化

serialization in java, java serialization, what is serialization in java, serializable in java Serialization in Java allows us to convert an Object to stream that we can send over the network or save it as file or store in DB for later usage. Deserialization is the process of converting Object stream to actual Java Object to be used in our program. Serialization in Java seems very easy to use at first but it comes with some trivial security and integrity issues that we will look in the later part of this article. We will look into following topics in this tutorial.

  1. 可在 Java 中进行序列化(#serializable)
  2. 可在 Java 中进行序列化(#serialVersionUID)
  3. 可在 Java 中进行外部化接口(#Externalizable-interface)
  4. 可在 Java 中进行序列化(#Serialization-Methods)
  5. 可在遗传中进行序列化(#Serialization-Inheritance)
  6. 可在 Java 中进行序列化(#Serialization-Proxy-Pattern)

可在Java中编辑

如果你想一个类对象是可 serializable 的,你所需要做的就是执行java.io.Serializable接口。在 java 中 Serializable 是一个标记接口,没有任何领域或方法来实现。 它就像一个 Opt-In 过程,通过它我们使我们的类可 serializable。 在 java 中 Serialization 是由ObjectInputStreamObjectOutputStream 实现的,所以我们所需要的只是一个包装器,可以将其保存到文件中或通过网络发送。

 1package com.journaldev.serialization;
 2
 3import java.io.Serializable;
 4
 5public class Employee implements Serializable {
 6
 7//	private static final long serialVersionUID = -6470090944414208496L;
 8    
 9    private String name;
10    private int id;
11    transient private int salary;
12//	private String password;
13    
14    @Override
15    public String toString(){
16    	return "Employee{name="+name+",id="+id+",salary="+salary+"}";
17    }
18    
19    //getter and setter methods
20    public String getName() {
21    	return name;
22    }
23
24    public void setName(String name) {
25    	this.name = name;
26    }
27
28    public int getId() {
29    	return id;
30    }
31
32    public void setId(int id) {
33    	this.id = id;
34    }
35
36    public int getSalary() {
37    	return salary;
38    }
39
40    public void setSalary(int salary) {
41    	this.salary = salary;
42    }
43
44//	public String getPassword() {
45//		return password;
46//	}
47//
48//	public void setPassword(String password) {
49//		this.password = password;
50//	}
51    
52}

请注意,它是一个简单的Java豆子,具有一些属性和getter-setter方法。如果您希望一个对象属性不会被序列化流,您可以使用 transient关键字,就像我用工资变量一样。

 1package com.journaldev.serialization;
 2
 3import java.io.FileInputStream;
 4import java.io.FileOutputStream;
 5import java.io.IOException;
 6import java.io.ObjectInputStream;
 7import java.io.ObjectOutputStream;
 8
 9/**
10 * A simple class with generic serialize and deserialize method implementations
11 * 
12 * @author pankaj
13 * 
14 */
15public class SerializationUtil {
16
17    // deserialize to Object from given file
18    public static Object deserialize(String fileName) throws IOException,
19    		ClassNotFoundException {
20    	FileInputStream fis = new FileInputStream(fileName);
21    	ObjectInputStream ois = new ObjectInputStream(fis);
22    	Object obj = ois.readObject();
23    	ois.close();
24    	return obj;
25    }
26
27    // serialize the given object and save it to file
28    public static void serialize(Object obj, String fileName)
29    		throws IOException {
30    	FileOutputStream fos = new FileOutputStream(fileName);
31    	ObjectOutputStream oos = new ObjectOutputStream(fos);
32    	oos.writeObject(obj);
33
34    	fos.close();
35    }
36
37}

请注意,方法论点与 Object 一起工作,它是任何 java 对象的基本类。它是这样写的,以便在本质上是通用的。

 1package com.journaldev.serialization;
 2
 3import java.io.IOException;
 4
 5public class SerializationTest {
 6    
 7    public static void main(String[] args) {
 8    	String fileName="employee.ser";
 9    	Employee emp = new Employee();
10    	emp.setId(100);
11    	emp.setName("Pankaj");
12    	emp.setSalary(5000);
13    	
14    	//serialize to file
15    	try {
16    		SerializationUtil.serialize(emp, fileName);
17    	} catch (IOException e) {
18    		e.printStackTrace();
19    		return;
20    	}
21    	
22    	Employee empNew = null;
23    	try {
24    		empNew = (Employee) SerializationUtil.deserialize(fileName);
25    	} catch (ClassNotFoundException | IOException e) {
26    		e.printStackTrace();
27    	}
28    	
29    	System.out.println("emp Object::"+emp);
30    	System.out.println("empNew Object::"+empNew);
31    }
32}

当我们在java中运行测试程序进行序列化时,我们会得到以下输出。

1emp Object::Employee{name=Pankaj,id=100,salary=5000}
2empNew Object::Employee{name=Pankaj,id=100,salary=0}

由于工资是一个暂时变量,所以它的值没有被保存到文件中,因此在新对象中没有被检索。

通过 Serialization 和 serialVersionUID 进行类重构

java 中的序列化允许对 java 类进行一些更改,如果可以忽略它们,在类中不会影响 deserialization 过程的一些更改是:

  • 将新的变量添加到类
  • 将变量从过渡到非过渡的变量更改,对于序列化,这就像有一个新的字段一样。

但是,要使所有这些变化工作,Java 类应该为该类定义了 **serialVersionUID。

 1package com.journaldev.serialization;
 2
 3import java.io.IOException;
 4
 5public class DeserializationTest {
 6
 7    public static void main(String[] args) {
 8
 9    	String fileName="employee.ser";
10    	Employee empNew = null;
11    	
12    	try {
13    		empNew = (Employee) SerializationUtil.deserialize(fileName);
14    	} catch (ClassNotFoundException | IOException e) {
15    		e.printStackTrace();
16    	}
17    	
18    	System.out.println("empNew Object::"+empNew);
19    	
20    }
21}

现在,不评论密码变量,它是从员工类别的getter-setter方法,然后运行它。

 1java.io.InvalidClassException: com.journaldev.serialization.Employee; local class incompatible: stream classdesc serialVersionUID = -6470090944414208496, local class serialVersionUID = -6234198221249432383
 2    at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:604)
 3    at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1601)
 4    at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1514)
 5    at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1750)
 6    at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1347)
 7    at java.io.ObjectInputStream.readObject(ObjectInputStream.java:369)
 8    at com.journaldev.serialization.SerializationUtil.deserialize(SerializationUtil.java:22)
 9    at com.journaldev.serialization.DeserializationTest.main(DeserializationTest.java:13)
10empNew Object::null

事实上,如果该类不定义 serialVersionUID,它会自动计算并分配给该类。Java使用类变量,方法,类名称,包等来生成这个独特的长数。如果您正在使用任何IDE,您将自动收到一个警告:可 serializable 类员工不会宣布一个静态的最终 serialVersionUID字段长

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

请注意,它不需要从这个程序本身生成的序列版本,我们可以根据我们的意愿分配这个值。它只需要在那里让 deserialization 流程知道,新类是同一类的新版本,应该被 deserialized 可能的。例如,单击员工类的 serialVersionUID 字段,并运行SerializationTest程序.现在,单击员工类的密码字段,运行DeserializationTest程序,你会看到对象流被成功 deserialized,因为员工类的变化与序列化过程兼容。

Java 外部化接口

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

 1package com.journaldev.externalization;
 2
 3import java.io.Externalizable;
 4import java.io.IOException;
 5import java.io.ObjectInput;
 6import java.io.ObjectOutput;
 7
 8public class Person implements Externalizable{
 9
10    private int id;
11    private String name;
12    private String gender;
13    
14    @Override
15    public void writeExternal(ObjectOutput out) throws IOException {
16    	out.writeInt(id);
17    	out.writeObject(name+"xyz");
18    	out.writeObject("abc"+gender);
19    }
20
21    @Override
22    public void readExternal(ObjectInput in) throws IOException,
23    		ClassNotFoundException {
24    	id=in.readInt();
25    	//read in the same order as written
26    	name=(String) in.readObject();
27    	if(!name.endsWith("xyz")) throw new IOException("corrupted data");
28    	name=name.substring(0, name.length()-3);
29    	gender=(String) in.readObject();
30    	if(!gender.startsWith("abc")) throw new IOException("corrupted data");
31    	gender=gender.substring(3);
32    }
33
34    @Override
35    public String toString(){
36    	return "Person{id="+id+",name="+name+",gender="+gender+"}";
37    }
38    public int getId() {
39    	return id;
40    }
41
42    public void setId(int id) {
43    	this.id = id;
44    }
45
46    public String getName() {
47    	return name;
48    }
49
50    public void setName(String name) {
51    	this.name = name;
52    }
53
54    public String getGender() {
55    	return gender;
56    }
57
58    public void setGender(String gender) {
59    	this.gender = gender;
60    }
61
62}

请注意,在将其转换为流之前,我已经更改了字段值,然后在阅读过程中扭转了更改。这样,我们可以保持某些类型的数据完整性。

 1package com.journaldev.externalization;
 2
 3import java.io.FileInputStream;
 4import java.io.FileOutputStream;
 5import java.io.IOException;
 6import java.io.ObjectInputStream;
 7import java.io.ObjectOutputStream;
 8
 9public class ExternalizationTest {
10
11    public static void main(String[] args) {
12    	
13    	String fileName = "person.ser";
14    	Person person = new Person();
15    	person.setId(1);
16    	person.setName("Pankaj");
17    	person.setGender("Male");
18    	
19    	try {
20    		FileOutputStream fos = new FileOutputStream(fileName);
21    		ObjectOutputStream oos = new ObjectOutputStream(fos);
22    	    oos.writeObject(person);
23    	    oos.close();
24    	} catch (IOException e) {
25    		// TODO Auto-generated catch block
26    		e.printStackTrace();
27    	}
28    	
29    	FileInputStream fis;
30    	try {
31    		fis = new FileInputStream(fileName);
32    		ObjectInputStream ois = new ObjectInputStream(fis);
33    	    Person p = (Person)ois.readObject();
34    	    ois.close();
35    	    System.out.println("Person Object Read="+p);
36    	} catch (IOException | ClassNotFoundException e) {
37    		e.printStackTrace();
38    	}
39        
40    }
41}

当我们在程序上运行时,我们会跟踪输出。

1Person Object Read=Person{id=1,name=Pankaj,gender=Male}

实际上,最好使用Serializable界面,到我们在文章结束时,你会知道为什么。

Java 序列化方法

我们已经看到 Java 中的序列化是自动的,我们所需要的只是实现 Serializable 接口。 实现在 ObjectInputStream 和 ObjectOutputStream 类中存在,但如果我们想要改变我们保存数据的方式,例如,我们在对象中有一些敏感信息,然后在保存/恢复之前,我们想要加密/解密它,这就是为什么我们可以在类中提供四种方法来改变序列化行为。

  1. readObject(ObjectInputStream ois):如果这种方法存在于类中,ObjectInputStream readObject()方法将使用这种方法来从流中读取对象
  2. writeObject(ObjectOutputStream oos):如果这种方法存在于类中,那么ObjectOutputStream writeObject()方法将使用这种方法来写对象以流。 一个常见的用途是掩盖对象变量以保持数据完整性
  3. Object writeReplace():如果这种方法存在,那么在序列化过程之后,这个方法被调用,而返回的对象被序列化到流( _4)。 **Object readResolve():如果这种方法存在,

通常在实施上述方法时,它被保留为私密,以便子类不能超过它们,它们仅用于序列化目的,并保持隐私避免任何安全问题。

继承的序列化

有时我们需要扩展一个不实现 Serializable 接口的类别。如果我们依靠自动序列化行为,而超级类有某种状态,那么它们将不会被转换为流,因此不会在以后恢复。

 1package com.journaldev.serialization.inheritance;
 2
 3public class SuperClass {
 4
 5    private int id;
 6    private String value;
 7    
 8    public int getId() {
 9    	return id;
10    }
11    public void setId(int id) {
12    	this.id = id;
13    }
14    public String getValue() {
15    	return value;
16    }
17    public void setValue(String value) {
18    	this.value = value;
19    }	
20}

SuperClass 是一个简单的 java 棒,但它没有实现 Serializable 界面。

 1package com.journaldev.serialization.inheritance;
 2
 3import java.io.IOException;
 4import java.io.InvalidObjectException;
 5import java.io.ObjectInputStream;
 6import java.io.ObjectInputValidation;
 7import java.io.ObjectOutputStream;
 8import java.io.Serializable;
 9
10public class SubClass extends SuperClass implements Serializable, ObjectInputValidation{
11
12    private static final long serialVersionUID = -1322322139926390329L;
13
14    private String name;
15
16    public String getName() {
17    	return name;
18    }
19
20    public void setName(String name) {
21    	this.name = name;
22    }
23    
24    @Override
25    public String toString(){
26    	return "SubClass{id="+getId()+",value="+getValue()+",name="+getName()+"}";
27    }
28    
29    //adding helper method for serialization to save/initialize super class state
30    private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException{
31    	ois.defaultReadObject();
32    	
33    	//notice the order of read and write should be same
34    	setId(ois.readInt());
35    	setValue((String) ois.readObject());	
36    }
37    
38    private void writeObject(ObjectOutputStream oos) throws IOException{
39    	oos.defaultWriteObject();
40    	
41    	oos.writeInt(getId());
42    	oos.writeObject(getValue());
43    }
44
45    @Override
46    public void validateObject() throws InvalidObjectException {
47    	//validate the object here
48    	if(name == null || "".equals(name)) throw new InvalidObjectException("name can't be null or empty");
49    	if(getId() <=0) throw new InvalidObjectException("ID can't be negative or zero");
50    }	
51}

请注意,写入和读取流中的额外数据的顺序应该是相同的。我们可以把一些逻辑放在读取和写入数据中,以使其安全。 另外,请注意,该类正在实施ObjectInputValidation接口。 通过实施 validateObject() 方法,我们可以放置一些业务验证来确保数据完整性没有受到损害。 让我们写一个测试类,看看我们是否可以从序列化数据中获取超级类状态。

 1package com.journaldev.serialization.inheritance;
 2
 3import java.io.IOException;
 4
 5import com.journaldev.serialization.SerializationUtil;
 6
 7public class InheritanceSerializationTest {
 8
 9    public static void main(String[] args) {
10    	String fileName = "subclass.ser";
11    	
12    	SubClass subClass = new SubClass();
13    	subClass.setId(10);
14    	subClass.setValue("Data");
15    	subClass.setName("Pankaj");
16    	
17    	try {
18    		SerializationUtil.serialize(subClass, fileName);
19    	} catch (IOException e) {
20    		e.printStackTrace();
21    		return;
22    	}
23    	
24    	try {
25    		SubClass subNew = (SubClass) SerializationUtil.deserialize(fileName);
26    		System.out.println("SubClass read = "+subNew);
27    	} catch (ClassNotFoundException | IOException e) {
28    		e.printStackTrace();
29    	}
30    }
31}

当我们跑高于班级时,我们会跟踪输出。

1SubClass read = SubClass{id=10,value=Data,name=Pankaj}

因此,通过这种方式,我们可以将超级阶级状态序列化,即使它没有实施可序列化接口,这种策略在超级阶级是第三方阶级时非常有用,我们无法改变。

序列化代理模式

java中的序列化伴随着一些严重的陷阱,如:

因此,即使我们不需要稍后一些变量,我们只需要为后向兼容而保留它们

  • Serialization 会造成巨大的安全风险,攻击者可以改变流序列并对系统造成损害。

Java Serialization Proxy 模式是通过 Serialization 实现更大的安全性的一种方式. 在这个模式中,一个内部的私有静态类被用作代理类用于 serialization 的目的。 这个类是为了保持主类的状态而设计的。 这个模式是通过正确地执行 readResolve()writeReplace() 方法来实现的。 让我们先写一个实现 serialization 代理模式的类,然后我们将对其进行分析,以便更好地理解。

 1package com.journaldev.serialization.proxy;
 2
 3import java.io.InvalidObjectException;
 4import java.io.ObjectInputStream;
 5import java.io.Serializable;
 6
 7public class Data implements Serializable{
 8
 9    private static final long serialVersionUID = 2087368867376448459L;
10
11    private String data;
12    
13    public Data(String d){
14    	this.data=d;
15    }
16
17    public String getData() {
18    	return data;
19    }
20
21    public void setData(String data) {
22    	this.data = data;
23    }
24    
25    @Override
26    public String toString(){
27    	return "Data{data="+data+"}";
28    }
29    
30    //serialization proxy class
31    private static class DataProxy implements Serializable{
32    
33    	private static final long serialVersionUID = 8333905273185436744L;
34    	
35    	private String dataProxy;
36    	private static final String PREFIX = "ABC";
37    	private static final String SUFFIX = "DEFG";
38    	
39    	public DataProxy(Data d){
40    		//obscuring data for security
41    		this.dataProxy = PREFIX + d.data + SUFFIX;
42    	}
43    	
44    	private Object readResolve() throws InvalidObjectException {
45    		if(dataProxy.startsWith(PREFIX) && dataProxy.endsWith(SUFFIX)){
46    		return new Data(dataProxy.substring(3, dataProxy.length() -4));
47    		}else throw new InvalidObjectException("data corrupted");
48    	}
49    	
50    }
51    
52    //replacing serialized object to DataProxy object
53    private Object writeReplace(){
54    	return new DataProxy(this);
55    }
56    
57    private void readObject(ObjectInputStream ois) throws InvalidObjectException{
58    	throw new InvalidObjectException("Proxy is not used, something fishy");
59    }
60}

DataProxy应该能够维持数据对象的状态 *DataProxy是内部私有静态类,所以其他类无法访问它 *DataProxy应该有一个单个构造器,将数据作为论点 *DataProxy类应该提供返回DataProxy实例的 _writeReplace() 方法。因此,当数据对象进行序列化时,返回的流程是DataProxy类。然而,DataProxy类外面是不可见的,所以它不能直接使用 *DataProxy类应该执行 _readResolve() 方法返回DataProxy对象。因此,当数据类被

让我们写一个小测试来检查实施是否有效。

 1package com.journaldev.serialization.proxy;
 2
 3import java.io.IOException;
 4
 5import com.journaldev.serialization.SerializationUtil;
 6
 7public class SerializationProxyTest {
 8
 9    public static void main(String[] args) {
10    	String fileName = "data.ser";
11    	
12    	Data data = new Data("Pankaj");
13    	
14    	try {
15    		SerializationUtil.serialize(data, fileName);
16    	} catch (IOException e) {
17    		e.printStackTrace();
18    	}
19    	
20    	try {
21    		Data newData = (Data) SerializationUtil.deserialize(fileName);
22    		System.out.println(newData);
23    	} catch (ClassNotFoundException | IOException e) {
24    		e.printStackTrace();
25    	}
26    }
27
28}

当我们在类上运行时,我们会在控制台上输出。

1Data{data=Pankaj}

如果您要打开 data.ser 文件,您可以看到 DataProxy 对象被保存为文件中的流。

下载Java Serialization Project

这就是Java中的Serialization,它看起来很简单,但我们应该明智地使用它,而且总是最好不要依赖默认实现。

Published At
Categories with 技术
Tagged with
comments powered by Disqus