加载中...


什么是序列化、反序列化?

如果我们需要持久化Java对象比如将Java对象保存在文件中,或者在网络传输Java对象,这些场景都需要用到序列化。简单来说:

  • 序列化:将数据结构或对象转换成二进制字节流的过程
  • 反序列化:将在序列化过程中所生成的二进制字节流转换成数据结构或者对象的过程

对于Java这种面向对象编程语言来说,我们序列化的都是对象(Object)也就是实例化后的类(Class),但是在C++这种半面向对象的语言中,struct(结构体)定义的是数据结构类型,而class对应的是对象类型。

下面是序列化和反序列化常见应用场景:

  • 对象在进行网络传输(比如远程方法调用RPC的时候)之前需要先被序列化,接收到序列化的对象之后需要再进行反序列化;
  • 将对象存储到文件之前需要进行序列化,将对象从文件中读取出来需要进行反序列化;
  • 将对象存储到数据库(如Redis)之前需要用到序列化,将对象从缓存数据库中读取出来需要反序列化;
  • 将对象存储到内存之前需要进行序列化,从内存中读取出来之后需要进行反序列化。

维基百科是如是介绍序列化的:
序列化(serialization)在计算机科学的数据处理中,是指将数据结构或对象状态转换成可取用格式(例如存成文件,存于缓冲,或经由网络中发送),以留待后续在相同或另一台计算机环境中,能恢复原先状态的过程。依照序列化格式重新获取字节的结果时,可以利用它来产生与原始对象相同语义的副本。对于许多对象,像是使用大量引用的复杂对象,这种序列化重建的过程并不容易。面向对象中的对象序列化,并不概括之前原始对象所关系的函数。这种过程也称为对象编组(marshalling)。从一系列字节提取数据结构的反向操作,是反序列化(也称为解编组、deserialization、unmarshalling)。

综上:序列化的主要目的是通过网络传输对象或者说是将对象存储到文件系统、数据库、内存中

img

序列化协议对应于TCP/IP4层模型的哪一层?

我们知道网络通信的双方必须要采用和遵守相同的协议。TCP/IP四层模型是下面这样的,序列化协议属于哪一层呢?

  1. 应用层
  2. 传输层
  3. 网络层
  4. 网络接口层

TCP/IP四层模型

如上图所示,OSI七层协议模型中,表示层做的事情主要就是对应用层的用户数据进行处理转换为二进制流。反过来的话,就是将二进制流转换成应用层的用户数据。这不就对应的是序列化和反序列化么?OSI七层协议模型中的应用层、表示层和会话层对应的都是TCP/IP四层模型中的应用层,所以序列化协议属于TCP/IP协议应用层的一部分。

如果有些字段不想进行序列化怎么办?(transient)

对于不想进行序列化的变量,使用transient关键字修饰。

transient关键字的作用是:阻止实例中那些用此关键字修饰的的变量序列化;当对象被反序列化时,被transient修饰的变量值不会被持久化和恢复。关于transient还有几点注意:

  • transient只能修饰变量,不能修饰类和方法。
  • transient修饰的变量,在反序列化后变量值将会被置成类型的默认值。例如,如果是修饰int类型,那么反序列后结果就是0。
  • static变量因为不属于任何对象(Object),所以无论有没有transient关键字修饰,均不会被序列化。

常见序列化协议

JDK自带的序列化方式一般不会用,因为序列化效率低并且存在安全问题。比较常用的序列化协议有Hessian、Kryo、Protobuf、ProtoStuff,这些都是基于二进制的序列化协议。像JSON和XML这种属于文本类序列化方式。虽然可读性比较好,但是性能较差,一般不会选择。

JDK自带的序列化方式

JDK自带的序列化,只需实现java.io.Serializable接口即可。

@AllArgsConstructor
@NoArgsConstructor
@Getter
@Builder
@ToString
public class RpcRequest implements Serializable {
    private static final long serialVersionUID = 1905122041950251207L;
    private String requestId;
    private String interfaceName;
    private String methodName;
    private Object[] parameters;
    private Class<?>[] paramTypes;
    private RpcMessageTypeEnum rpcMessageTypeEnum;
}

serialVersionUID有什么作用?

序列化号serialVersionUID属于版本控制的作用。反序列化时,会检查serialVersionUID是否和当前类的serialVersionUID一致。如果serialVersionUID不一致则会抛出InvalidClassException异常。强烈推荐每个序列化类都手动指定其serialVersionUID,如果不手动指定,那么编译器会动态生成默认的serialVersionUID。

serialVersionUID不是被static变量修饰了吗?为什么还会被“序列化”?

static修饰的变量是静态变量,位于方法区,本身是不会被序列化的。static变量是属于类的而不是对象。你反序列之后,static变量的值就像是默认赋予给了对象一样,看着就像是static变量被序列化,实际只是假象罢了。

为什么不推荐使用JDK自带的序列化?

我们很少或者说几乎不会直接使用JDK自带的序列化方式,主要原因有下面这些原因:

  • 不支持跨语言调用:如果调用的是其他语言开发的服务的时候就不支持了。

  • 性能差:相比于其他序列化框架性能更低,主要原因是序列化之后的字节数组体积较大,导致传输成本加大。

  • 存在安全问题:序列化和反序列化本身并不存在问题。但当输入的反序列化的数据可被用户控制,那么攻击者即可通过构造恶意输入,让反序列化产生非预期的对象,在此过程中执行构造的任意代码。

    相关阅读:应用安全:JAVA反序列化漏洞之殇-CryinJava反序列化安全漏洞怎么回事?-Monica

Kryo

Kryo是一个高性能的序列化/反序列化工具,由于其变长存储特性并使用了字节码生成机制,拥有较高的运行速度和较小的字节码体积。另外,Kryo已经是一种非常成熟的序列化实现了,已经在Twitter、Groupon、Yahoo以及多个著名开源项目(如Hive、Storm)中广泛的使用。guide-rpc-framework就是使用的kryo进行序列化,序列化和反序列化相关的代码如下:

/**
 * Kryo serialization class, Kryo serialization efficiency is very high, but only compatible with Java language
 *
 * @author shuang.kou
 * @createTime 2020年05月13日 19:29:00
 */
@Slf4j
public class KryoSerializer implements Serializer {

    /**
     * Because Kryo is not thread safe. So, use ThreadLocal to store Kryo objects
     */
    private final ThreadLocal<Kryo> kryoThreadLocal = ThreadLocal.withInitial(() -> {
        Kryo kryo = new Kryo();
        kryo.register(RpcResponse.class);
        kryo.register(RpcRequest.class);
        return kryo;
    });

    @Override
    public byte[] serialize(Object obj) {
        try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
             Output output = new Output(byteArrayOutputStream)) {
            Kryo kryo = kryoThreadLocal.get();
            // Object->byte:将对象序列化为byte数组
            kryo.writeObject(output, obj);
            kryoThreadLocal.remove();
            return output.toBytes();
        } catch (Exception e) {
            throw new SerializeException("Serialization failed");
        }
    }

    @Override
    public <T> T deserialize(byte[] bytes, Class<T> clazz) {
        try (ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
             Input input = new Input(byteArrayInputStream)) {
            Kryo kryo = kryoThreadLocal.get();
            // byte->Object:从byte数组中反序列化出对象
            Object o = kryo.readObject(input, clazz);
            kryoThreadLocal.remove();
            return clazz.cast(o);
        } catch (Exception e) {
            throw new SerializeException("Deserialization failed");
        }
    }

}

Github地址

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;
}

Github地址

ProtoStuff

由于Protobuf的易用性,它的哥哥Protostuff诞生了。protostuff基于Google protobuf,但是提供了更多的功能和更简易的用法。虽然更加易用,但是不代表ProtoStuff性能更差。

Github地址

Hessian

Hessian是一个轻量级的,自定义描述的二进制RPC协议。Hessian是一个比较老的序列化实现了,并且同样也是跨语言的。

img

Dubbo2.x默认启用的序列化方式是Hessian2,但是,Dubbo对Hessian2进行了修改,不过大体结构还是差不多。

Fury

Fury是一个基于JIT动态编译和零拷贝的多语言序列化框架,支持Java/Python/Golang/JavaScript/C++等语言,提供全自动的对象多语言/跨语言序列化能力,和相比 JDK最高170倍的性能。

GitHub地址
官方网站

测试用例:

import io.fury.Fury;
import io.fury.Language;
import io.fury.ThreadSafeFury;

public class StudentDTO {
    private Long id;
    private String name;
    private int age;
    // ... 省略 getter setter

    public static void main(String[] args) {
        StudentDTO studentDTO = new StudentDTO();
        studentDTO.setAge(18);
        studentDTO.setName("Java极客技术");
        studentDTO.setId(1L);

        Fury fury = Fury.builder().withLanguage(Language.JAVA).build();
        fury.register(StudentDTO.class);
        byte[] bytes = fury.serialize(studentDTO);
        System.out.println(fury.deserialize(bytes));


        ThreadSafeFury fury2 = Fury.builder().withLanguage(Language.JAVA)
                // Allow to deserialize objects unknown types,
                // more flexible but less secure.
//                 .withSecureMode(false)
                .buildThreadSafeFury();
        fury2.getCurrentFury().register(StudentDTO.class);
        byte[] bytes2 = fury2.serialize(studentDTO);
        System.out.println(fury2.deserialize(bytes2));
    }
}

构造Fury的时候我们可以构造单线程Fury或者线程安全的Fuyr:

创建单线程Fury

Fury fury = Fury.builder()
  .withLanguage(Language.JAVA)
  // 是否开启循环引用,如果并不需要的话可以关闭,提升性能
  .withRefTracking(false)
  // 是否压缩 Integer 或者 Long 类型的数字
  // .withNumberCompressed(true)
  // 类型前后一致性兼容
  .withCompatibleMode(CompatibleMode.SCHEMA_CONSISTENT)
  // .withCompatibleMode(CompatibleMode.COMPATIBLE)
  // 是否开启多线程编译
  .withAsyncCompilationEnabled(true)
  .build();
byte[] bytes = fury.serialize(object);
System.out.println(fury.deserialize(bytes));

创建线程安全的Fury

ThreadSafeFury fury = Fury.builder()
  .withLanguage(Language.JAVA)
  .withRefTracking(false)
  .withCompatibleMode(CompatibleMode.SCHEMA_CONSISTENT)
  .withAsyncCompilationEnabled(true)
  .buildThreadSafeFury();
byte[] bytes = fury.serialize(object);
System.out.println(fury.deserialize(bytes));

零拷贝用例

零拷贝(Zero Copy)是一种优化技术,旨在减少数据在内存中的复制操作,提高数据传输和处理的效率。
在传统的拷贝操作中,当数据从一个位置(例如磁盘、网络等)传输到另一个位置(例如应用程序的内存),通常需要将数据从源位置复制到中间缓冲区,然后再从中间缓冲区复制到目标位置。这种复制操作会占用时间、CPU和内存资源。而零拷贝技术通过避免这些不必要的数据复制,从而提高数据传输和处理的效率。具体而言,它允许数据在不进行额外复制的情况下直接从源位置传输到目标位置。
在实现零拷贝时,通常会使用一些特定的技术和API,如操作系统提供的零拷贝接口、内核缓冲区、DMA(直接内存访问)等。这些技术可以减少或消除不必要的数据复制,从而提高系统的性能和吞吐量。
零拷贝技术在处理大量数据的场景中特别有用,例如高性能网络传输、文件 I/O、数据库操作等。它可以减少不必要的CPU负担和内存消耗,并提高数据的传输速度和处理效率。

public class StudentDTO {
    private Long id;
    private String name;
    private int age;

    public static void main(String[] args) {
        // 在main方法中创建一个Fury实例,该实例用于进行序列化和反序列化操作
        Fury fury = Fury.builder()
                .withLanguage(Language.JAVA)
                .build();
        // 创建一个包含不同类型对象的列表list
        List<Object> list = Arrays.asList("str", new byte[1000], new int[100], new double[100]);
        // 创建一个存储BufferObject的集合bufferObjects,用于在序列化过程中收集需要单独处理的对象(即非基本类型对象)
        Collection<BufferObject> bufferObjects = new ArrayList<>();
        // 使用Fury的serialize方法对列表进行序列化,并传入一个lambda表达式来判断对象是否需要单独处理(即加入bufferObjects集合)
        byte[] bytes = fury.serialize(list, e -> !bufferObjects.add(e));
        // 使用Java8的流操作将bufferObjects集合转换为MemoryBuffer类型集合buffers
        List<MemoryBuffer> buffers = bufferObjects.stream().map(BufferObject::toBuffer).collect(Collectors.toList());
        // 使用Fury的deserialize方法对序列化的字节数组bytes进行反序列化,并传入buffers集合用于零拷贝操作
        System.out.println(fury.deserialize(bytes, buffers));
    }
}

这段代码的主要目的是将一个包含不同类型对象的列表进行序列化和反序列化操作,其中序列化操作通过Fury库实现零拷贝。

总结

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

img

像Protobuf、ProtoStuff、hessian这类都是跨语言的序列化方式,如果有跨语言需求的话可以考虑使用。除了我上面介绍到的序列化方式的话,还有像Thrift,Avro这些。

原文链接

测试代码

public class SerializableTest {
    public static void main(String[] args) throws Exception {
        serializeFlyPig();
        FlyPig flyPig = deserializeFlyPig();
        System.out.println(flyPig.toString());
    }
    /**
     * 序列化
     */
    private static void serializeFlyPig() throws Exception {
        FlyPig flyPig = new FlyPig();
        flyPig.setColor("black");
        flyPig.setName("riemann");
        flyPig.setName("audi");
        // ObjectOutputStream对象输出流,将flyPig对象存储到E盘的flyPig.txt文件中,完成对flyPig对象的序列化操作
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(new File("C:\\Users\\wangx\\Desktop\\flypig.txt")));
        oos.writeObject(flyPig);
        System.out.println("FlyPig 对象序列化成功!");
        oos.close();
    }

    /**
     * 反序列化
     */
    private static FlyPig deserializeFlyPig() throws Exception {
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("C:\\Users\\wangx\\Desktop\\flypig.txt")));
        FlyPig pig = (FlyPig) ois.readObject();
        System.out.println("FlyPig 对象反序列化成功!");
        return pig;
    }

    /**
     * map写文件
     */
    public void writeFileByMap(Map<String,Object> map){
        File file = new File("");
        try{
            StringBuffer stringBuffer = new StringBuffer();
            FileWriter fileWriter = new FileWriter(file,true);
            Set<Entry<String,Object>> set = map.entrySet();
            Iterator<Entry<String,Object>> it = set.iterator();
            while(it.hasNext()){
                Map.Entry<String,Object> en = it.next();
                if(en.getKey().equals("anObject")){
                    stringBuffer.append(en.getKey()+":"+en.getValue()).append(System.getProperty("line.separator"));

                }
            }
            fileWriter.write(stringBuffer.toString());
            fileWriter.close();

        }catch(Exception e){
            e.printStackTrace();
        }
    }

}

class FlyPig implements Serializable {

    private static final long serialVersionUID = 1L;
    
    private static String AGE = "269";
    private String name;
    private String color;
    transient private String car;

    private String addTip;

    public String getName() {
        return name;
    }

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

    public String getColor() {
        return color;
    }

    public void setColor(String color) {
        this.color = color;
    }

    public String getCar() {
        return car;
    }

    public void setCar(String car) {
        this.car = car;
    }

    public String getAddTip() {
        return addTip;
    }

    public void setAddTip(String addTip) {
        this.addTip = addTip;
    }

    @Override
    public String toString() {
        return "FlyPig{" + "name='" + name + '\'' + ", color='" + color + '\'' + ", car='" + car + '\'' + ", AGE='"
                + AGE + '\'' + '}';
    }

}

相关文章

Java Serializable:明明就一个空的接口嘛 什么是序列化,怎么序列化,为什么序列化,反序列化会遇到什么问题,如何解决 Java序列化和反序列化为什么要实现Serializable接口
关于序列化/反序列化,我梭哈

文章作者: xmxe
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 xmxe !
 上一篇
代理 代理
代理模式代理模式是一种比较好理解的设计模式。简单来说就是我们使用代理对象来代替对真实对象(real object)的访问,这样就可以在不修改原目标对象的前提下,提供额外的功能操作,扩展目标对象的功能。代理模式的主要作用是扩展目标对象的功能,
下一篇 
Spring中的Bean对象 Spring中的Bean对象
Spring Bean、生命周期、扩展接口
  目录