Talk about serialization

If there are 100,000 unknown puzzles in the world, I will go to find the 100,000 puzzles.

What is serialization and deserialization

First exposure to the serialization is in my freshman year, that time happened to be the final exam to complete the course set, where the class structure of the data set, the topic we chose a pedigree management system, need C++to achieve. One problem encountered when doing the course set up like this - how will the structure of the family tree is stored in the text, and then open the program reads the text again when the contents loadinto the system do?

Later we found access to information on serialization and de-serialization of these two things, my friends and I read online to try some of the code sample preparation (at the time of class-based team is made of, everyone has their own division).

The next day, my friend dragged his tired body and came to me with heavy dark circles and said to me: " made, last night to two o'clock, finally got it!" So he opened the management system program and showed me how the tree is serialized into the text, then the text content deserialized mapped into the program. Looking at my friend's panda's eyes, I couldn't help but gave a thumbs up (the freshman is so capable, and I have to work). So, I remember these two words deeply.

For serialization and de-serialization and binary tree more than in leetcodethe original title have, it would be a high-frequency interview questions it, and realize it or some difficulty, interested students can challenge it.

Difficulty of serialization and deserialization of binary tree: difficult .

Serialization and deserialization binary search tree difficulty: medium .

Serialization and deserialization of N-tree difficulty: difficult .

At that time I understood the serialization and de-serialization is actually the object into a byte sequence is converted into a sequence of bytes and targeting process .

Later contacted Java, recognized Serializableinterfaces, and slowly read a lot of books, I realized that learning about the serialization and de-serialization much more.

How to implement serialization and deserialization in Java

We let the object supports serialization and de-serialization only need to implement an Serializableinterface to the line. As for Serializablethe interface, you view the source code will find that this thing would declare an interface, and nothing of the other.

public interface Serializable {
}
复制代码

The explanation of this interface is also clearly written in the file comments, only to identify the semantics of being serializableonly used to determine whether the object supports serialization and deserialization.

Analysis of serialization principle

So how do you serialize and deserialize objects?

Mainly through ObjectOutputStreamand ObjectInputStreamthese two classes to implement. For example, we may be Boyan instance of the file to the target sequence, and then build another deserialize Boyobjects.

@Data
public class Boy implements Serializable {

    private int girlFriendCount;

}

// 测试方法
public static void main(String[] args) throws IOException, ClassNotFoundException {
    Boy b1 = new Boy();
    b1.setGirlFriendCount(10);
    // 序列化
    ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream(new File("test")));
    objectOutputStream.writeObject(b1);
    // 反序列化
    ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("test"));
    Boy b2 = (Boy)objectInputStream.readObject();
    // 得到结果为10
    System.out.println(b2.getGirlFriendCount());
}
复制代码

In writeObject(obj)this method (serialization), it is how to determine whether to serialize it? The answer is simple, we can debugsee the core source code.

writeObject0
writeObject0

This code is required to determine the type of object serialization, if go instanceof Serializable, then it is not in line with the will throw an NoSerializableExceptionexception. So if you declare the class does not implement the Serializableinterfaces that will throw this exception.

以此类推,如果我对一个实现了 Serializable 接口的类的对象进行序列化,但是它持有的一个对象并没有实现 Serializable 接口,此过程是否一定会抛出 NoSerializableException 异常呢 ?我看很多博客中都写到会抛出,其实我觉得不一定。如果说,该对象持有的那个未实现 Serializable 接口的对象并没有进行初始化(也就是说为 null ),那么此时是不会报错的,其原因也在源代码中。

首先判断是否为null
首先判断是否为null

其实你可以尝试一下对 null 进行序列化,也是不会报错的。

而对于 Serialize 还有一个注意点就是,如果 父类实现了 Serialize 接口,子类继承了父类,子类是也默认实现了 Serialize 。这其实是一个设计的问题,没有那么多为什么,如果究其原因还是在那句 obj instanceof Serializable ,因为继承实现了 Serializable 接口的类的类,必然符合这个条件。

对于一个类的 静态成员 来说是不会被序列化的,而序列化本身就是对 对象 状态的记录,需要的是对象的属性而不是类的属性。但是有个有意思的地方,内部静态类 可以实现序列化接口,甚至我们还可以使用静态类来创建 序列化代理 以此来提升序列化的安全能力。

public class Test implements Serializable {

    private static final long serialVersionUID = 3005560411086043165L;

    private int age;
    private boolean sex;

    private static class SerializableProxy implements Serializable {

        private static final long serialVersionUID = 2677759736303136145L;

        private final int age;
        private final boolean sex;

        SerializableProxy(Test test) {
            this.age = test.age;
            this.sex = test.sex;
        }
    }

    private void readObject(ObjectInputStream stream) throws InvalidObjectException {
        System.out.println("执行了 Test 的 readObject 方法");
        throw new InvalidObjectException("需要代理!");
    }

    private Object writeReplace() {
        System.out.println("执行了替代方法");
        return new SerializableProxy(this);
    }

}
复制代码

在上面代码中的 readObjectwriteReplace 方法是序列化过程中会被调用的方法(可以理解为钩子函数),你可能会有疑问,不是调用的 objectInputStream.readObject(obj) 这个方法么,这两个钩子函数为什么会被执行?它们没有继承 ObjectInputStream 并且重写 readObject 方法呀,为什么会被调用?

其实原因也很简单,就在 ObjectInputStream 的执行流程中,这里我展示一下 ObjectOutpuStream 中的相关调用流程,原理都是一样的。

序列化中的反射
序列化中的反射

你可以进行 debug 然后查看里面具体的调用流程。而对于 Java 的序列化来说可以实现很多钩子函数,writeReplace() 亦是如此。

使用序列化代理来提高安全性

继续来说说,使用静态类来进行 序列化代理 的目的是什么?为什么要这么麻烦呢?Java 中为什么要定义这么多序列化钩子函数呢?

其实答案就是 安全 ,为了安全我们甚至可以牺牲一定的 性能开销 。如果一个类决定了实现 Serializable 接口那么也就意味着我们可以通过 语言机制 以外的方式去创建实例(因为我们可以调用 readObject,通过字节流创建了呀)。可以这么理解,反序列化其实就是一个 隐藏的构造器 ,反序列化过程中我们可以去违反类原本的构造器 约束 ,甚至去干一些构造以外的事情。

这必定会带来不可估量的安全问题,比如说,业界中常常提到的 反序列化炸弹 来实现 DoS 拒绝服务攻击 ,我们可以通过互相引用的200个 HashSet 互相引用实例来构建 2^100 次的 hashCode 方法调用;而攻击者还能根据 反序列化期间被调用的方法 ,形成根据序列化形成的调用代码来 任意 的在程序中进行执行,这个后果也就意味着攻击者能直接控制你所谓的程序。

序列化破坏单例

所以,是否该实现序列化是一个慎重的决定,例如如果让 单例 的类去实现 Serializable 接口,那么就会破话单例模式。

当然你可以再次使用一个钩子函数 readResolve ,当对象已经通过 readObject 方法产生了,如果说你书写了这个 readResolve 方法,那么就会调用这个方法并且返回你想要的真正的对象。

private Object readResolve() {
    // 直接返回单例
    return INSTANCE;
}
复制代码

但是这种方式也 并不是绝对安全 ,如果一个单例包含一个非瞬时(未被 transient 修饰)对象,那么这个域的内容就可以在单例的 readResolve 方法运行之前被反序列化,具体编码方式可以参考 《Effective Java》,这里不做过多描述。

谈谈 serialVersionUID

在上文中的 Test 类以及它的序列化代理类我都添加了 serialVersionUID 这个私有静态不可变属性,为什么要添加呢?

所谓 serialVersionUID ,顾名思义,其实就是 序列化版本ID ,序列化为什么要和版本牵扯关系呢?

首先需要明确的是即使我们不加上 serialVersionUID 这个字段,Java 也会根据这个类的 类名称、所实现接口的名称、以及所有 public protected 字段 通过加密散列函数来生成一个默认的 serialVersionUID。这个序列号在反序列化过程中会用于验证序列化对象的发送者和接收者是否为该对象加载了与序列化兼容的类。如果接收者加载的该对象的类的 serialVersionUID 与对应的发送者的类的 版本号不同 ,则反序列化将会导致 InvalidClassException

所以给类加上一个 serialVersionUID 字段,以确保兼容问题是一个明智的选择。

其他序列化方式以及安全防范

现如今有很多前沿的序列化机制,例如 JSONProtoBuf 等,他们能更好地支撑不同平台之间的序列化问题,而对于 Java 原生的序列化因为其性能,安全,应用的问题也有可能会在未来被淘汰。

也并不是说其他序列化方式没有漏洞,就比如我们常使用的 fastJSON 就被曝出过好几次漏洞,因为反序列化本身就是一个范围比较广的安全问题,如果黑客利用反序列化漏洞构造执行链就相当于控制了你的整个程序,他就可以为所欲为了。

而对于如何进行安全防范也是一个比较头疼的问题。比如说如何去阻止黑客去构造调用链,其实对于黑客来说构建调用链都是基于类的方法,我们可以去添加一个黑名单,让一些本不必要执行序列化的类纳入到 黑名单 中。

像我们前面提到的很多 钩子函数 ,这也是一种序列化安全的解决方案 RASP 。我们可以在钩子函数中加入一层规则判断,判断是否有非正常代码的执行,甚至我们可以直接限制程序的序列化反序列化过程。

参考

《Effective Java》第三版

Java工程师成神之路 | 2020正式版

Guess you like

Origin juejin.im/post/5e96a82af265da47c35d867a