🔥 Java学习:Java从入门到精通总结
🔥 Spring系列推荐:Spring源码解析
📆 最近更新:2022年1月20日
🍊 个人简介:通信工程本硕💪、阿里新晋猿同学🌕。我的故事充满机遇、挑战与翻盘,欢迎关注作者来共饮一杯鸡汤
🍊 点赞 👍 收藏 ⭐留言 📝 都是我最大的动力!
文章目录
85 其他方法优先于Java序列化
🔥 先说结论:
-
序列化是危险的,应该避免
-
如果从头开始设计一个系统,可以使用跨平台的结构化数据,如
JSON
或protobuf
-
如果必须编写可序列化的类,要加倍小心地进行试验
序列化的一个根本问题是它的可攻击范围太大,且难以防御:通过调用ObjectInputStream
上的readObject
方法反序列化对象。可以用来实例化类路径上任何类型的对象,只要该类型实现Serializable
接口,有了实例化之后的对象,就可以执行这些类的代码,因此所有这些类都在攻击范围内。
static byte[] bomb() {
Set<Object> root = new HashSet<>();
Set<Object> s1 = root;
Set<Object> s2 = new HashSet<>();
for (int i = 0; i < 100; i++) {
Set<Object> t1 = new HashSet<>();
Set<Object> t2 = new HashSet<>();
t1.add("foo"); // Make t1 unequal to t2
s1.add(t1); s1.add(t2);
s2.add(t1); s2.add(t2);
s1 = t1;
s2 = t2;
}
return serialize(root); // Method omitted for brevity
}
对象图由201个HashSet
实例组成,整个流的⻓度为5744字节,但是在对其进行反序列化之前,资源就已经耗尽了。问题在于,反序列化HashSet
实例需要计算其内部元素的哈希码,深度为100,反序列化Set
会导致hashCode
方法被调用超过2^100次。
避免序列化利用的最好方法是永远不要反序列化任何东西。还有其他一些机制可以在对象和字节序列之间进行转换,从而避免了Java
序列化的许多危险。
这些方法共同点是它们比Java
序列化简单得多。它们不支持任意对象图的自动序列化和反序列化。而是支持简单的结构化数据对象,由一组「属性-值」对组成。只支持少数基本数据类型和数组数据类型。
最前沿的跨平台结构化数据表示是JSON
和protobuf
,
JSON
和protobuf
之间最显著的区别是JSON
是基于文本的,而protobuf
是二进制的,但效率更高;
如果你不能完全避免Java序列化,这时的最佳选择是永远不要反序列化不可信的数据。
86 谨慎实现 Serializable 接口
🔥 先说结论:
-
除非一个类只在受保护的环境下使用(版本之间无交互、服务器不会暴露给不可信任的人),否则必须认真考虑是否要实现
Serializable
接口 -
如果一个类允许继承,更要加倍小心
-
除了静态成员类之外,内部类不要实现这个接口
实现 Serializable
接口的一个主要代价是,一旦类的实现被发布,它就会降低修改灵活性。
即使是设计良好的序列化形式,也会限制类的演化;而设计不良的序列化形式,则可能会造成严重后果。
可序列化会使类的演变受到限制,因为每个可序列化的类都有一个与之关联的唯一标识符UID
(serial version UID)。
UID
是自动产生的,这个值受到类的名称、实现的接口及其大多数成员的影响。例如,通过添加一个临时的方法,生成的序列版本UID
就会更改。
实现 Serializable
接口的第二个代价是,增加了出现bug和安全漏洞的可能性。
序列化是一种语言之外的对象创建机制。依赖默认的反序列化机制,会让对象容易收到不变性破坏和非法访问。
实现 Serializable
接口的第三个代价是,如果要发布类的新版本,相关的测试负担就会增加。
当一个可序列化的类被修改时,重要的是检查是否可以在新版本中序列化一个实例,并在旧版本中反序列化它。
如果在第一次编写类时精心设计了自定义序列化形式,那么测试的工作量就会减少,后面会讨论到
如果一个类要参与一个框架,该框架依赖于Java序列化来进行对象传输或持久化,这对于类来说实现Serializable
接口就是非常重要的。
根据经验,像BigInteger
和 Instant
这样的值类实现了 Serializable
接口,集合类也实现了 Serializable
接口。表示活动实体(如线程池)的类很少情况适合实现 Serializable
接口。
为继承而设计的类应该尽量不实现 Serializable
接口,接口也应该尽量不继承 Serializable
。
在为了继承而设计的类中,Throwable
类和 Component
类都实现了 Serializable
接口。正是因为
Throwable
实现了 Serializable
接口,RMI可以将异常从服务器发送到客户端,这其实是不好的。
内部类不应该实现Serializable
,静态成员类可以实现这个接口。
87 考虑使用自定义的序列化形式
即如何实现一个自定义的序列化形式,阿里内部最经典的RPC框架HSF其中有一大块就是序列化和反序列化的设计,所以这项技术有很高的实战价值,这里也给出了一些建议。
🔥 先说结论:
-
只有当默认的序列化形式能合理描述对象的逻辑状态时,才使用默认的序列化形式
-
其他情况,应该设计一个自定义的序列化形式,通过它来合理地描述对象的状态
如果对象的物理表示与其逻辑内容相同,则默认的序列化形式是合适的。
例如,默认序列化形式对于Name
类来说是合理的,它只表示一个人的名字:
public class Name implements Serializable {
/**
* Last name. Must be non-null.
*
* @serial
*/
private final String lastName;
/**
* First name. Must be non-null.
*
* @serial
*/
private final String firstName;
/**
* Middle name, or null if there is none.
*
* @serial
*/
private final String middleName;
public Name(String lastName, String firstName, String middleName) {
this.lastName = lastName;
this.firstName = firstName;
this.middleName = middleName;
}
// Remainder omitted
}
@serial
告诉Javadoc将此文档放在一个特殊的⻚面上,该⻚面记录序列化的形式。
从逻辑上讲,名字由三个字符串组成:姓、名和中间名。Name
的实例字段精确地反映了这个逻辑内容。
此外,还必须提供readObject
方法来确保约束关系和安全性。对于 Name
类而言,readObject
方法必须确保字段lastName
和firstName
是非null
的。
下面的类表示了一个字符串列表:
public final class StringList implements Serializable {
private int size = 0;
private Entry head = null;
private static class Entry implements Serializable {
String data;
Entry next;
Entry previous;
}
... // Remainder omitted
}
从逻辑上讲,这个类表示字符串序列。在物理上,它将序列表示为双向链表。当对象的物理表示与其逻辑数据内容有很大差异时,使用默认的序列化形式有四个缺点:
- 它将导出的API永久地束缚在该类的内部实现上
在上面的例子中,私有StringList.Entry
类成为公共API的一部分。如果在将来的版本中更改了实现,StringList
类仍然需要接受链表形式的输出,并产生链表形式的输出。这个类永远也摆脱不掉处理链表项所需要的所有代码,即使不再使用链表作为内部数据结构。
- 会占用过多的空间
这些链表项以及链接只不过是实现细节,不值得记录在序列化形式中。
- 会消耗过多的时间
序列化逻辑不知道对象图的拓扑结构,因此必须经过一个高开销的遍历过程。在上面的例子中,只要沿着next
遍历就足够了。
- 可能导致堆栈溢出
StringList的合理序列化形式只需要包含列表中的字符串数量和字符串本身即可。这构成了由StringList
表示的逻辑数据,去掉了其物理表示的细节。下面是修改后的StringList
版本:
public final class StringList implements Serializable {
private transient int size = 0;
private transient Entry head = null;
// No longer Serializable!
private static class Entry {
String data;
Entry next;
Entry previous;
}
// Appends the specified string to the list
public final void add(String s) {
}
/**
* Serialize this {@code StringList} instance.
*
* @serialData The size of the list (the number of strings
* it contains) is emitted ({@code int}), followed by all of
* its elements (each a {@code String}), in the proper
* sequence.
*/
private void writeObject(ObjectOutputStream s) throws IOException {
s.defaultWriteObject();
s.writeInt(size);
// Write out all elements in the proper order.
for (Entry e = head; e != null; e = e.next)
s.writeObject(e.data);
}
private void readObject(ObjectInputStream s) throws IOException,
ClassNotFo