一、简介
Java序列化是将对象转换为字节序列的过程,以便在网络上传输或保存到文件中。反序列化是将字节序列转换回对象的过程。它们是Java中非常重要的特性,可以帮助我们方便地将对象进行传输和保存,同时也是Java RMI(远程方法调用)的基础之一。
二、序列化实现
在Java中,我们可以将一个类序列化并保存到文件中,以便以后使用。这需要先实现 java.io.Serializable
接口,该接口没有任何方法,只是一个标记接口。实现了 java.io.Serializable
接口的类才能被序列化。接着就可以使用 java.io.ObjectOutputStream
对象的 writeObject()
方法将对象序列化为字节序列。
public class Student implements Serializable { private String name; private int age; private String address; public Student(String name, int age, String address) { this.name = name; this.age = age; this.address = address; } // getters and setters public static void main(String[] args) { Student student = new Student("张三", 18, "北京市"); try { FileOutputStream fos = new FileOutputStream("student.dat"); ObjectOutputStream oos = new ObjectOutputStream(fos); oos.writeObject(student); oos.close(); fos.close(); } catch (IOException e) { e.printStackTrace(); } } }
在上述示例中,我们实现了 Student
类并实现了 Serializable
接口。接下来,在 main
方法中创建了一个 Student
对象,然后将其写入到名为 student.dat
的文件中。这个文件就是序列化后的字节流。
三、反序列化实现
反序列化是将一个序列化的对象还原成原有的对象。需要使用 java.io.ObjectInputStream
对象的 readObject()
方法。在这之前,需要创建一个与序列化时相同的类,并且实现 Serializable
接口。
public static void main(String[] args) { try { FileInputStream fis = new FileInputStream("student.dat"); ObjectInputStream ois = new ObjectInputStream(fis); Student student = (Student) ois.readObject(); System.out.println(student.getName()); ois.close(); fis.close(); } catch (IOException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } }
在上述代码中,我们首先读取 student.dat
文件,然后使用 ObjectInputStream
对象的 readObject()
方法反序列化出一个Student
对象。最后打印该对象的姓名。
四、序列化UID的作用
Java序列化机制提供了一个叫做 serialVersionUID
的序列化版本号。这个版本号在序列化时会一起保存到文件中,反序列化时也会对比版本号是否一致,如果不一致,则会抛出一个InvalidClassException。
当一个类的实例被序列化时,serialVersionUID
的值也会被序列化保存下来。如果类的实现发生了变化(例如增加或删除了字段),它的 serialVersionUID
可能会发生变化。因此,我们应该总是手动声明 serialVersionUID
,以确保正确性。
public class Student implements Serializable { private static final long serialVersionUID = 1L; private String name; private int age; private String address; // constructors and methods }
五、transient关键字的作用
在Java中,有时候我们不想将某些字段序列化,这时候可以使用 transient
关键字。被 transient
修饰的字段会被忽略并跳过序列化过程。
public class Student implements Serializable { private String name; private int age; private transient String address; // constructors and methods }
六、序列化的风险与预防措施
因为Java序列化会将对象的状态以二进制数据的形式保存在磁盘上或在网络中进行传输,因此可能会存在安全隐患,主要包括以下方面:
1. 反序列化漏洞:通过精心构造的序列化数据,黑客可以触发执行任意的代码。这个问题已在Java 8及以上版本中得到修复,但仍然需要对旧版本的代码进行升级。
2. 替换可以序列化的类:如果需要序列化的类没有明确指定 serialVersionUID
,那么在该类发生变化后(增加或删除字段),反序列化时可能会导致程序崩溃。此时,黑客可以使用可序列化的替代类来执行攻击。
3. 盗取会话信息:网站可能会将认证信息序列化后保存到Cookie中,如果黑客获取了这个Cookie并解析出认证信息,就可以劫持用户的会话。
为了解决上述问题,可以采取以下预防措施:
1. 明确指定 serialVersionUID:默认情况下,Java会通过计算类的哈希值来生成一个 serialVersionUID。因此,如果在类中增加或删除变量,可能会导致 serialVersionUID 的变化,进而导致对象不能被反序列化。因此,建议明确指定 serialVersionUID 的值。
2. 使用白名单过滤类:在服务器端,可以对反序列化的类进行白名单过滤,只允许反序列化指定的类。
3. 针对特定场景开启安全机制:对于需要保证安全的场景,可以开启Java安全机制。例如,在Tomcat中可以开启安全管理模块,限制运行权限。
七、总结
Java序列化和反序列化是Java中非常重要的特性,可以帮助我们方便地将对象进行传输和保存。序列化时需要实现 java.io.Serializable
接口,并使用 java.io.ObjectOutputStream
对象将对象序列化为字节序列;反序列化时需要使用 java.io.ObjectInputStream
对象将字节序列反序列化为对象。为了保证安全性,需要手动指定 serialVersionUID
,并在服务器端对反序列化的类进行白名单过滤。此外,也可以设置 transient
关键字来忽略某些字段的序列化,避免数据泄露。