Java中的序列化是在 JDK 1.1 中引入的,它是Core Java的重要特性之一。
Java中的序列化
Java 中的序列化允许我们将对象转换为流,我们可以通过网络发送或将其保存为文件或存储在数据库中以备后用。反序列化是将 Object 流转换为我们程序中要使用的实际 Java Object 的过程。Java 中的序列化起初似乎很容易使用,但它带来了一些微不足道的安全性和完整性问题,我们将在本文的后面部分讨论这些问题。我们将在本教程中研究以下主题。
在 Java 中可序列化
如果您希望类对象可序列化,您只需实现java.io.Serializable
接口即可。Java 中的 Serializable 是一个标记接口,没有要实现的字段或方法。这就像一个 Opt-In 过程,通过它我们可以使我们的类可序列化。
java 中的序列化是由ObjectInputStream
and实现的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;
// }
}
请注意,它是一个简单的 java bean,具有一些属性和 getter-setter 方法。如果您希望对象属性不被序列化为流,您可以像我对salary 变量所做的那样使用transient关键字。
现在假设我们想将我们的对象写入文件,然后从同一个文件中反序列化它。因此,我们需要将使用的实用方法ObjectInputStream
,并ObjectOutputStream
进行序列化的目的。
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 一起使用,Object 是任何 java 对象的基类。它以这种方式编写,本质上是通用的。
现在让我们编写一个测试程序来查看 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);
//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}
由于salary 是一个瞬态变量,它的值没有保存到文件中,因此没有在新对象中检索。同样,静态变量值也没有序列化,因为它们属于类而不是对象。
使用序列化和 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类中password字段的注释并运行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 类中。但是如果我们想改变我们保存数据的方式,例如我们在对象中有一些敏感信息并且在保存/检索之前我们想要加密/解密它,该怎么办。这就是为什么我们可以在类中提供四种方法来更改序列化行为。
如果这些方法存在于类中,它们将用于序列化目的。
- readObject(ObjectInputStream ois):如果类中存在此方法,则 ObjectInputStream readObject() 方法将使用此方法从流中读取对象。
- writeObject(ObjectOutputStream oos):如果类中存在此方法,则 ObjectOutputStream writeObject() 方法将使用此方法将对象写入流。一种常见的用法是隐藏对象变量以保持数据完整性。
- Object writeReplace():如果存在此方法,则在序列化过程后调用此方法并将返回的对象序列化到流中。
- Object readResolve():如果存在此方法,则在反序列化过程后,将调用此方法将最终对象返回给调用程序。此方法的用途之一是使用序列化类实现单例模式。
通常在实现上述方法时,它保持私有,以便子类不能覆盖它们。它们仅用于序列化目的,将它们保密可避免任何安全问题。
继承序列化
有时我们需要扩展一个没有实现 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 序列化代理模式是一种通过序列化实现更高安全性的方法。在此模式中,内部私有静态类用作序列化目的的代理类。这个类的设计方式是维护主类的状态。这种模式是通过正确实现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");
}
}
- 无论
Data
和DataProxy
类应该实现Serializable接口。 DataProxy
应该能够维护数据对象的状态。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 中的序列化的全部内容,它看起来很简单,但我们应该明智地使用它,最好不要依赖默认实现。