Java IO篇:序列化与反序列化

1、什么是序列化:

        两个服务之间要传输一个数据对象,就需要将对象转换成二进制流,通过网络传输到对方服务,再转换成对象,供服务方法调用。这个编码和解码的过程称之为序列化和反序列化。所以序列化就是把 Java 对象变成二进制形式,本质上就是一个byte[]数组。将对象序列化之后,就可以写入磁盘进行保存或者通过网络中输出给远程服务了。反之,反序列化可以从网络或者磁盘中读取的字节数组,反序列化成对象,在程序中使用。

2、序列化优点:

① 永久性保存对象:将对象转为字节流存储到硬盘上,即使 JVM 停机,字节流还会在硬盘上等待,等待下一次 JVM 启动时,反序列化为原来的对象,并且序列化的二进制序列能够减少存储空间

② 方便网络传输:序列化成字节流形式的对象可以方便网络传输(二进制形式),节约网络带宽

③ 通过序列化可以在进程间传递对象

3、序列化的几种方式:

参考文章:https://www.jianshu.com/p/7298f0c559dc

3.1、Java 原生序列化:

        Java 默认通过 Serializable 接口实现序列化,只要实现了该接口,该类就会自动实现序列化与反序列化,该接口没有任何方法,只起标识作用。Java序列化保留了对象类的元数据(如类、成员变量、继承类信息等),以及对象数据等,兼容性最好,但不支持跨语言,而且性能一般。

        实现 Serializable 接口的类在每次运行时,编译器会根据类的内部实现,包括类名、接口名、方法和属性等自动生成一个 serialVersionUID,serialVersionUID 主要用于验证对象在反序列化过程中,序列化对象是否加载了与序列化兼容的类,如果是具有相同类名的不同版本号的类,在反序列化中是无法获取对象的,显式地定义 serialVersionUID 有两种用途:

  • 在某些场合,希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有相同的 serialVersionUID;
  • 在某些场合,不希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有不同的 serialVersionUID;

如果源码改变,那么重新编译后的 serialVersionUID 可能会发生变化,因此建议一定要显示定义 serialVersionUID 的属性值。

3.2、Hessian 序列化:

        Hessian 序列化是一种支持动态类型、跨语言、基于对象传输的网络协议。Java 对象序列化的二进制流可以被其他语言反序列化。 Hessian 协议具有如下特性:

  • 自描述序列化类型。不依赖外部描述文件或接口定义, 用一个字节表示常用
  • 基础类型,极大缩短二进制流
  • 语言无关,支持脚本语言
  • 协议简单,比 Java 原生序列化高效

        Hessian 2.0 中序列化二进制流大小是 Java 序列化的 50%,序列化耗时是 Java 序列化的 30%,反序列化耗时是 Java 反序列化的20% 。

        Hessian 会把复杂对象所有属性存储在一个 Map 中进行序列化。所以在父类、子类存在同名成员变量的情况下, Hessian 序列化时,先序列化子类 ,然后序列化父类,因此反序列化结果会导致子类同名成员变量被父类的值覆盖。

3.3、Json 序列化:

        JSON 是一种轻量级的数据交换格式。JSON 序列化就是将数据对象转换为 JSON 字符串,在序列化过程中抛弃了类型信息,所以反序列化时需要提供类型信息才能准确地反序列化,相比前两种方式,JSON 可读性比较好,方便调试。

4、为什么不建议使用Java序列化

该部分主要参考文章:为什么我不建议你使用Java序列化 - 掘金

        目前主流框架很少使用到 Java 序列化,比如 SpringCloud 使用的 Json 序列化,Dubbo 虽然兼容 Java 序列化,但默认使用的是 Hessian 序列化。这是为什么呢?主要是因为 JDK 默认的序列化方式存在以下一些缺陷:无法跨语言、易被攻击、序列化的流太大、序列化性能太差等

4.1、无法跨语言:

        Java 序列化只支持 Java 语言实现的框架,其它语言大部分都没有使用 Java 的序列化框架,也没有实现 Java 序列化这套协议,因此,两个不同语言编写的应用程序之间通信,无法使用 Java 序列化实现应用服务间传输对象的序列化和反序列化。

4.2、易被攻击:

        对象是通过在 ObjectInputStream 上调用 readObject() 方法进行反序列化的,它可以将类路径上几乎所有实现了 Serializable 接口的对象都实例化。这意味着,在反序列化字节流的过程中,该方法可以执行任意类型的代码,这是非常危险的。

        对于需要长时间进行反序列化的对象,不需要执行任何代码,也可以发起一次攻击。攻击者可以创建循环对象链,然后将序列化后的对象传输到程序中反序列化,这种情况会导致 hashCode 方法被调用次数呈次方爆发式增长, 从而引发栈溢出异常。

        序列化通常会通过网络传输对象,而对象中往往有敏感数据,所以序列化常常成为黑客的攻击点,攻击者巧妙地利用反序列化过程构造恶意代码,使得程序在反序列化的过程中执行任意代码。 Java 工程中广泛使用的 Apache Commons Collections、Jackson、fastjson 等都出现过反序列化漏洞。如何防范这种黑客攻击呢?有些对象的敏感属性不需要进行序列化传输,可以加 transient 关键字,避免把此属性信息转化为序列化的二进制流,除此之外,声明为 static 类型的成员变量也不能要序列化。如果一定要传递对象的敏感属性,可以使用对称与非对称加密方式独立传输,再使用某个方法把属性还原到对象中。

4.3、序列化后的流太大

        序列化后的二进制流大小能体现序列化的性能。序列化后的二进制数组越大,占用的存储空间就越多,存储硬件的成本就越高。如果我们是进行网络传输,则占用的带宽就更多,这时就会影响到系统的吞吐量。

        Java 序列化中使用了 ObjectOutputStream 来实现对象转二进制编码,那么这种序列化机制实现的二进制编码完成的二进制数组大小,相比于 NIO 中的 ByteBuffer 实现的二进制编码完成的数组大小,有没有区别呢?

我们可以通过一个简单的例子来验证下:

User user = new User();
user.setUserName("test");
user.setPassword("test");
      
ByteArrayOutputStream os =new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(os);
out.writeObject(user);
byte[] testByte = os.toByteArray();
System.out.print("ObjectOutputStream 字节编码长度:" + testByte.length + "\n");
ByteBuffer byteBuffer = ByteBuffer.allocate(2048);

byte[] userName = user.getUserName().getBytes();
byte[] password = user.getPassword().getBytes();
byteBuffer.putInt(userName.length);
byteBuffer.put(userName);
byteBuffer.putInt(password.length);
byteBuffer.put(password);        
byteBuffer.flip();
byte[] bytes = new byte[byteBuffer.remaining()];
System.out.print("ByteBuffer 字节编码长度:" + bytes.length+ "\n");

运行结果:

ObjectOutputStream 字节编码长度:99
ByteBuffer 字节编码长度:16

 4.4、序列化性能太差:

        序列化的速度也是体现序列化性能的重要指标,如果序列化的速度慢,网络通信效率就低,从而增加系统的响应时间。我们再来通过上面这个例子,来对比下 Java 序列化与 NIO 中的 ByteBuffer 编码的性能:

    User user = new User();
    user.setUserName("test");
    user.setPassword("test");
      
    long startTime = System.currentTimeMillis();
      
     for(int i=0; i<1000; i++) {
        ByteArrayOutputStream os =new ByteArrayOutputStream();
          ObjectOutputStream out = new ObjectOutputStream(os);
          out.writeObject(user);
          out.flush();
          out.close();
          byte[] testByte = os.toByteArray();
          os.close();
     }
      
    long endTime = System.currentTimeMillis();
    System.out.print("ObjectOutputStream 序列化时间:" + (endTime - startTime) + "\n");
long startTime1 = System.currentTimeMillis();
for(int i=0; i<1000; i++) {
   ByteBuffer byteBuffer = ByteBuffer.allocate( 2048);

        byte[] userName = user.getUserName().getBytes();
        byte[] password = user.getPassword().getBytes();
        byteBuffer.putInt(userName.length);
        byteBuffer.put(userName);
        byteBuffer.putInt(password.length);
        byteBuffer.put(password);
            
        byteBuffer.flip();
        byte[] bytes = new byte[byteBuffer.remaining()];
}
long endTime1 = System.currentTimeMillis();
System.out.print("ByteBuffer 序列化时间:" + (endTime1 - startTime1)+ "\n");

运行结果:

ObjectOutputStream 序列化时间:29
ByteBuffer 序列化时间:6

猜你喜欢

转载自blog.csdn.net/a745233700/article/details/122445225