Java中的序列化是在JDK 1.1中引入的,它是核心Java的重要特性之一。
Java中的序列化
Java中的序列化允许我们将对象转换为流,以便通过网络发送,保存为文件或存储在数据库中供以后使用。反序列化是将对象流转换为实际Java对象的过程,以在程序中使用。起初,Java中的序列化似乎很容易使用,但它带有一些微不足道的安全性和完整性问题,我们将在本文的后部分进行讨论。在本教程中,我们将探讨以下主题。
Java中的可序列化
如果你想让一个类对象变得可序列化,你只需要实现java.io.Serializable
接口。在Java中,可序列化是一个标记接口,没有需要实现的字段或方法。这就像一个选择性流程,通过它我们使我们的类可序列化。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和setter方法
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关键字,就像我在salary变量上所做的那样。现在假设我们想将对象写入文件,然后从同一文件反序列化它。因此,我们需要使用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 {
// 从给定文件反序列化为对象
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;
}
// 将给定对象序列化并保存到文件
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.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);
// 序列化到文件
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}
由于薪水是一个transient变量,其值未保存到文件中,因此在新对象中未检索到。类似地,static变量值也未序列化,因为它们属于类而不是对象。
使用序列化和serialVersionUID进行类重构
Java中的序列化允许对Java类进行一些更改,如果可以忽略这些更改。一些不会影响反序列化过程的类中的更改包括:
- 向类中添加新变量
- 将变量从transient更改为non-transient,对于序列化来说就像有了一个新字段。
- 将变量从static更改为non-static,对于序列化来说就像有了一个新字段。
但要使所有这些更改生效,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);
}
}
现在取消注释Employee类中的“password”变量以及其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未声明静态final serialVersionUID字段,类型为long”。我们可以使用Java实用程序“serialver”生成类的serialVersionUID,对于Employee类,我们可以使用以下命令运行它。
SerializationExample/bin$serialver -classpath . com.journaldev.serialization.Employee
请注意,不一定要从此程序生成serial版本,我们可以根据需要分配此值。它只需存在,以便让反序列化过程知道新类是相同类的新版本,并且应尽可能反序列化。例如,仅取消注释Employee
类中的serialVersionUID字段,然后运行SerializationTest
程序。现在取消注释Employee类中的密码字段,并运行DeserializationTest
程序,您将看到对象流成功反序列化,因为对Employee类的更改与序列化过程兼容。
Java Externalizable接口
如果您注意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();
//按写入顺序读取
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.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自动生成的catch块
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模式。阅读更多:Serialization and 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()+"}";
}
//为序列化添加辅助方法以保存/初始化超类状态
private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException{
ois.defaultReadObject();
//注意读和写的顺序应该相同
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 {
//在这里验证对象
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+"}";
}
//序列化代理类
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){
//为了安全性而对数据进行混淆
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");
}
}
//将序列化对象替换为DataProxy对象
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.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中的序列化就介绍到这里,看起来很简单,但我们应该明智地使用它,最好不要依赖默认实现。从上面的链接下载项目并尽情探索,以获取更多知识。
Source:
https://www.digitalocean.com/community/tutorials/serialization-in-java