Please enable Javascript to view the contents

Java 反序列化基础

 ·   ·  ☕ 4 分钟

    又臭又长🙃

    java反序列化学习

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

    反序列化demo

     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
    43
    
    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反射:入门、使用、原理

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

    分享
    您的鼓励是我最大的动力
    alipay QR Code
    wechat QR Code

    目录