带你快速看完9.8分神作《Effective Java》—— 序列化篇(所有RPC框架的基石)

🔥 Java学习:Java从入门到精通总结

🔥 Spring系列推荐:Spring源码解析

📆 最近更新:2022年1月20日

🍊 个人简介:通信工程本硕💪、阿里新晋猿同学🌕。我的故事充满机遇、挑战与翻盘,欢迎关注作者来共饮一杯鸡汤

🍊 点赞 👍 收藏 ⭐留言 📝 都是我最大的动力!

85 其他方法优先于Java序列化

🔥 先说结论:

  1. 序列化是危险的,应该避免

  2. 如果从头开始设计一个系统,可以使用跨平台的结构化数据,如JSONprotobuf

  3. 如果必须编写可序列化的类,要加倍小心地进行试验


序列化的一个根本问题是它的可攻击范围太大,且难以防御:通过调用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序列化简单得多。它们不支持任意对象图的自动序列化和反序列化。而是支持简单的结构化数据对象,由一组「属性-值」对组成。只支持少数基本数据类型和数组数据类型。


最前沿的跨平台结构化数据表示是JSONprotobuf

JSONprotobuf之间最显著的区别是JSON是基于文本的,而protobuf是二进制的,但效率更高;

如果你不能完全避免Java序列化,这时的最佳选择是永远不要反序列化不可信的数据


86 谨慎实现 Serializable 接口

🔥 先说结论:

  1. 除非一个类只在受保护的环境下使用(版本之间无交互、服务器不会暴露给不可信任的人),否则必须认真考虑是否要实现 Serializable 接口

  2. 如果一个类允许继承,更要加倍小心

  3. 除了静态成员类之外,内部类不要实现这个接口


实现 Serializable 接口的一个主要代价是,一旦类的实现被发布,它就会降低修改灵活性

即使是设计良好的序列化形式,也会限制类的演化;而设计不良的序列化形式,则可能会造成严重后果。

可序列化会使类的演变受到限制,因为每个可序列化的类都有一个与之关联的唯一标识符UID(serial version UID)。

UID是自动产生的,这个值受到类的名称、实现的接口及其大多数成员的影响。例如,通过添加一个临时的方法,生成的序列版本UID就会更改。


实现 Serializable 接口的第二个代价是,增加了出现bug和安全漏洞的可能性

序列化是一种语言之外的对象创建机制。依赖默认的反序列化机制,会让对象容易收到不变性破坏和非法访问。


实现 Serializable 接口的第三个代价是,如果要发布类的新版本,相关的测试负担就会增加

当一个可序列化的类被修改时,重要的是检查是否可以在新版本中序列化一个实例,并在旧版本中反序列化它。

如果在第一次编写类时精心设计了自定义序列化形式,那么测试的工作量就会减少,后面会讨论到


如果一个类要参与一个框架,该框架依赖于Java序列化来进行对象传输或持久化,这对于类来说实现Serializable 接口就是非常重要的。

根据经验,像BigIntegerInstant 这样的值类实现了 Serializable 接口,集合类也实现了 Serializable 接口。表示活动实体(如线程池)的类很少情况适合实现 Serializable 接口。


为继承而设计的类应该尽量不实现 Serializable 接口,接口也应该尽量不继承 Serializable

在为了继承而设计的类中,Throwable 类和 Component 类都实现了 Serializable 接口。正是因为
Throwable 实现了 Serializable 接口,RMI可以将异常从服务器发送到客户端,这其实是不好的。


内部类不应该实现Serializable,静态成员类可以实现这个接口。


87 考虑使用自定义的序列化形式

即如何实现一个自定义的序列化形式,阿里内部最经典的RPC框架HSF其中有一大块就是序列化和反序列化的设计,所以这项技术有很高的实战价值,这里也给出了一些建议。

🔥 先说结论:

  1. 只有当默认的序列化形式能合理描述对象的逻辑状态时,才使用默认的序列化形式

  2. 其他情况,应该设计一个自定义的序列化形式,通过它来合理地描述对象的状态


如果对象的物理表示与其逻辑内容相同,则默认的序列化形式是合适的

例如,默认序列化形式对于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 方法必须确保字段lastNamefirstName是非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
}

从逻辑上讲,这个类表示字符串序列。在物理上,它将序列表示为双向链表。当对象的物理表示与其逻辑数据内容有很大差异时,使用默认的序列化形式有四个缺点

  1. 它将导出的API永久地束缚在该类的内部实现上

在上面的例子中,私有StringList.Entry类成为公共API的一部分。如果在将来的版本中更改了实现,StringList 类仍然需要接受链表形式的输出,并产生链表形式的输出。这个类永远也摆脱不掉处理链表项所需要的所有代码,即使不再使用链表作为内部数据结构。

  1. 会占用过多的空间

这些链表项以及链接只不过是实现细节,不值得记录在序列化形式中。

  1. 会消耗过多的时间

序列化逻辑不知道对象图的拓扑结构,因此必须经过一个高开销的遍历过程。在上面的例子中,只要沿着next遍历就足够了。

  1. 可能导致堆栈溢出

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
  • 95
    点赞
  • 99
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 135
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 135
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小王曾是少年

如果对你有帮助,欢迎支持我

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值