Java 反序列化基础

警告
本文最后更新于 2019-10-14,文中内容可能已过时。

又臭又长🙃

序列化是将面向对象中的对象转化为文件的过程,通过在流中使用文件可以实现对象的持久存储。 和PHP一样,java中也有序列化和反序列化,先来看下最基本的反序列化代码。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import java.io.*;

interface Animal {
    public void eat();
}

class Ani implements Serializable {
    public String name;

    private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {
        //执行默认的readObject()方法
        in.defaultReadObject();
        //执行打开计算器程序命令
        Runtime.getRuntime().exec("calc");
    }
}

class Cat extends Ani implements Animal {
    @Override
    public void eat() {
        System.out.println("cat eat.");
    }
}

public class Test {

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        Ani cat = new Cat();
        cat.name = "tom";
        FileOutputStream fos = new FileOutputStream("obj");
        ObjectOutputStream os = new ObjectOutputStream(fos);
        os.writeObject(cat);
        os.close();
        //从文件中反序列化obj对象
        FileInputStream fis = new FileInputStream("obj");
        ObjectInputStream ois = new ObjectInputStream(fis);
        //恢复对象
        Cat objectFromDisk = (Cat) ois.readObject();
        System.out.println(objectFromDisk.name);
        ois.close();
    }
}

在java中,反序列化对象是通过继承Serializable接口来实现的,只要一个类实现了java.io.Serializable接口,那么它就可以被序列化。

在上面这段代码中,创建了一个Ani父类和一个Cat子类,以及一个Animal接口,在子类中通过重写接口的eat方法来实现猫吃饭的功能。

在main方法中,我们创建了一个cat对象,通过文件流的形式来将对象写入到磁盘中实现持久性存储,这个写入的过程就是序列化的过程。

反序列化的过程是从文件obj中读取输入流,然后通过ObjectInputStream将输入流中的字节码反序列化为对象,最后通过ois.readObject()将对象恢复为类的实例。

readObject()是干什么吃的呢?

我们知道,在php序列化的过程中,会判断是否有__sleep()魔术方法,如果存在这个魔术方法,会先执行这个方法,反序列unserialize()的时候先执行的是__wakeup()魔术方法。

java同理,readObject()=__wakeup(),还有一个writeObject()=__sleep(),这两个方法默认是可以不写的,我在上面的代码中重写了readObject()方法,通过自定义的writeObject()readObject()方法可以允许用户控制序列化的过程,比如可以在序列化的过程中动态改变序列化的数值。

那么写到这里差不多就明了了,cat对象在反序列化的时候会自动自动调用readObject()方法,导致执行命令。

但是你肯定会问,真正开发的时候谁会这么写啊,这不是故意写bug吗?确实,开发人员不会这么写,但是在重写readObject()方法时会写一些正常的操作,我们这个时候就要提到反射了,关于更多java反序列化的问题请移步 深入分析Java的序列化与反序列化

在java中,反序列化很大部分是通过反射来构造pop链。

什么是反射?反射之中包含了一个「反」字,所以想要解释反射就必须先从「正」开始解释。

我们先来看一段代码

1
2
fanshe testObj = new fanshe();
testObj.setPrice(5);

很简单,就是通过new创建了一个fanshe类的对象testObj,这是[正射]。在这个实例化的过程中,我们需要知道类名,那么实际开发中如果我们不确定类名的话就没办法new一个实例了,为此java搞了一个反射出来。

所以反射就是在运行时才知道要操作的类是什么,并且可以在运行时获取类的完整构造,并调用对应的方法,来看例子

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;

public class fanshe {
    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }


    public static void main(String[] args) throws Exception {
        //正常的调用
        fanshe testObj = new fanshe();
        testObj.setName("tom");
        System.out.println("Obj name:" + testObj.getName());
        //使用反射调用
        Class clz = Class.forName("fanshe");
        Method setNameMethod = clz.getMethod("setName", String.class);
        Constructor testConstructor = clz.getConstructor();
        Object testObj1 = testConstructor.newInstance();
        setNameMethod.invoke(testObj1, "tom");
        Method getNameMethod = clz.getMethod("getName");
        System.out.println("Obj name:" + getNameMethod.invoke(testObj1));
    }
}

在这段代码中我们使用了两种方式来创建实例,第一种就是正常的new,第二种是使用反射来创建,我们主要看反射部分的。

首先使用Class.forName()来加载fanshe类,然后通过getMethod()来拿到fanshe类下的setName的方法和参数,通过getConstructor()拿到类的完整构造,通过testConstructor.newInstance()创建新实例,也就是new一个对象,最后通过setNameMethod.invoke(testObj1, "tom")来调用testObj对象的setName()方法赋值为"tom"。

两种方法的运行结果是一样的。

再来捋一下,正射是直接new,反射是invoke回调。个人理解,反射等同于php中的回调函数。

更多反射相关移步 大白话说Java反射:入门、使用、原理

文笔垃圾,措辞轻浮,内容浅显,操作生疏。不足之处欢迎大师傅们指点和纠正,感激不尽。