探索Java的序列化和反序列化

Java的序列化和反序列化

1、探索Serializable和对象的序列化的关系

所谓序列化就是将内存中的Java对象转换成字节,从而持久化到硬盘或者用于网络的传输。

接下来就来看一下Java的是怎么将一个对象进行序列化的。(此处不借助第三方的包
正常的一个实体类:

import java.io.Serializable;

/**
 * Description:
 *
 * @author:qjx
 * @date:2021/12/15
 */
public class Food implements Serializable {
    private String name;

    public Food(String name) {
        this.name = name;
    }

    //setter getter
}

测试代码如下:

ObjectOutputStream:对象的序列化流

作用:把对象转成字节数据的输出到文件中保存,对象的输出过程称为序列化,可实现对象的持久存储。

private static void serialize() throws IOException {
    Food food = new Food("唐僧肉~~");
    ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:\\food.txt"));
    oos.writeObject(food);
    oos.close();
}

这样我们就得到一个对象转出的二进制文件food.txt,相反的可以将该文家反序列化到内存,测试代码如下:

ObjectInputStream :反序列化流

将之前使用 ObjectOutputStream 序列化的原始数据恢复为对象,以流的方式读取对象。

private static void deserialize() throws IOException, ClassNotFoundException {
    ObjectInputStream ois = new ObjectInputStream(new FileInputStream("food.txt"));
    Food o = (Food)ois.readObject();
    System.out.println(o.getName()); //唐僧肉~~
}

如果我们恰巧在实体类在序列化是并没有实现Serializable接口的话,就会得到这样的异常:java.io.NotSerializableException如下图:

image-20211216202618400

我们点开Serializable接口的可以看到就是一个空的接口:

image-20211216203332028

官方给出的接口的部分文档如下:

类的可序列化由实现 java.io.Serializable 接口的类启用。 未实现此接口的类将不会对其任何状态进行序列化或反序列化。 可序列化类的所有子类型本身都是可序列化的。 序列化接口没有方法或字段,仅用于标识可序列化的语义。

可以看出Serializable就是一个普通java类是否可以被序列化的flag具体报错我们跟进ObjectOutputStream类。

我们跟进到writeObject0 line 1184,如下图,从而得出StringArrayEnum、和Serializable只有这些类型的类型的对象才能进行write,否则就会抛出此异常,很明显我们的Food类并不是其中之一。

image-20211216202856819

总结:在对象序列化为二进制文件时只有以下类型的类的对象可以被序列化

  • String
  • Array
  • Enum
  • Serializable

2、探索serialVersionUID序列号与对象序列化的关系

众所周知,在实现了Serializable接口的类中都有一个serialVersionUID字段,如下图:

image-20211219134157322

如果我们没有明显声明,运行时也会自动生成该字段,那么serialVersionUID号有何用?

验证过程:在上面我我们已经获取了food.txt序列化的类,在反序列化时我们增加一个字段,如下:

public class Food implements Serializable {
    private String name;
    private String id; //新增id字段验证serialVersionUID作用
    //...
}

这是我们试着将该类反序列化到内存就看到了下面的报错:

java.io.InvalidClassException:xxx.Food; local class incompatible: stream classdesc serialVersionUID = 6153504616478967942, local class serialVersionUID = -6029597911198096662

这说的就是在这两个Food类不相容,原因是serialVersionUID不相等。

image-20211219135720367

跟进到ObjectStreamClass类的对应报错位置:

image-20211219141029627

suid就是反序列化出来的类的serialVersionUID, osc就是本地类的序列号,想到咱们的类并未声明序列号,所以跟进就看到下面赋值代码,也就是自动生成serialVersionUID的代码:

image-20211219140713285

总结:

  • 1、serialVersionUID是序列化前后的唯一标识符

  • 2、默认如果没有人为显式定义过serialVersionUID,那编译器会为它自动声明一个!

  • 3、凡是实现Serializable接口的类中,最好都要手动添加此字段。

注意:

  • 1、凡是被static修饰的字段是不会被序列化的
  • 2、凡是被transient修饰符修饰的字段也是不会被序列化的

3、序列化的受控和加强

3.1、约束性加持

从上面的过程可以看出,序列化和反序列化的过程其实是有漏洞的,因为从序列化到反序列化是有中间过程的,如果被别人拿到了中间字节流,然后加以伪造或者篡改,那反序列化出来的对象就会有一定风险了。

毕竟反序列化也相当于一种 “隐式的”对象构造 ,因此我们希望在反序列化时,进行受控的对象反序列化动作。

那怎么个受控法呢?

答案就是: 自行在需要反序列化的类中编写readObject()函数,用于对象的反序列化构造,从而提供约束性。

既然自行编写readObject()函数,那就可以做很多可控的事情:比如各种判断工作。

还以上面的Food类为例,规定我们的name中不能出现 字,我们可以自行编写readObject()函数用于反序列化的控制:

private void readObject( ObjectInputStream objectInputStream ) throws IOException, ClassNotFoundException {
        // 调用默认的反序列化函数
        objectInputStream.defaultReadObject();

        // 手工检查反序列化后name中不能出现 `肉` 字
        if( name!= null && name.contains("肉")) {
            throw new IllegalArgumentException("name不可以出现肉哦!");
        }
}

为什么自定义的privatereadObject()方法可以被自动调用,我们跟进ObjectStreamClass源码:

image-20211219144616294

发现在ObjectStreamClass构造时就进行私有方法的反射获取了!!!

3.2、单例模式增强

一个容易被忽略的问题是:可序列化的单例类有可能并不单例

举个代码小例子就清楚了。

比如这里我们先用java写一个常见的「静态内部类」方式的单例模式实现:

public class Singleton implements Serializable {

    private static final long serialVersionUID = -1576643344804979563L;

    private Singleton() {
    }

    private static class SingletonHolder {
        private static final Singleton singleton = new Singleton();
    }

    public static synchronized Singleton getSingleton() {
        return SingletonHolder.singleton;
    }
}

然后写一个验证主函数:

public class Test2 {

    public static void main(String[] args) throws IOException, ClassNotFoundException {

        ObjectOutputStream objectOutputStream =
                new ObjectOutputStream(
                    new FileOutputStream( new File("singleton.txt") )
                );
        // 将单例对象先序列化到文本文件singleton.txt中
        objectOutputStream.writeObject( Singleton.getSingleton() );
        objectOutputStream.close();

        ObjectInputStream objectInputStream =
                new ObjectInputStream(
                    new FileInputStream( new File("singleton.txt") )
                );
        // 将文本文件singleton.txt中的对象反序列化为singleton1
        Singleton singleton1 = (Singleton) objectInputStream.readObject();
        objectInputStream.close();

        Singleton singleton2 = Singleton.getSingleton();

        // 运行结果竟打印 false !
        System.out.println( singleton1 == singleton2 );
    }

}

运行后我们发现:反序列化后的单例对象和原单例对象并不相等了,这无疑没有达到我们的目标。

解决办法是:在单例类中手写readResolve()函数,直接返回单例对象,来规避之:

private Object readResolve() {
    return SingletonHolder.singleton;
}

这样一来,当反序列化从流中读取对象时,readResolve()会被调用,用其中返回的对象替代反序列化新建的对象。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Q J X

你的鼓励将是我创作的最大动力

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

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

打赏作者

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

抵扣说明:

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

余额充值