每次创建实体类时都会纠结是否实现 Serializable 接口,但是实不实现好像都没什么影响,就此决定对 Java 序列化一探究竟
序列化的意义
互联网的迅猛发展受益于网络通信技术的成熟和稳定,网络通信协议是机器双方共同约定的协议。在应用层看到的是结构体、对象,但是在网络协议里,机器之间传输的都是二进制流。网络编程中,需要定义应用层协议。最原始的应用层协议是字节数组,在 Java 语言里以 byte[] 体现,在 C 语言里以 char[] 体现。不管是 Java 语言还是 C 语言,开发人员都需要知道字节数组里每个字节的含义才能保证数据写入和读取的正确性。这对开发人员来说,是非常严苛且低效的。 如何将程序中的结构体、对象等结构化内存对象转换为扁平的二进制流?如何将二进制流还原为结构化内存对象?为了解决这些问题,序列化/反序列化技术应运而生。
核心意义:对象状态的保存(序列化)和重建(反序列化)
序列化的方式
一般来说,序列化/反序列化分为 IDL(Interface Description Language,接口描述语言)和非 IDL 两类。非 IDL 技术方案包含 JSON、XML 等,提供构造和解析的工具包即可使用,不需要做代码生成的工作。IDL 技术方案包含 Thrift、Protocol Buffer、Avro 等,有比较完整的规约和框架实现。
以我了解的 Protocol Buffer 为例,客户端与服务端使用 protoc
命令根据 .proto
文件生成相应语言的代码(Java、C++…)然后就会有相应的序列化与反序列化方法进行请求的发送与接收。
JDK 中的序列化与反序列化
实现 Serializable 接口
这是最常见的一种 Java 序列化方式
实现 Serializable
的待序列化的对象 Student
1 2 3 4 5 6 7 8
| @Data @NoArgsConstructor @AllArgsConstructor class Student implements Serializable { private static final long serialVersionUID = 7013329321078382881L; private String name; private Integer age; }
|
将 Student 序列化到指定文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| @Test public void test() { Student student = new Student("venom", 18); serializeToFile(student, "D:\\temp\\student.ser"); }
public void serializeToFile(Student student, String filePath) { try (FileOutputStream fileOut = new FileOutputStream(filePath); ObjectOutputStream objectOut = new ObjectOutputStream(fileOut)) { objectOut.writeObject(student); System.out.println("Serialization completed. Object saved to file: " + filePath); } catch (IOException e) { e.printStackTrace(); } }
|
查看生成的二进制文件
将刚才生成的二进制文件反序列化成 Student
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| @Test public void test() { Student deserializedObj = deserializeFromFile("D:\\temp\\student.ser"); System.out.println(deserializedObj); }
public Student deserializeFromFile(String filePath) { try (FileInputStream fileIn = new FileInputStream(filePath); ObjectInputStream objectIn = new ObjectInputStream(fileIn)) { Student deserializedObj = (Student) objectIn.readObject(); System.out.println("Deserialization completed. Object loaded from file: " + filePath); return deserializedObj; } catch (IOException | ClassNotFoundException e) { e.printStackTrace(); return null; } }
|
console
1 2
| Deserialization completed. Object loaded from file: D:\temp\student.ser Student(name=venom, age=18)
|
实现 Externalizable 接口
除了实现 Serializable
接口完成序列化/反序列化外,还可以通过实现 Externalizable
接口达到序列化与反序列化的目的。与 Serializable
不同的是,Externalizable
有抽象方法需要实现,Externalizable
继承了 Serializable
实现 Externalizable
的待序列化对象 User
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| @Data @NoArgsConstructor @AllArgsConstructor class User implements Externalizable { private static final long serialVersionUID = 4523061523714222909L; private String name; private Integer age;
@Override public void writeExternal(ObjectOutput out) throws IOException { out.writeObject(name); out.writeObject(age); }
@Override public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { name = (String) in.readObject(); age = (Integer) in.readObject(); } }
|
将 User 序列化到指定文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| @Test public void test() { User user = new User("venom", 18); serializeToFile(user, "D:\\temp\\user.ser"); }
public void serializeToFile(User user, String filePath) { try (FileOutputStream fileOut = new FileOutputStream(filePath); ObjectOutputStream objectOut = new ObjectOutputStream(fileOut)) { objectOut.writeObject(user); System.out.println("Serialization completed. Object saved to file: " + filePath); } catch (IOException e) { e.printStackTrace(); } }
|
查看生成的二进制文件
将刚才生成的二进制文件反序列化成 User
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| @Test public void test() { User user = deserializeFromFile("D:\\temp\\user.ser"); System.out.println(user); }
public User deserializeFromFile(String filePath) { try (FileInputStream fileIn = new FileInputStream(filePath); ObjectInputStream objectIn = new ObjectInputStream(fileIn)) { User deserializedObj = (User) objectIn.readObject(); System.out.println("Deserialization completed. Object loaded from file: " + filePath); return deserializedObj; } catch (IOException | ClassNotFoundException e) { e.printStackTrace(); return null; } }
|
console
1 2
| Deserialization completed. Object loaded from file: D:\temp\user.ser User(name=venom, age=18)
|
自定义序列化与反序列化
除了使用默认的序列化机制外,对于一些特殊的类, 我们需要定制序列化和反序列化方法的时候,可以通过重写以下方法实现
1 2 3
| private void writeObject(java.io.ObjectOutputStream out) throws IOException; private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException; private void readObjectNoData() throws ObjectStreamException;
|
将 Student 的序列化 name 加上 24,age + 1
1 2 3 4
| private void writeObject(ObjectOutputStream out) throws IOException { out.writeObject(name + "24"); out.writeObject(age + 1); }
|
重新序列化
1 2 3 4 5
| @Test public void test() { Student student = new Student("venom", 18); serializeToFile(student, "D:\\temp\\student2.ser"); }
|
查看二进制文件
可以看出现在是 venom24
反序列化将前面的改动还原
1 2 3 4
| private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { name = ((String) in.readObject()).substring(0, 5); age = ((Integer) in.readObject()) - 1; }
|
反序列化
1 2 3 4 5
| @Test public void test() { Student student = deserializeFromFile("D:\\temp\\student2.ser"); System.out.println(student); }
|
console
1 2
| Deserialization completed. Object loaded from file: D:\temp\student2.ser Student(name=venom, age=18)
|
JDK 序列化原理
// TODO
serialVersionUID
serialVersionUID
用来实现类版本兼容,在实际开发中能满足类字段变化的需求。如果我们有一个 Student 类,实现了 Serializable 接口,但是没有定义 serialVersionUID
,对 Student 类增加一个 double 类型的字段 height,再读取增加字段之前的序列化数据,反序列化会报 InvalidCastException
异常。如果 Student 类定义了 serialVersionUID
,对 Student 类增加一个 double 类型的字段 height,再读取增加字段之前的序列化数据,反序列化可以成功。serialVersionUID
必须是 static+final
类型,否则 serialVersionUID 不会被序列化。
static
static 字段属于类全局共有,不会被序列化。在反序列化得到的结果里,静态变量的值依赖类对该静态字段的初始化操作以及是否在同一个 JVM 进程内。比如说静态变量初始值为 0,在序列化之前静态变量的值被设置为 10,在同一个 JVM 进程内执行反序列化操作,得到的静态变量的值为 10。如果在另外一个 JVM 进程内执行反序列化操作,得到的静态变量的值为 0。这是因为类在 JVM 进程内只会被加载一次,相同的类在不同的 JVM 内都会初始化一遍。
transient
Java 序列化可以通过 transient 关键字来控制字段不被序列化。通过跟进 ObjectStreamClass 的 getDefaultSerialFields 方法内部实现,可以看到序列化字段不能为 static 且不能为 transient
writeReplace
writeReplace
用于序列化写入时拦截并替换成一个自定义的对象。 用于多种用途,包括安全性、版本兼容性和对象共享等。当需要精细控制对象序列化过程时,可以考虑使用它 。
- 安全性:
writeReplace
方法可以用于返回安全的代理对象,以确保敏感数据在序列化时不会泄露。
- 压缩:
writeReplace
中返回一个压缩过的对象,以减小序列化数据的大小
- 版本兼容: 如果类的版本发生了变化,可以使用
writeReplace
来返回与旧版本兼容的替代对象
- 对象共享: 可以返回一个代理对象,以确保在序列化和反序列化期间共享对象实例
由于 writeReplace
的调用是基于反射来执行的,所以作用域限定符不受限制,可以是 private、default、protected、public 中的任意一种。 如果定义了 wirteReplace
方法,就没必要再定义 writeObject
方法了。即使定义了 writeObject
方法,该方法也不会被调用,内部会先调用 writeReplace
方法将当前序列化对象替换成自定义目标对象。同理,也没必要定义 readObject
方法,即使定义了也不会被调用。
readResolve
readResolve
方法与 writeReplace
方法的作用相反。它用于在反序列化时提供一个机会来替代反序列化得到的对象,从而可以进行自定义的对象还原或替代操作。
- 对象替代: 在反序列化时,可以返回一个替代的对象,而不是反序列化得到的对象。这可以用于实现单例模式,对象池,或其他替代逻辑
- 对象还原: 如果对象的类结构发生变化,可以在
readResolve
中还原对象,以确保与旧版本的对象一致性
- 安全性: 可以在
readResolve
中检查反序列化得到的对象,以确保它符合某些安全性规则,然后返回一个安全的对象
readResolve
方法用于反序列化拦截并替换成自定义的对象。但和 writeReplace
方法不同的是,如果定义了 readResolve
方法,readObject
方法是允许出现的。
ObjectInputValidation
接口是 Java 序列化机制提供的一种机制,用于验证反序列化的对象是否合法。它允许在反序列化对象之后执行自定义的验证操作,以确保反序列化的对象状态是有效的。
实现接口:验证类实现 ObjectInputValidation
接口,并实现 validateObject
方法
验证: 在 validateObject
方法中编写验证逻辑,检查对象的状态是否合法
抛出异常: 如果在验证过程中发现对象状态不合法,可以通过抛出 InvalidObjectException
异常来中止对象的反序列化
注册验证: 在反序列化之前,通过 ObjectInputStream
的 registerValidation
方法将实现了 ObjectInputValidation
接口的对象注册为验证对象
1 2 3
| ObjectInputValidation validationObject = new MyValidationObject(); ObjectInputStream ois = new ObjectInputStream(inputStream); ois.registerValidation(validationObject, 0);
|
反序列化:进行反序列化操作
以 Student 为例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| @Data @NoArgsConstructor @AllArgsConstructor class Student implements Serializable, ObjectInputValidation { private static final long serialVersionUID = 6397825858906650077L; private String name; private Integer age;
@Override public void validateObject() throws InvalidObjectException { if (age < 0) { throw new InvalidObjectException("age cannot be negative"); } } }
|
序列化与反序列化的加解密
Java 序列化后的数据是明文形式,而且数据的组成格式有明确的规律。当这些数据脱离 Java 安全体系存在磁盘中时,可以通过二进制数编辑工具查看,甚至修改。如果这些数据注入了病毒,应用程序的表现行为将无法预计。为了保障数据的安全性,引入 SealedObject
和 SignedObject
对序列化数据进行加密
SealedObject
序列化加密
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| @Test public void test() throws NoSuchAlgorithmException { Student student = new Student("venom", 18); SecretKey key = KeyGenerator.getInstance("DESede").generateKey(); encryptAndSerializeToFile(student, "D:\\temp\\encrypt_student.ser", key); }
public void encryptAndSerializeToFile(Student student, String filePath, SecretKey key) { try (FileOutputStream fileOut = new FileOutputStream(filePath); ObjectOutputStream objectOut = new ObjectOutputStream(fileOut)) { Cipher cipher = Cipher.getInstance("DESede"); cipher.init(Cipher.ENCRYPT_MODE, key); SealedObject sealedObject = new SealedObject(student, cipher); objectOut.writeObject(sealedObject); System.out.println("Encrypted Serialization completed. Object saved to file: " + filePath); } catch (IOException e) { e.printStackTrace(); } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | IllegalBlockSizeException e) { throw new RuntimeException(e); } }
|
查看生成的二进制文件
可以看到现在基本已面目全非
反序列化解密
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public Student decryptAndDeserializeFromFile(String filePath, SecretKey key) { try (FileInputStream fileIn = new FileInputStream(filePath); ObjectInputStream objectIn = new ObjectInputStream(fileIn)) { SealedObject sealedObject = (SealedObject) objectIn.readObject(); Student deserializedObj = (Student) sealedObject.getObject(key); System.out.println("Decrypted Serialization completed. Object loaded from file: " + filePath); return deserializedObj; } catch (IOException | ClassNotFoundException e) { e.printStackTrace(); return null; } catch (NoSuchAlgorithmException | InvalidKeyException e) { throw new RuntimeException(e); } }
|
console
1 2
| Decrypted Serialization completed. Object loaded from file: D:\temp\encrypt_student.ser Student(name=venom, age=18)
|
SignedObject
SignedObject 也是通过加解密的方式来保护序列化安全,与 SealedObject
不同的是,SignedObject
使用数字签名来验证对象的完整性和来源,所以 SignedObject
使用的是非对称加密
序列化加签
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| @Test public void test() throws NoSuchAlgorithmException { Student student = new Student("venom", 18); KeyPair keyPair = KeyPairGenerator.getInstance("RSA").generateKeyPair(); encryptAndSerializeToFile(student, "D:\\temp\\encrypt_student1.ser", keyPair); }
public void encryptAndSerializeToFile(Student student, String filePath, KeyPair keyPair) { try (FileOutputStream fileOut = new FileOutputStream(filePath); ObjectOutputStream objectOut = new ObjectOutputStream(fileOut)) { SignedObject signedObject = new SignedObject(student, keyPair.getPrivate(), Signature.getInstance("SHA256withRSA")); objectOut.writeObject(signedObject); System.out.println("Encrypted Serialization completed. Object saved to file: " + filePath); } catch (IOException e) { e.printStackTrace(); } catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException e) { throw new RuntimeException(e); } }
|
查看生成的二进制文件
可以看出,还是能看到相关属性的
反序列化验签
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| public Student decryptAndDeserializeFromFile(String filePath, KeyPair keyPair) { try (FileInputStream fileIn = new FileInputStream(filePath); ObjectInputStream objectIn = new ObjectInputStream(fileIn)) { SignedObject signedObject = (SignedObject) objectIn.readObject(); signedObject.verify(keyPair.getPublic(), Signature.getInstance("SHA256withRSA")); Student deserializedObj = (Student) signedObject.getObject(); System.out.println("Decrypted Serialization completed. Object loaded from file: " + filePath); return deserializedObj; } catch (IOException | ClassNotFoundException e) { e.printStackTrace(); return null; } catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException e) { throw new RuntimeException(e); } }
|
要不要加 Serializable
到了最终的问题,一个对象或者实体类,到底需不需要实现 Serializable
接口呢?
从前面的了解可知,实现 Serializable
的目的是为了 Java 的序列化与反序列化,那么如果是 Java 与 Python,或者 Java 与 C++ 之间的网络传输呢?其根本就是要看序列化的方式,如果使用了 Java 自身的序列化方案,那么就需要实现 Serializable
,否则就不必
拿分布式系统中的 SpringCloud
和 Duboo
来说
Spring Cloud 的 Feign 使用了一种自定义的序列化和反序列化机制来传输数据。默认情况下,Feign 使用基于 JSON 的序列化。也可以选择其他序列化格式,如 XML,或者自定义序列化/反序列化机制。因此,不需要在远程调用的请求和响应对象上实现 Serializable
接口
Dubbo 也提供了自定义的序列化和反序列化机制,通常使用 Hessian、Protobuf、或其他序列化框架。也可以选择其他适合需求的序列化方式。Dubbo 的序列化机制不要求对象实现 Serializable
接口
再比如常见的 ORM 框架
Hibernate
提供了自定义的对象到关系数据库表的映射机制。Hibernate
使用自己的序列化机制来将对象持久化到数据库,而不依赖于 Java 自带的序列化。对象可以映射到数据库表,并且可以通过自定义的 SQL 或 HQL(Hibernate Query Language)进行操作
MyBatis
将 SQL 语句与 Java 对象进行映射。MyBatis
通常不依赖于 Java 自带的序列化。它通过 SQL 映射文件或注解将查询语句映射到 Java 方法,然后使用自定义的机制将查询结果映射到 Java 对象