Java序列化详解

目录

一、什么是序列化

二、什么是反序列化

三、序列化和反序列化的作用

四、序列化和反序列化应用案例

五、常见序列化协议对比

5.1 JDK 自带的序列化方式

5.2 JDK序列化的缺陷

1. 无法跨语言

2. 易被攻击

3. 序列化后的流太大

4. 序列化性能太差

5.3 Kryo

5.4 Protobuf

5.5 总结


一、什么是序列化

序列化是指将对象转化为字节流的过程,以便于存储或传输。在序列化过程中,对象的状态被保存为一连串的字节,可以将这些字节保存到文件中或通过网络传输。序列化后的字节流可以在需要时进行反序列化,将字节流重新转化为对象,并恢复对象的状态。

在Java中,对象的序列化是通过实现Serializable接口来实现的。Serializable接口是一个标记接口,没有任何方法,只是用于标识一个类可以被序列化。当一个类实现了Serializable接口,它的对象就可以被序列化为字节流。

序列化在很多场景中都有应用,例如在分布式系统中,可以将对象序列化后通过网络传输,或者将对象存储到缓存中。但需要注意的是,序列化和反序列化可能会引发安全问题,因此在进行序列化和反序列化操作时,要谨慎处理。

二、什么是反序列化

反序列化是指将字节流转化为对象的过程,与序列化相反。在反序列化过程中,字节流被重新组装成对象,并恢复对象的状态。在JDK中,反序列化是通过ObjectInputStream类来实现的。ObjectInputStream类提供了readObject()方法,用于从输入流中读取对象。

反序列化是序列化的逆过程,可以将序列化后的字节流重新转化为原始的对象。这在很多场景中都有应用,例如在分布式系统中,可以将序列化后的对象通过网络传输,然后在接收方进行反序列化,恢复原始的对象。需要注意的是,在进行反序列化操作时,需要确保序列化和反序列化的版本一致,否则可能会出现不兼容的问题。

三、序列化和反序列化的作用

序列化和反序列化是用于在Java中将对象转换为字节流并从字节流中恢复对象的过程。它们的作用主要有以下几个方面:

数据持久化:通过序列化,可以将对象转换为字节流并保存到文件系统或数据库中。这样可以实现数据的持久化存储,方便后续的读取和使用。

对象传输:通过序列化,可以将对象转换为字节流,并在网络中传输。这在分布式系统、远程方法调用等场景中非常常见。发送方将对象序列化为字节流,通过网络发送给接收方,接收方再通过反序列化将字节流转换为对象。

缓存机制:序列化可以用于缓存机制,将对象序列化后存储在缓存中,以提高系统性能和响应速度。当需要使用对象时,可以直接从缓存中反序列化获取对象,避免了频繁的数据库访问或计算操作。

跨平台和跨语言通信:通过序列化,可以将对象转换为字节流,使得对象在不同的平台和不同的编程语言之间进行通信成为可能。只要各方都能正确地进行序列化和反序列化操作,就可以实现跨平台和跨语言的通信。

序列化和反序列化提供了一种方便的方式来将对象转换为字节流,并在需要时恢复为原始对象。它们在数据持久化、对象传输、缓存机制以及跨平台和跨语言通信等方面都有广泛的应用。

四、序列化和反序列化应用案例

序列化和反序列化在实际应用中有很多用途,以下是一些常见的应用案例:

数据库缓存:在Web应用中,为了提高性能,我们通常会将一些经常使用的数据缓存到内存中,以减少数据库的访问次数。这时,我们可以将数据对象序列化为字节流,存储到缓存中。当需要使用数据时,我们可以从缓存中读取字节流,并反序列化为对象,以提高访问速度。

分布式系统通信:在分布式系统中,不同的节点之间需要进行通信,传递数据对象是非常常见的操作。这时,我们可以将数据对象序列化为字节流,并通过网络传递。接收方再将字节流反序列化为对象,以获取数据。

消息队列:消息队列是一种常用的异步通信方式,可以将消息对象序列化为字节流,放入消息队列中。消费者再从消息队列中获取字节流,并反序列化为对象,以获取消息内容。

远程方法调用:在分布式系统中,我们通常需要调用远程节点的方法。这时,我们可以将方法参数对象序列化为字节流,通过网络传递给远程节点,并在远程节点上反序列化为对象,以调用方法。

对象持久化:在一些应用中,我们需要将数据对象持久化到磁盘中,以便下次启动应用时能够恢复数据。这时,我们可以将数据对象序列化为字节流,存储到磁盘中。下次启动应用时,我们可以从磁盘中读取字节流,并反序列化为对象,以恢复数据。

序列化和反序列化在很多应用场景中都有广泛的应用,可以方便地将对象转换为字节流,并在需要时恢复为原始对象。

五、常见序列化协议对比

​ JDK 自带的序列化方式一般不会用 ,因为序列化效率低并且部分版本有安全漏洞。(为啥效率低?安全漏洞又是啥?)比较常用的序列化协议有 hessian、kyro、protostuff。

​ 下面提到的都是基于二进制的序列化协议,像 JSON 和 XML 这种属于文本类序列化方式。虽然 JSON 和 XML 可读性比较好,但是性能较差,一般不会选择。

5.1 JDK 自带的序列化方式

​ JDK 自带的序列化,只需实现 java.io.Serializable接口即可。比如下面的Student类:

@NoArgsConstructor
@AllArgsConstructor
@Getter
@Builder
@ToString
public class Student implements Serializable {
    private static final long serialVersionUID = 1905122041950251207L;
    private String name;
    private transient Integer age;
    private String address;
}

private transient Integer age中的transient表示不会序列化该属性,当对象被序列化时该属性age不会被序列化,反序列化时,该属性是以默认值赋值。比如下面反序列化时,age的值为空。

public class SerializeOperation {
    /**
     * 序列化对象,并使用了try-with-resources
     * @param student
     * @param path
     * @throws IOException
     */
    public static void serializeToFile(Student student, String path) throws IOException {
        Student s = new Student("小明",16,"翻斗花园");
        try(FileOutputStream fileOut = new FileOutputStream(path);
            ObjectOutputStream out = new ObjectOutputStream(fileOut);) {
            out.writeObject(s);
        }catch (Exception e){
            e.printStackTrace();
        }
    }
    /**
     * 反序列化对象
     * @param path
     */
    public static void deserializationFromFile(String path){
        try(FileInputStream fileIn = new FileInputStream(path);
            ObjectInputStream in = new ObjectInputStream(fileIn);){
            Student s = (Student) in.readObject();
            System.out.println(s.getAddress());
            System.out.println(s.getAge());
            System.out.println(s.getName());
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

上面使用了try-with-resources可以自动关闭任何实现 java.lang.AutoCloseable或者 java.io.Closeable 的对象。这样我们就不用在finally再关闭了。

主程序:

public class mainPractice {
    public static void main(String[] args) throws IOException {
        Student toBeSerializedObject = new Student("小明",16,"翻斗花园!");
        StringBuffer path = new StringBuffer("E:");
        path.append(File.separator).append("test").append(File.separator).append("student.ser");
        SerializeOperation.serializeToFile(toBeSerializedObject, path.toString());
        SerializeOperation.deserializationFromFile(path.toString());
    }
}

5.2 JDK序列化的缺陷

​ 我们在用过的RPC(远程方法调用)通信框架中,很少会发现使用JDK提供的序列化,主要是因为JDK默认的序列化存在着如下一些缺陷:

1. 无法跨语言

​ 现在很多系统的复杂度很高,采用多种语言来编码,而Java序列化目前只支持Java语言实现的框架,其它语言大部分都没有使用Java的序列化框架,也没有实现Java序列化这套协议,因此,如果两个基于不同语言编写的应用程序之间通信,使用Java序列化,则无法实现两个应用服务之间传输对象的序列化和反序列化。 像JSON序列化的话就可以跨语言,因为JSON这种数据格式是通用的。

2. 易被攻击

​ Java官网安全编码指导方针里有说明,“对于不信任数据的反序列化,从本质上来说是危险的,应该避免“。可见Java序列化并不是安全的。

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

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

Set root = new HashSet();  
Set s1 = root;  
Set s2 = new HashSet();  
for (int i = 0; i < 100; i++) {  
   Set t1 = new HashSet();  
   Set t2 = new HashSet();  
   t1.add("test"); //使t2不等于t1  
   s1.add(t1);  
   s1.add(t2);  
   s2.add(t1);  
   s2.add(t2);  
   s1 = t1;  
   s2 = t2;   
} 

如何解决这个漏洞?

​ 很多序列化协议都制定了一套数据结构来保存和获取对象。例如,JSON 序列化、ProtocolBuf 等,它们只支持一些基本类型和数组数据类型,这样可以避免反序列化创建一些不确定的实例。虽然它们的设计简单,但足以满足当前大部分系统的数据传输需求。我们也可以通过反序列化对象白名单来控制反序列化对象,可以重写 resolveClass 方法,并在该方法中校验对象名字。代码如下所示:

@Override
protected Class resolveClass(ObjectStreamClass desc) throws IOException,ClassNotFoundException {
	if (!desc.getName().equals(Bicycle.class.getName())) {
		throw new InvalidClassException(
		"Unauthorized deserialization attempt", desc.getName());
	}
	return super.resolveClass(desc);
}
3. 序列化后的流太大

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

​ Java 序列化中使用了 ObjectOutputStream 来实现对象转二进制编码,那么这种序列化机制实现的二进制编码完成的二进制数组大小,相比于 NIO 中的 ByteBuffer 实现的二进制编码完成的数组大小,要大上几倍。

4. 序列化性能太差

​ Java 序列化中的编码耗时要比 ByteBuffer 长很多

5.3 Kryo

​ Kryo 是一个高性能的序列化/反序列化工具,由于其变长存储特性并使用了字节码生成机制,拥有较高的运行速度和较小的字节码体积。

​ 另外,Kryo 已经是一种非常成熟的序列化实现了,已经在 Twitter、Groupon、Yahoo 以及多个著名开源项目(如 Hive、Storm)中广泛的使用。刚刚序列化和反序列化Student的案例在Kryo上使用如下:

public class KryoSerializerOperation {
    public static void serializeToFile(Object toBeSerializedObject, String path){
        Kryo kryo = new Kryo();
        kryo.register(toBeSerializedObject.getClass());
        try (Output output = new Output(new FileOutputStream(path));){
            kryo.writeObject(output, toBeSerializedObject);
        }catch (Exception e){
            e.printStackTrace();
        }
    }
    public static void deSerializeFromFile(Class toBeSerializedObject, String path){
        Kryo kryo = new Kryo();
        kryo.register(toBeSerializedObject);
        try (Input input = new Input(new FileInputStream(path));){
            Student s = (Student) kryo.readObject(input, toBeSerializedObject);
            System.out.println(s);
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

主程序:

public class mainPractice {
    public static void main(String[] args) {
        KryoSerializerOperation.serializeToFile(ConstantUsedBySerialization.student
                ,ConstantUsedBySerialization.path);
        KryoSerializerOperation.deSerializeFromFile(ConstantUsedBySerialization.student.getClass()
                ,ConstantUsedBySerialization.path);
    }
}

其中的变量:

public class ConstantUsedBySerialization {
    public static Student student = new Student("小明",16,"翻斗花园");
    public static String path = "E:"+ File.separator+"test"+File.separator+"student.ser";
}

5.4 Protobuf

​ Protobuf 出自于 Google,性能还比较优秀,也支持多种语言,同时还是跨平台的。就是在使用中过于繁琐,因为你需要自己定义 IDL 文件和生成对应的序列化代码。这样虽然不然灵活,但是,另一方面导致 protobuf 没有序列化漏洞的风险。

Protobuf 包含序列化格式的定义、各种语言的库以及一个 IDL 编译器。正常情况下你需要定义 proto 文件,然后使用 IDL 编译器编译成你需要的语言

​ 一个简单的 proto 文件如下:

// protobuf的版本
syntax = "proto3";
// SearchRequest会被编译成不同的编程语言的相应对象,比如Java中的class、Go中的struct
message Person {
  //string类型字段
  string name = 1;
  // int 类型字段
  int32 age = 2;
}

5.5 总结

​ Kryo 是专门针对 Java 语言序列化方式并且性能非常好,如果你的应用是专门针对 Java 语言的话可以考虑使用,并且 Dubbo 官网的一篇文章中提到说推荐使用 Kryo 作为生产环境的序列化方式。

你可能感兴趣的:(java,java,序列化,kryo,Protobuf)